From d5eda96efd957497dab331f805355a435355cbbc Mon Sep 17 00:00:00 2001 From: Gaurav Vaidya Date: Tue, 16 Jan 2024 00:53:35 -0500 Subject: [PATCH 01/23] First stab at supporting multiple phylogenies. --- src/components/phylogeny/Phylotree.vue | 25 ++++++++++++++++++++++++- src/components/phyx/PhyxView.vue | 19 +++++++++++++++++++ 2 files changed, 43 insertions(+), 1 deletion(-) diff --git a/src/components/phylogeny/Phylotree.vue b/src/components/phylogeny/Phylotree.vue index 1f9a257f..03bc514d 100644 --- a/src/components/phylogeny/Phylotree.vue +++ b/src/components/phylogeny/Phylotree.vue @@ -42,6 +42,7 @@ import jQuery from "jquery"; import { PhylogenyWrapper, PhylorefWrapper } from "@phyloref/phyx"; import { addCustomMenu } from "phylotree/src/render/menus"; import { saveAs } from "filesaver.js-npm"; +import {text} from "@fortawesome/fontawesome-svg-core"; /* * Note that this requires the Phylotree Javascript to be loaded in the HTML @@ -68,6 +69,11 @@ export default { type: String, required: false, }, + phylorefNoFilter: { + // If true, then don't filter phyloreferences: display them all! + type: Boolean, + default: false, + } }, data() { return { @@ -367,7 +373,7 @@ export default { ); // If the internal label has the same IRI as the currently selected - // phyloreference's reasoned node, further mark it as the resolved node. + // phyloreference's reasoned node, further mark or label it as the resolved node. // // Note that this node might NOT be labeled, in which case we need to // label it now! @@ -394,6 +400,23 @@ export default { "id", `current_pinning_node_phylogeny_${this.phylogenyIndex}` ); + + // If we have phylorefNoFilter set, then + if (this.phylorefNoFilter) { + // Make sure we don't already have an internal label node on this SVG node! + let textLabel = element.selectAll("text"); + + if (textLabel.empty()) textLabel = element.append("text"); + console.log(`Found text label `, textLabel); + let textLabelText = textLabel.text; + if (!textLabelText) textLabelText = data.name; + else textLabelText = textLabelText + '_and_' + data.name; + textLabel + .classed("internal-label", true) + .text(textLabelText) + .attr("dx", "0.3em") + .attr("dy", "0.35em"); + } } // Maybe this isn't a pinning node, but it is a child of a pinning node. diff --git a/src/components/phyx/PhyxView.vue b/src/components/phyx/PhyxView.vue index 3a572e7c..7894afeb 100644 --- a/src/components/phyx/PhyxView.vue +++ b/src/components/phyx/PhyxView.vue @@ -271,6 +271,23 @@ + + + @@ -293,10 +310,12 @@ import { PhylorefWrapper, PhylogenyWrapper, TaxonNameWrapper, TaxonomicUnitWrapper, } from '@phyloref/phyx'; import { newickParser } from 'phylotree'; +import Phylotree from "@/components/phylogeny/Phylotree.vue"; export default { name: 'PhyxView', components: { + Phylotree, BIconTrash, }, computed: { From 374c865be26fb664de494c81bf01c9b991f452c4 Mon Sep 17 00:00:00 2001 From: Gaurav Vaidya Date: Wed, 17 Jan 2024 01:48:47 -0500 Subject: [PATCH 02/23] Fixed phylogeny-index type. --- src/components/phyx/PhyxView.vue | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/phyx/PhyxView.vue b/src/components/phyx/PhyxView.vue index 7894afeb..73c90b6c 100644 --- a/src/components/phyx/PhyxView.vue +++ b/src/components/phyx/PhyxView.vue @@ -282,7 +282,7 @@
From e8a9d0519fa48a11b897a6b696d1404ecab128a1 Mon Sep 17 00:00:00 2001 From: Gaurav Vaidya Date: Wed, 17 Jan 2024 02:18:50 -0500 Subject: [PATCH 03/23] Updated Phylotree phyloref to phylorefs. This allows us to support multiple phylorefs. --- src/components/phylogeny/Phylotree.vue | 417 +++++++++++------------ src/components/phyloref/PhylorefView.vue | 2 +- src/components/phyx/PhyxView.vue | 1 + 3 files changed, 210 insertions(+), 210 deletions(-) diff --git a/src/components/phylogeny/Phylotree.vue b/src/components/phylogeny/Phylotree.vue index 03bc514d..ab939e44 100644 --- a/src/components/phylogeny/Phylotree.vue +++ b/src/components/phylogeny/Phylotree.vue @@ -53,7 +53,7 @@ export default { name: "Phylotree", props: { phylogeny: Object, // The phylogeny to render. - phyloref: Object, // The phyloreference to highlight. + phylorefs: Array, // The phyloreferences to highlight. spacingX: { // Spacing in the X axis in pixels. type: Number, @@ -68,11 +68,6 @@ export default { // The selected node label. If not set, we display all node labels. type: String, required: false, - }, - phylorefNoFilter: { - // If true, then don't filter phyloreferences: display them all! - type: Boolean, - default: false, } }, data() { @@ -152,7 +147,6 @@ export default { const newickStr = this.tree.getNewick((node) => { // Is the resolved node for this phyloref? If so, let's make an annotation. if ( - this.phyloref !== undefined && has(node, "data") && has(node.data, "@id") ) { @@ -164,52 +158,54 @@ export default { return '"' + str.replaceAll('"', "''") + '"'; }; - if ( - this.$store.getters - .getResolvedNodesForPhylogeny(this.phylogeny, this.phyloref) - .includes(data["@id"]) - ) { - if (has(this.phyloref, "@id")) { - annotations.push( - "phyloref:actual=" + - convertToNexusAnnotationValue(this.phyloref["@id"]) - ); - } + this.phylorefs.forEach(phyloref => { + if ( + this.$store.getters + .getResolvedNodesForPhylogeny(this.phylogeny, phyloref) + .includes(data["@id"]) + ) { + if (has(phyloref, "@id")) { + annotations.push( + "phyloref:actual=" + + convertToNexusAnnotationValue(phyloref["@id"]) + ); + } - if (has(this.phyloref, "label")) { - annotations.push( - "phyloref:actualLabel=" + - convertToNexusAnnotationValue(this.phyloref["label"]) - ); + if (has(phyloref, "label")) { + annotations.push( + "phyloref:actualLabel=" + + convertToNexusAnnotationValue(phyloref["label"]) + ); + } + + // We don't know what to call this phyloref, but nevertheless we label it minimally. + if (!has(phyloref, "@id") && !has(phyloref, "label")) + annotations.push("phyloref:actual="); } - // We don't know what to call this phyloref, but nevertheless we label it minimally. - if (!has(this.phyloref, "@id") && !has(this.phyloref, "label")) - annotations.push("phyloref:actual="); - } + if ( + this.selectedNodeLabel && + this.selectedNodeLabel.toLowerCase() === data.name.toLowerCase() + ) { + if (has(this.phyloref, "@id")) { + annotations.push( + "phyloref:expected=" + + convertToNexusAnnotationValue(phyloref["@id"]) + ); + } - if ( - this.selectedNodeLabel && - this.selectedNodeLabel.toLowerCase() === data.name.toLowerCase() - ) { - if (has(this.phyloref, "@id")) { - annotations.push( - "phyloref:expected=" + - convertToNexusAnnotationValue(this.phyloref["@id"]) - ); - } + if (has(phyloref, "label")) { + annotations.push( + "phyloref:expectedLabel=" + + convertToNexusAnnotationValue(phyloref["label"]) + ); + } - if (has(this.phyloref, "label")) { - annotations.push( - "phyloref:expectedLabel=" + - convertToNexusAnnotationValue(this.phyloref["label"]) - ); + // We don't know what to call this phyloref, but nevertheless we label it minimally. + if (!has(phyloref, "@id") && !has(phyloref, "label")) + annotations.push("phyloref:expected="); } - - // We don't know what to call this phyloref, but nevertheless we label it minimally. - if (!has(this.phyloref, "@id") && !has(this.phyloref, "label")) - annotations.push("phyloref:expected="); - } + }); console.log("Annotations: ", annotations); if (annotations.length === 0) return undefined; @@ -296,186 +292,188 @@ export default { const wrappedPhylogeny = new PhylogenyWrapper(this.phylogeny || {}); // Wrap the phyloref is there is one. - const wrappedPhyloref = new PhylorefWrapper(this.phyloref || {}); + this.phylorefs.forEach(phyloref => { + const wrappedPhyloref = new PhylorefWrapper(phyloref || {}); - // Make sure we don't already have an internal label node on this SVG node! - let textLabel = element.selectAll("text"); + // Make sure we don't already have an internal label node on this SVG node! + let textLabel = element.selectAll("text"); - if (has(data, "name") && data.name !== "" && data.children) { - // If the internal label has the same label as the currently - // selected phyloreference, add an 'id' so we can jump to it - // and a CSS class to render it differently from other labels. - if ( - // Display a label if: - // (1) No selectedNodeLabel was provided to us (i.e. display all node labels), or - // (2) We are currently rendering the selectedNodeLabel. - !this.selectedNodeLabel || - this.selectedNodeLabel.toLowerCase() === data.name.toLowerCase() - ) { - if (textLabel.empty()) textLabel = element.append("text"); - textLabel - .classed("internal-label", true) - .text(data.name) - .attr("dx", "0.3em") - .attr("dy", "0.35em"); - - // Is this the currently selected internal label? + if (has(data, "name") && data.name !== "" && data.children) { + // If the internal label has the same label as the currently + // selected phyloreference, add an 'id' so we can jump to it + // and a CSS class to render it differently from other labels. if ( - this.selectedNodeLabel && - this.selectedNodeLabel.toLowerCase() === data.name.toLowerCase() + // Display a label if: + // (1) No selectedNodeLabel was provided to us (i.e. display all node labels), or + // (2) We are currently rendering the selectedNodeLabel. + !this.selectedNodeLabel || + this.selectedNodeLabel.toLowerCase() === data.name.toLowerCase() ) { - textLabel.attr( - "id", - `current_expected_label_phylogeny_${this.phylogenyIndex}` - ); - textLabel.classed("selected-internal-label", true); - } - } else if (!textLabel.empty()) textLabel.remove(); - } - - // Clear any existing menu items. - node.menu_items = []; - - // Add a custom menu item to allow us to rename this node. - console.log("node", node); - addCustomMenu( - node, - (node) => "Rename this node", - () => { - const node = data; - const existingName = node.name || "(none)"; - const newName = window.prompt( - `Rename node named '${existingName}' to:` - ); - if (newName === null) { - // This means the user clicked "Cancel", so don't do anything. - } else if (!newName || newName === "undefined") { - // Apparently IE7 and IE8 will return the string 'undefined' if the user doesn't - // enter anything. - // - // Remove the current label. - node.name = ""; - } else { - // Set the new label. - node.name = newName; - } + if (textLabel.empty()) textLabel = element.append("text"); + textLabel + .classed("internal-label", true) + .text(data.name) + .attr("dx", "0.3em") + .attr("dy", "0.35em"); + + // Is this the currently selected internal label? + if ( + this.selectedNodeLabel && + this.selectedNodeLabel.toLowerCase() === data.name.toLowerCase() + ) { + textLabel.attr( + "id", + `current_expected_label_phylogeny_${this.phylogenyIndex}` + ); + textLabel.classed("selected-internal-label", true); + } + } else if (!textLabel.empty()) textLabel.remove(); + } - // Export the entire phylogeny as a Newick string, and store that - // in the phylogeny object. - const updatedNewickString = this.tree.getNewick(); - console.log("updatedNewickString", updatedNewickString); - this.$store.commit("setPhylogenyProps", { - phylogeny: this.phylogeny, - newick: updatedNewickString, - }); - }, - (node) => true // We can replace this with a condition that indicates whether this node should be displayed. - ); - - // If the internal label has the same IRI as the currently selected - // phyloreference's reasoned node, further mark or label it as the resolved node. - // - // Note that this node might NOT be labeled, in which case we need to - // label it now! - if ( - this.phyloref !== undefined && - has(data, "@id") && - this.$store.getters - .getResolvedNodesForPhylogeny(this.phylogeny, this.phyloref) - .includes(data["@id"]) - ) { - // We found another pinning node! - this.recurseNodes(data, (node) => - pinningNodeChildrenIRIs.add(node["@id"]) + // Clear any existing menu items. + node.menu_items = []; + + // Add a custom menu item to allow us to rename this node. + console.log("node", node); + addCustomMenu( + node, + (node) => "Rename this node", + () => { + const node = data; + const existingName = node.name || "(none)"; + const newName = window.prompt( + `Rename node named '${existingName}' to:` + ); + if (newName === null) { + // This means the user clicked "Cancel", so don't do anything. + } else if (!newName || newName === "undefined") { + // Apparently IE7 and IE8 will return the string 'undefined' if the user doesn't + // enter anything. + // + // Remove the current label. + node.name = ""; + } else { + // Set the new label. + node.name = newName; + } + + // Export the entire phylogeny as a Newick string, and store that + // in the phylogeny object. + const updatedNewickString = this.tree.getNewick(); + console.log("updatedNewickString", updatedNewickString); + this.$store.commit("setPhylogenyProps", { + phylogeny: this.phylogeny, + newick: updatedNewickString, + }); + }, + (node) => true // We can replace this with a condition that indicates whether this node should be displayed. ); - // Mark this node as the pinning node. - element.classed("pinning-node", true); + // If the internal label has the same IRI as the currently selected + // phyloreference's reasoned node, further mark or label it as the resolved node. + // + // Note that this node might NOT be labeled, in which case we need to + // label it now! + if ( + phyloref !== undefined && + has(data, "@id") && + this.$store.getters + .getResolvedNodesForPhylogeny(this.phylogeny, phyloref) + .includes(data["@id"]) + ) { + // We found another pinning node! + this.recurseNodes(data, (node) => + pinningNodeChildrenIRIs.add(node["@id"]) + ); - // Make the pinning node circle larger (twice its usual size of 3). - element.select("circle").attr("r", 6); + // Mark this node as the pinning node. + element.classed("pinning-node", true); - // Set its id to 'current_pinning_node_phylogeny{{phylogenyIndex}}' - element.attr( - "id", - `current_pinning_node_phylogeny_${this.phylogenyIndex}` - ); + // Make the pinning node circle larger (twice its usual size of 3). + element.select("circle").attr("r", 6); - // If we have phylorefNoFilter set, then - if (this.phylorefNoFilter) { - // Make sure we don't already have an internal label node on this SVG node! - let textLabel = element.selectAll("text"); - - if (textLabel.empty()) textLabel = element.append("text"); - console.log(`Found text label `, textLabel); - let textLabelText = textLabel.text; - if (!textLabelText) textLabelText = data.name; - else textLabelText = textLabelText + '_and_' + data.name; - textLabel - .classed("internal-label", true) - .text(textLabelText) - .attr("dx", "0.3em") - .attr("dy", "0.35em"); - } - } + // Set its id to 'current_pinning_node_phylogeny{{phylogenyIndex}}' + element.attr( + "id", + `current_pinning_node_phylogeny_${this.phylogenyIndex}` + ); - // Maybe this isn't a pinning node, but it is a child of a pinning node. - if (has(data, "@id") && pinningNodeChildrenIRIs.has(data["@id"])) { - // Apply a class. - // Note that this applies to the resolved-node too. - element.classed("descendant-of-pinning-node-node", true); - } + // If we have phylorefNoFilter set, then + if (this.phylorefNoFilter) { + // Make sure we don't already have an internal label node on this SVG node! + let textLabel = element.selectAll("text"); + + if (textLabel.empty()) textLabel = element.append("text"); + console.log(`Found text label `, textLabel); + let textLabelText = textLabel.text; + if (!textLabelText) textLabelText = data.name; + else textLabelText = textLabelText + '_and_' + data.name; + textLabel + .classed("internal-label", true) + .text(textLabelText) + .attr("dx", "0.3em") + .attr("dy", "0.35em"); + } + } - if (data.name !== undefined && data.children === undefined) { - // Labeled leaf node! Look for taxonomic units. - const tunits = wrappedPhylogeny.getTaxonomicUnitsForNodeLabel( - data.name - ); + // Maybe this isn't a pinning node, but it is a child of a pinning node. + if (has(data, "@id") && pinningNodeChildrenIRIs.has(data["@id"])) { + // Apply a class. + // Note that this applies to the resolved-node too. + element.classed("descendant-of-pinning-node-node", true); + } - if (tunits.length === 0) { - element.classed("terminal-node-without-tunits", true); - } else if (this.phyloref !== undefined) { - // If this is a terminal node, we should set its ID to - // `current_expected_label_phylogeny${phylogenyIndex}` if it is - // the currently expected node label. - if ( - has(this.phyloref, "label") && - this.selectedNodeLabel && - this.selectedNodeLabel.toLowerCase() === data.name.toLowerCase() - ) { - textLabel.attr( - "id", - `current_expected_label_phylogeny_${this.phylogenyIndex}` - ); - } + if (data.name !== undefined && data.children === undefined) { + // Labeled leaf node! Look for taxonomic units. + const tunits = wrappedPhylogeny.getTaxonomicUnitsForNodeLabel( + data.name + ); - // We should highlight internal specifiers. - if (has(this.phyloref, "internalSpecifiers")) { + if (tunits.length === 0) { + element.classed("terminal-node-without-tunits", true); + } else if (phyloref !== undefined) { + // If this is a terminal node, we should set its ID to + // `current_expected_label_phylogeny${phylogenyIndex}` if it is + // the currently expected node label. if ( - this.phyloref.internalSpecifiers.some((specifier) => - wrappedPhylogeny - .getNodeLabelsMatchedBySpecifier(specifier) - .includes(data.name) - ) + has(phyloref, "label") && + this.selectedNodeLabel && + this.selectedNodeLabel.toLowerCase() === data.name.toLowerCase() ) { - element.classed("internal-specifier-node", true); + textLabel.attr( + "id", + `current_expected_label_phylogeny_${this.phylogenyIndex}` + ); } - } - // We should highlight external specifiers. - if (has(this.phyloref, "externalSpecifiers")) { - if ( - this.phyloref.externalSpecifiers.some((specifier) => - wrappedPhylogeny - .getNodeLabelsMatchedBySpecifier(specifier) - .includes(data.name) - ) - ) { - element.classed("external-specifier-node", true); + // We should highlight internal specifiers. + if (has(phyloref, "internalSpecifiers")) { + if ( + phyloref.internalSpecifiers.some((specifier) => + wrappedPhylogeny + .getNodeLabelsMatchedBySpecifier(specifier) + .includes(data.name) + ) + ) { + element.classed("internal-specifier-node", true); + } + } + + // We should highlight external specifiers. + if (has(phyloref, "externalSpecifiers")) { + if ( + phyloref.externalSpecifiers.some((specifier) => + wrappedPhylogeny + .getNodeLabelsMatchedBySpecifier(specifier) + .includes(data.name) + ) + ) { + element.classed("external-specifier-node", true); + } } } } - } + }); }, "edge-styler": (element, data) => { // const data = node.data; @@ -493,10 +491,11 @@ export default { // Is the source ID part of this phylogeny? If so, we want to highlight it! if ( - this.phyloref !== undefined && - this.$store.getters - .getResolvedNodesForPhylogeny(this.phylogeny, this.phyloref) + this.phylorefs.length > 0 && + this.phylorefs.some(phyloref => this.$store.getters + .getResolvedNodesForPhylogeny(this.phylogeny, phyloref) .includes(source_id) + ) ) { pinningNodeChildrenIRIs.add(source_id); this.recurseNodes(source, (node) => { diff --git a/src/components/phyloref/PhylorefView.vue b/src/components/phyloref/PhylorefView.vue index 68efd466..c8fd6790 100644 --- a/src/components/phyloref/PhylorefView.vue +++ b/src/components/phyloref/PhylorefView.vue @@ -508,7 +508,7 @@ diff --git a/src/components/phyx/PhyxView.vue b/src/components/phyx/PhyxView.vue index 73c90b6c..28e1f3dd 100644 --- a/src/components/phyx/PhyxView.vue +++ b/src/components/phyx/PhyxView.vue @@ -284,6 +284,7 @@ From 7936cf9d27eae524ce47f738038c42f18b7625c5 Mon Sep 17 00:00:00 2001 From: Gaurav Vaidya Date: Wed, 8 May 2024 02:25:46 -0400 Subject: [PATCH 04/23] Incorporated phyloref display improvements from PR #322. Also reformatted this file. --- src/components/phylogeny/Phylotree.vue | 194 +++++++++++++------------ 1 file changed, 100 insertions(+), 94 deletions(-) diff --git a/src/components/phylogeny/Phylotree.vue b/src/components/phylogeny/Phylotree.vue index ab939e44..b0b328fe 100644 --- a/src/components/phylogeny/Phylotree.vue +++ b/src/components/phylogeny/Phylotree.vue @@ -42,7 +42,7 @@ import jQuery from "jquery"; import { PhylogenyWrapper, PhylorefWrapper } from "@phyloref/phyx"; import { addCustomMenu } from "phylotree/src/render/menus"; import { saveAs } from "filesaver.js-npm"; -import {text} from "@fortawesome/fontawesome-svg-core"; +import { text } from "@fortawesome/fontawesome-svg-core"; /* * Note that this requires the Phylotree Javascript to be loaded in the HTML @@ -68,7 +68,7 @@ export default { // The selected node label. If not set, we display all node labels. type: String, required: false, - } + }, }, data() { return { @@ -146,10 +146,7 @@ export default { // Export this phylogeny as a Nexus string in a .nex file for download. const newickStr = this.tree.getNewick((node) => { // Is the resolved node for this phyloref? If so, let's make an annotation. - if ( - has(node, "data") && - has(node.data, "@id") - ) { + if (has(node, "data") && has(node.data, "@id")) { const annotations = []; const data = node.data; @@ -158,22 +155,22 @@ export default { return '"' + str.replaceAll('"', "''") + '"'; }; - this.phylorefs.forEach(phyloref => { + this.phylorefs.forEach((phyloref) => { if ( - this.$store.getters - .getResolvedNodesForPhylogeny(this.phylogeny, phyloref) - .includes(data["@id"]) + this.$store.getters + .getResolvedNodesForPhylogeny(this.phylogeny, phyloref) + .includes(data["@id"]) ) { if (has(phyloref, "@id")) { annotations.push( - "phyloref:actual=" + + "phyloref:actual=" + convertToNexusAnnotationValue(phyloref["@id"]) ); } if (has(phyloref, "label")) { annotations.push( - "phyloref:actualLabel=" + + "phyloref:actualLabel=" + convertToNexusAnnotationValue(phyloref["label"]) ); } @@ -184,19 +181,19 @@ export default { } if ( - this.selectedNodeLabel && - this.selectedNodeLabel.toLowerCase() === data.name.toLowerCase() + this.selectedNodeLabel && + this.selectedNodeLabel.toLowerCase() === data.name.toLowerCase() ) { if (has(this.phyloref, "@id")) { annotations.push( - "phyloref:expected=" + + "phyloref:expected=" + convertToNexusAnnotationValue(phyloref["@id"]) ); } if (has(phyloref, "label")) { annotations.push( - "phyloref:expectedLabel=" + + "phyloref:expectedLabel=" + convertToNexusAnnotationValue(phyloref["label"]) ); } @@ -292,7 +289,7 @@ export default { const wrappedPhylogeny = new PhylogenyWrapper(this.phylogeny || {}); // Wrap the phyloref is there is one. - this.phylorefs.forEach(phyloref => { + this.phylorefs.forEach((phyloref) => { const wrappedPhyloref = new PhylorefWrapper(phyloref || {}); // Make sure we don't already have an internal label node on this SVG node! @@ -303,27 +300,28 @@ export default { // selected phyloreference, add an 'id' so we can jump to it // and a CSS class to render it differently from other labels. if ( - // Display a label if: - // (1) No selectedNodeLabel was provided to us (i.e. display all node labels), or - // (2) We are currently rendering the selectedNodeLabel. - !this.selectedNodeLabel || - this.selectedNodeLabel.toLowerCase() === data.name.toLowerCase() + // Display a label if: + // (1) No selectedNodeLabel was provided to us (i.e. display all node labels), or + // (2) We are currently rendering the selectedNodeLabel. + !this.selectedNodeLabel || + this.selectedNodeLabel.toLowerCase() === data.name.toLowerCase() ) { if (textLabel.empty()) textLabel = element.append("text"); textLabel - .classed("internal-label", true) - .text(data.name) - .attr("dx", "0.3em") - .attr("dy", "0.35em"); + .classed("internal-label", true) + .text(data.name) + .attr("dx", "0.3em") + .attr("dy", "0.35em"); // Is this the currently selected internal label? if ( - this.selectedNodeLabel && - this.selectedNodeLabel.toLowerCase() === data.name.toLowerCase() + this.selectedNodeLabel && + this.selectedNodeLabel.toLowerCase() === + data.name.toLowerCase() ) { textLabel.attr( - "id", - `current_expected_label_phylogeny_${this.phylogenyIndex}` + "id", + `current_expected_label_phylogeny_${this.phylogenyIndex}` ); textLabel.classed("selected-internal-label", true); } @@ -336,37 +334,37 @@ export default { // Add a custom menu item to allow us to rename this node. console.log("node", node); addCustomMenu( - node, - (node) => "Rename this node", - () => { - const node = data; - const existingName = node.name || "(none)"; - const newName = window.prompt( - `Rename node named '${existingName}' to:` - ); - if (newName === null) { - // This means the user clicked "Cancel", so don't do anything. - } else if (!newName || newName === "undefined") { - // Apparently IE7 and IE8 will return the string 'undefined' if the user doesn't - // enter anything. - // - // Remove the current label. - node.name = ""; - } else { - // Set the new label. - node.name = newName; - } + node, + (node) => "Rename this node", + () => { + const node = data; + const existingName = node.name || "(none)"; + const newName = window.prompt( + `Rename node named '${existingName}' to:` + ); + if (newName === null) { + // This means the user clicked "Cancel", so don't do anything. + } else if (!newName || newName === "undefined") { + // Apparently IE7 and IE8 will return the string 'undefined' if the user doesn't + // enter anything. + // + // Remove the current label. + node.name = ""; + } else { + // Set the new label. + node.name = newName; + } - // Export the entire phylogeny as a Newick string, and store that - // in the phylogeny object. - const updatedNewickString = this.tree.getNewick(); - console.log("updatedNewickString", updatedNewickString); - this.$store.commit("setPhylogenyProps", { - phylogeny: this.phylogeny, - newick: updatedNewickString, - }); - }, - (node) => true // We can replace this with a condition that indicates whether this node should be displayed. + // Export the entire phylogeny as a Newick string, and store that + // in the phylogeny object. + const updatedNewickString = this.tree.getNewick(); + console.log("updatedNewickString", updatedNewickString); + this.$store.commit("setPhylogenyProps", { + phylogeny: this.phylogeny, + newick: updatedNewickString, + }); + }, + (node) => true // We can replace this with a condition that indicates whether this node should be displayed. ); // If the internal label has the same IRI as the currently selected @@ -375,27 +373,32 @@ export default { // Note that this node might NOT be labeled, in which case we need to // label it now! if ( - phyloref !== undefined && - has(data, "@id") && - this.$store.getters - .getResolvedNodesForPhylogeny(this.phylogeny, phyloref) - .includes(data["@id"]) + phyloref !== undefined && + has(data, "@id") && + this.$store.getters + .getResolvedNodesForPhylogeny(this.phylogeny, phyloref) + .includes(data["@id"]) ) { // We found another pinning node! this.recurseNodes(data, (node) => - pinningNodeChildrenIRIs.add(node["@id"]) + pinningNodeChildrenIRIs.add(node["@id"]) ); // Mark this node as the pinning node. element.classed("pinning-node", true); - // Make the pinning node circle larger (twice its usual size of 3). - element.select("circle").attr("r", 6); + // If there is no circle, add one. + if (element.select("circle").empty()) { + element.append("circle").attr("cx", -3).attr("r", 6); + } else { + // Make the pinning node circle larger (twice its usual size of 3). + element.select("circle").attr("r", 6); + } // Set its id to 'current_pinning_node_phylogeny{{phylogenyIndex}}' element.attr( - "id", - `current_pinning_node_phylogeny_${this.phylogenyIndex}` + "id", + `current_pinning_node_phylogeny_${this.phylogenyIndex}` ); // If we have phylorefNoFilter set, then @@ -407,12 +410,12 @@ export default { console.log(`Found text label `, textLabel); let textLabelText = textLabel.text; if (!textLabelText) textLabelText = data.name; - else textLabelText = textLabelText + '_and_' + data.name; + else textLabelText = textLabelText + "_and_" + data.name; textLabel - .classed("internal-label", true) - .text(textLabelText) - .attr("dx", "0.3em") - .attr("dy", "0.35em"); + .classed("internal-label", true) + .text(textLabelText) + .attr("dx", "0.3em") + .attr("dy", "0.35em"); } } @@ -426,7 +429,7 @@ export default { if (data.name !== undefined && data.children === undefined) { // Labeled leaf node! Look for taxonomic units. const tunits = wrappedPhylogeny.getTaxonomicUnitsForNodeLabel( - data.name + data.name ); if (tunits.length === 0) { @@ -436,24 +439,25 @@ export default { // `current_expected_label_phylogeny${phylogenyIndex}` if it is // the currently expected node label. if ( - has(phyloref, "label") && - this.selectedNodeLabel && - this.selectedNodeLabel.toLowerCase() === data.name.toLowerCase() + has(phyloref, "label") && + this.selectedNodeLabel && + this.selectedNodeLabel.toLowerCase() === + data.name.toLowerCase() ) { textLabel.attr( - "id", - `current_expected_label_phylogeny_${this.phylogenyIndex}` + "id", + `current_expected_label_phylogeny_${this.phylogenyIndex}` ); } // We should highlight internal specifiers. if (has(phyloref, "internalSpecifiers")) { if ( - phyloref.internalSpecifiers.some((specifier) => - wrappedPhylogeny - .getNodeLabelsMatchedBySpecifier(specifier) - .includes(data.name) - ) + phyloref.internalSpecifiers.some((specifier) => + wrappedPhylogeny + .getNodeLabelsMatchedBySpecifier(specifier) + .includes(data.name) + ) ) { element.classed("internal-specifier-node", true); } @@ -462,11 +466,11 @@ export default { // We should highlight external specifiers. if (has(phyloref, "externalSpecifiers")) { if ( - phyloref.externalSpecifiers.some((specifier) => - wrappedPhylogeny - .getNodeLabelsMatchedBySpecifier(specifier) - .includes(data.name) - ) + phyloref.externalSpecifiers.some((specifier) => + wrappedPhylogeny + .getNodeLabelsMatchedBySpecifier(specifier) + .includes(data.name) + ) ) { element.classed("external-specifier-node", true); } @@ -492,9 +496,10 @@ export default { // Is the source ID part of this phylogeny? If so, we want to highlight it! if ( this.phylorefs.length > 0 && - this.phylorefs.some(phyloref => this.$store.getters - .getResolvedNodesForPhylogeny(this.phylogeny, phyloref) - .includes(source_id) + this.phylorefs.some((phyloref) => + this.$store.getters + .getResolvedNodesForPhylogeny(this.phylogeny, phyloref) + .includes(source_id) ) ) { pinningNodeChildrenIRIs.add(source_id); @@ -593,6 +598,7 @@ export default { .pinning-node text { fill: black !important; font-weight: bolder; + font-size: 16pt !important; } /* From fa6f7fe65d58dd29c4eb53331e45997a5da169a0 Mon Sep 17 00:00:00 2001 From: Gaurav Vaidya Date: Wed, 22 May 2024 03:01:46 -0400 Subject: [PATCH 05/23] Adjusted fonts to make internal and terminal labels more similar. --- src/components/phylogeny/Phylotree.vue | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/components/phylogeny/Phylotree.vue b/src/components/phylogeny/Phylotree.vue index b0b328fe..d6d0d203 100644 --- a/src/components/phylogeny/Phylotree.vue +++ b/src/components/phylogeny/Phylotree.vue @@ -561,8 +561,7 @@ export default { /* Labels for internal nodes, whether phylorefs or not */ .internal-label { font-family: serif; - font-size: 16pt; - font-style: italic; + font-size: 14pt; text-anchor: start; /* Align text so it starts at the coordinates provided */ alignment-baseline: middle; @@ -586,7 +585,7 @@ export default { /* The selected internal label on a phylogeny, whether determined to be the pinning node or not. */ .selected-internal-label { - font-size: 16pt; + font-size: 12pt !important; fill: rgb(0, 24, 168); } @@ -598,7 +597,7 @@ export default { .pinning-node text { fill: black !important; font-weight: bolder; - font-size: 16pt !important; + font-size: 14pt; } /* From ab1b42740fab55197008c0230d92535a1a6bd556 Mon Sep 17 00:00:00 2001 From: Gaurav Vaidya Date: Wed, 22 May 2024 03:12:03 -0400 Subject: [PATCH 06/23] Made the selected circles smaller. --- src/components/phylogeny/Phylotree.vue | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/components/phylogeny/Phylotree.vue b/src/components/phylogeny/Phylotree.vue index d6d0d203..bf940827 100644 --- a/src/components/phylogeny/Phylotree.vue +++ b/src/components/phylogeny/Phylotree.vue @@ -389,10 +389,10 @@ export default { // If there is no circle, add one. if (element.select("circle").empty()) { - element.append("circle").attr("cx", -3).attr("r", 6); + element.append("circle").attr("cx", -3).attr("r", 4); } else { - // Make the pinning node circle larger (twice its usual size of 3). - element.select("circle").attr("r", 6); + // Make the pinning node circle larger (slightly larger than its usual size of 3). + element.select("circle").attr("r", 4); } // Set its id to 'current_pinning_node_phylogeny{{phylogenyIndex}}' From f2c4ba640f397f7cfa380666eb664b0eec65ae68 Mon Sep 17 00:00:00 2001 From: Gaurav Vaidya Date: Sun, 12 Jan 2025 14:42:48 -0500 Subject: [PATCH 07/23] Fixed case where we don't pass phylorefs into a Phylotree. --- src/components/phylogeny/Phylotree.vue | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/components/phylogeny/Phylotree.vue b/src/components/phylogeny/Phylotree.vue index bf940827..54001e9f 100644 --- a/src/components/phylogeny/Phylotree.vue +++ b/src/components/phylogeny/Phylotree.vue @@ -53,7 +53,11 @@ export default { name: "Phylotree", props: { phylogeny: Object, // The phylogeny to render. - phylorefs: Array, // The phyloreferences to highlight. + phylorefs: { + // The phyloreferences to highlight. + type: Array, + default: [], + }, spacingX: { // Spacing in the X axis in pixels. type: Number, From ba9f3c1c0c1ca14f8da386caf361df9a2a948222 Mon Sep 17 00:00:00 2001 From: Gaurav Vaidya Date: Sun, 12 Jan 2025 15:00:09 -0500 Subject: [PATCH 08/23] Updated Nexus export so it works in TreeViewer. --- src/components/phylogeny/Phylotree.vue | 26 +++++++++++++++++++------- 1 file changed, 19 insertions(+), 7 deletions(-) diff --git a/src/components/phylogeny/Phylotree.vue b/src/components/phylogeny/Phylotree.vue index 54001e9f..3c55735b 100644 --- a/src/components/phylogeny/Phylotree.vue +++ b/src/components/phylogeny/Phylotree.vue @@ -73,6 +73,12 @@ export default { type: String, required: false, }, + supportTreeViewer: { + // TreeViewer (https://treeviewer.org/) is a fairly recent phylogenetic tree viewing software that + // has a slightly different idea about how annotations should be formatted in NEXUS files. + type: Boolean, + default: true, + } }, data() { return { @@ -167,21 +173,21 @@ export default { ) { if (has(phyloref, "@id")) { annotations.push( - "phyloref:actual=" + + "\"phyloref:actual\"=" + convertToNexusAnnotationValue(phyloref["@id"]) ); } if (has(phyloref, "label")) { annotations.push( - "phyloref:actualLabel=" + + "\"phyloref:actualLabel\"=" + convertToNexusAnnotationValue(phyloref["label"]) ); } // We don't know what to call this phyloref, but nevertheless we label it minimally. if (!has(phyloref, "@id") && !has(phyloref, "label")) - annotations.push("phyloref:actual="); + annotations.push("\"phyloref:actual\"="); } if ( @@ -190,27 +196,33 @@ export default { ) { if (has(this.phyloref, "@id")) { annotations.push( - "phyloref:expected=" + + "\"phyloref:expected\"=" + convertToNexusAnnotationValue(phyloref["@id"]) ); } if (has(phyloref, "label")) { annotations.push( - "phyloref:expectedLabel=" + + "\"phyloref:expectedLabel\"=" + convertToNexusAnnotationValue(phyloref["label"]) ); } // We don't know what to call this phyloref, but nevertheless we label it minimally. if (!has(phyloref, "@id") && !has(phyloref, "label")) - annotations.push("phyloref:expected="); + annotations.push("\"phyloref:expected\"="); } }); console.log("Annotations: ", annotations); if (annotations.length === 0) return undefined; - else return `[&${annotations.join(",")}]`; + else { + if (this.supportTreeViewer) { + return `[${annotations.join(",")}]`; + } else { + return `[&${annotations.join(",")}]`; + } + } } return undefined; From f97e4a5e05a69da9af9e940e898a4b413bff4f7f Mon Sep 17 00:00:00 2001 From: Gaurav Vaidya Date: Sun, 12 Jan 2025 15:37:29 -0500 Subject: [PATCH 09/23] Improved TreeViewer support. --- src/components/phylogeny/Phylotree.vue | 74 +++++++++++++++----------- 1 file changed, 43 insertions(+), 31 deletions(-) diff --git a/src/components/phylogeny/Phylotree.vue b/src/components/phylogeny/Phylotree.vue index 3c55735b..32385993 100644 --- a/src/components/phylogeny/Phylotree.vue +++ b/src/components/phylogeny/Phylotree.vue @@ -43,6 +43,7 @@ import { PhylogenyWrapper, PhylorefWrapper } from "@phyloref/phyx"; import { addCustomMenu } from "phylotree/src/render/menus"; import { saveAs } from "filesaver.js-npm"; import { text } from "@fortawesome/fontawesome-svg-core"; +import convert from "lodash/fp/convert"; /* * Note that this requires the Phylotree Javascript to be loaded in the HTML @@ -157,13 +158,13 @@ export default { const newickStr = this.tree.getNewick((node) => { // Is the resolved node for this phyloref? If so, let's make an annotation. if (has(node, "data") && has(node.data, "@id")) { - const annotations = []; - const data = node.data; - - const convertToNexusAnnotationValue = (str) => { - // We really just need to wrap this in double-quotes, which means we need to filter out existing double quotes. - return '"' + str.replaceAll('"', "''") + '"'; + const annotations = { + "phyloref:actual": [], + "phyloref:actualLabel": [], + "phyloref:expected": [], + "phyloref:expectedLabel": [], }; + const data = node.data; this.phylorefs.forEach((phyloref) => { if ( @@ -172,22 +173,16 @@ export default { .includes(data["@id"]) ) { if (has(phyloref, "@id")) { - annotations.push( - "\"phyloref:actual\"=" + - convertToNexusAnnotationValue(phyloref["@id"]) - ); + annotations["phyloref:actual"].push(phyloref["@id"]); } if (has(phyloref, "label")) { - annotations.push( - "\"phyloref:actualLabel\"=" + - convertToNexusAnnotationValue(phyloref["label"]) - ); + annotations["phyloref:actualLabel"].push(phyloref["label"]); } // We don't know what to call this phyloref, but nevertheless we label it minimally. if (!has(phyloref, "@id") && !has(phyloref, "label")) - annotations.push("\"phyloref:actual\"="); + annotations["phyloref:actual"].push(""); } if ( @@ -195,37 +190,54 @@ export default { this.selectedNodeLabel.toLowerCase() === data.name.toLowerCase() ) { if (has(this.phyloref, "@id")) { - annotations.push( - "\"phyloref:expected\"=" + - convertToNexusAnnotationValue(phyloref["@id"]) - ); + annotations["phyloref:expected"].push(phyloref["@id"]); } if (has(phyloref, "label")) { - annotations.push( - "\"phyloref:expectedLabel\"=" + - convertToNexusAnnotationValue(phyloref["label"]) - ); + annotations["phyloref:expectedLabel"].push(phyloref["label"]); } // We don't know what to call this phyloref, but nevertheless we label it minimally. if (!has(phyloref, "@id") && !has(phyloref, "label")) - annotations.push("\"phyloref:expected\"="); + annotations["phyloref:expected"].push(""); } }); console.log("Annotations: ", annotations); - if (annotations.length === 0) return undefined; - else { - if (this.supportTreeViewer) { - return `[${annotations.join(",")}]`; + const convertToNexusAnnotationValue = (str) => { + // We really just need to wrap this in double-quotes, which means we need to filter out existing double quotes. + return '"' + str.replaceAll('"', "''") + '"'; + }; + + // I think Nexus allows us to have multiple annotations with the same label, but TreeViewer doesn't. + // Also Nexus requires that the annotations start with '&', while TreeViewer doesn't. So we generate + // the annotation strings separately depending on whether we want to support TreeViewer or not. + if (this.supportTreeViewer) { + const annotationList = []; + + for (const [key, value] of Object.entries(annotations)) { + if (value.length > 0) { + annotationList.push(`${key.replaceAll(':', '.')}=${convertToNexusAnnotationValue(value.join("||"))}`); + } + } + + if (annotationList.length > 0) { + return `[${annotationList.join(",")}]`; } else { - return `[&${annotations.join(",")}]`; + return undefined; + } + } else { + const annotationList = annotations.entries().flatMap(entry => { + return entry[1].map(value => `"{'${entry[0]}"=${convertToNexusAnnotationValue(value)}`) + }); + + if (annotationList.length > 0) { + return `[&${annotationList.join(",")}]`; + } else { + return undefined; } } } - - return undefined; }); // Create a Nexus file to store this `klados_tree` in. From 0b1027fbe84a02b68c96d185b5d911dc5d017b60 Mon Sep 17 00:00:00 2001 From: Gaurav Vaidya Date: Sun, 12 Jan 2025 18:22:20 -0500 Subject: [PATCH 10/23] Improved comments. --- src/components/phylogeny/Phylotree.vue | 20 +++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/src/components/phylogeny/Phylotree.vue b/src/components/phylogeny/Phylotree.vue index 32385993..d910069a 100644 --- a/src/components/phylogeny/Phylotree.vue +++ b/src/components/phylogeny/Phylotree.vue @@ -158,6 +158,7 @@ export default { const newickStr = this.tree.getNewick((node) => { // Is the resolved node for this phyloref? If so, let's make an annotation. if (has(node, "data") && has(node.data, "@id")) { + // We collect annotations by type -- note that we can have multiple phylorefs on each node. const annotations = { "phyloref:actual": [], "phyloref:actualLabel": [], @@ -167,6 +168,7 @@ export default { const data = node.data; this.phylorefs.forEach((phyloref) => { + // Is this node one of the resolved nodes for this phyloreference? if ( this.$store.getters .getResolvedNodesForPhylogeny(this.phylogeny, phyloref) @@ -180,11 +182,12 @@ export default { annotations["phyloref:actualLabel"].push(phyloref["label"]); } - // We don't know what to call this phyloref, but nevertheless we label it minimally. + // We don't know what to call this phyloref, but nevertheless we tag it so we know there's _something_ here. if (!has(phyloref, "@id") && !has(phyloref, "label")) annotations["phyloref:actual"].push(""); } + // Is this node one of the expected nodes for this phyloreference? if ( this.selectedNodeLabel && this.selectedNodeLabel.toLowerCase() === data.name.toLowerCase() @@ -197,21 +200,24 @@ export default { annotations["phyloref:expectedLabel"].push(phyloref["label"]); } - // We don't know what to call this phyloref, but nevertheless we label it minimally. + // We don't know what to call this phyloref, but nevertheless we tag it so we know there's _something_ here. if (!has(phyloref, "@id") && !has(phyloref, "label")) annotations["phyloref:expected"].push(""); } }); - console.log("Annotations: ", annotations); + // console.log("Annotations: ", annotations); + // Helper method to wrap annotation values. const convertToNexusAnnotationValue = (str) => { // We really just need to wrap this in double-quotes, which means we need to filter out existing double quotes. return '"' + str.replaceAll('"', "''") + '"'; }; - // I think Nexus allows us to have multiple annotations with the same label, but TreeViewer doesn't. - // Also Nexus requires that the annotations start with '&', while TreeViewer doesn't. So we generate - // the annotation strings separately depending on whether we want to support TreeViewer or not. + // There are three differences between TreeViewer and other Nexus tools: + // - Nexus wants annotation comments to start with '&', but that confuses TreeViewer. + // - Nexus allows us to have multiple annotations with the same label, but TreeViewer doesn't, so we + // combine annotations for TreeViewer (separated by '||'). + // - Nexus allows ':' in the annotation names, while TreeViewer doesn't, so we change them into '.'s. if (this.supportTreeViewer) { const annotationList = []; @@ -228,7 +234,7 @@ export default { } } else { const annotationList = annotations.entries().flatMap(entry => { - return entry[1].map(value => `"{'${entry[0]}"=${convertToNexusAnnotationValue(value)}`) + return entry[1].map(value => `"${entry[0]}"=${convertToNexusAnnotationValue(value)}`) }); if (annotationList.length > 0) { From 16a3a6ca124b06927bbface9fc3b812a529f2583 Mon Sep 17 00:00:00 2001 From: Gaurav Vaidya Date: Sun, 12 Jan 2025 18:58:38 -0500 Subject: [PATCH 11/23] Improved internal node label drawing. --- src/components/phylogeny/Phylotree.vue | 37 ++++++++++++-------------- 1 file changed, 17 insertions(+), 20 deletions(-) diff --git a/src/components/phylogeny/Phylotree.vue b/src/components/phylogeny/Phylotree.vue index d910069a..fe3b4440 100644 --- a/src/components/phylogeny/Phylotree.vue +++ b/src/components/phylogeny/Phylotree.vue @@ -435,22 +435,19 @@ export default { `current_pinning_node_phylogeny_${this.phylogenyIndex}` ); - // If we have phylorefNoFilter set, then - if (this.phylorefNoFilter) { - // Make sure we don't already have an internal label node on this SVG node! - let textLabel = element.selectAll("text"); - - if (textLabel.empty()) textLabel = element.append("text"); - console.log(`Found text label `, textLabel); - let textLabelText = textLabel.text; - if (!textLabelText) textLabelText = data.name; - else textLabelText = textLabelText + "_and_" + data.name; - textLabel - .classed("internal-label", true) - .text(textLabelText) - .attr("dx", "0.3em") - .attr("dy", "0.35em"); - } + // Make sure we don't already have an internal label node on this SVG node! + let textLabel = element.selectAll("text"); + if (textLabel.empty()) textLabel = element.append("text"); + + const textLabels = (textLabel.textContent || "").split(", "); + textLabels.push(data.name); + const textLabelText = textLabels.map(label => label.trim()).filter(label => label !== '').sort().join(", "); + + textLabel + .classed("internal-label", true) + .text(textLabelText) + .attr("dx", "0.3em") + .attr("dy", "0.35em"); } // Maybe this isn't a pinning node, but it is a child of a pinning node. @@ -595,7 +592,7 @@ export default { /* Labels for internal nodes, whether phylorefs or not */ .internal-label { font-family: serif; - font-size: 14pt; + font-size: 12pt; text-anchor: start; /* Align text so it starts at the coordinates provided */ alignment-baseline: middle; @@ -629,16 +626,16 @@ export default { * than as an .internal-specifier-node. */ .pinning-node text { - fill: black !important; + fill: black; font-weight: bolder; - font-size: 14pt; + font-size: 14pt !important; } /* * Increase the font size to make the node text more readable. */ .phylotree-node-text { - font-size: 12pt !important; + font-size: 12pt; } /* From 2ebd367546dfe0317fecbf5d481322142b33288a Mon Sep 17 00:00:00 2001 From: Gaurav Vaidya Date: Sun, 12 Jan 2025 23:37:19 -0500 Subject: [PATCH 12/23] Attempt to fix multiple phylogeny labeling issue. --- src/components/phylogeny/Phylotree.vue | 31 ++++++++++++++++++++++---- 1 file changed, 27 insertions(+), 4 deletions(-) diff --git a/src/components/phylogeny/Phylotree.vue b/src/components/phylogeny/Phylotree.vue index fe3b4440..a757adab 100644 --- a/src/components/phylogeny/Phylotree.vue +++ b/src/components/phylogeny/Phylotree.vue @@ -319,6 +319,9 @@ export default { // - data: The data associated with the node being styled const data = node.data; + // Store the phylorefs that resolves to each node ID. + const phylorefsByNodeId = {}; + // Wrap the phylogeny so we can call methods on it. const wrappedPhylogeny = new PhylogenyWrapper(this.phylogeny || {}); @@ -366,7 +369,7 @@ export default { node.menu_items = []; // Add a custom menu item to allow us to rename this node. - console.log("node", node); + // console.log("node", node); addCustomMenu( node, (node) => "Rename this node", @@ -414,6 +417,8 @@ export default { .includes(data["@id"]) ) { // We found another pinning node! + if(!(data["@id"] in phylorefsByNodeId)) phylorefsByNodeId[data["@id"]] = new Set(); + phylorefsByNodeId[data["@id"]].add(phyloref); this.recurseNodes(data, (node) => pinningNodeChildrenIRIs.add(node["@id"]) ); @@ -437,11 +442,29 @@ export default { // Make sure we don't already have an internal label node on this SVG node! let textLabel = element.selectAll("text"); - if (textLabel.empty()) textLabel = element.append("text"); + // console.log(`Looking for textLabel for phyloref ${wrappedPhyloref.label} on node ${data['@id']}: `, textLabel); + // if (textLabel.empty()) element.append("text"); + if (!textLabel.empty()) textLabel.remove(); + textLabel = element.append("text"); - const textLabels = (textLabel.textContent || "").split(", "); + const textLabels = []; textLabels.push(data.name); - const textLabelText = textLabels.map(label => label.trim()).filter(label => label !== '').sort().join(", "); + + + + // TODO: we should get all the alternate labels for this node. + console.log("tunits = ", wrappedPhylogeny.getTaxonomicUnitsForNodeLabel(data.name)); + + // Add all the phyloref labels for this node. + const phylorefLabels = [...phylorefsByNodeId[data["@id"]]].map(phyloref => new PhylorefWrapper(phyloref).label).sort(); + textLabels.push(...phylorefLabels); + console.log(`Found phylorefs for node ${data['@id']}: `, textLabels); + const textLabelText = [...new Set(textLabels + .filter(label => label && label !== '') // Filter out undefined or blank label. + .map(label => label.trim()))] // Trim all labels + .join("||"); // Join them with '||'s so we can re-separate them if needed. + + console.log(`Found phyloref ${wrappedPhyloref.label}, assigned label '${textLabelText}' to ${data['@id']} from textLabels: `, textLabels); textLabel .classed("internal-label", true) From f2a8a9c83ac6c2d785812bb6f3c3d045c1d53150 Mon Sep 17 00:00:00 2001 From: Gaurav Vaidya Date: Wed, 15 Jan 2025 13:10:01 -0500 Subject: [PATCH 13/23] Change default to FigTree Nexus output. --- src/components/phylogeny/Phylotree.vue | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/phylogeny/Phylotree.vue b/src/components/phylogeny/Phylotree.vue index a757adab..6093ca47 100644 --- a/src/components/phylogeny/Phylotree.vue +++ b/src/components/phylogeny/Phylotree.vue @@ -78,7 +78,7 @@ export default { // TreeViewer (https://treeviewer.org/) is a fairly recent phylogenetic tree viewing software that // has a slightly different idea about how annotations should be formatted in NEXUS files. type: Boolean, - default: true, + default: false, } }, data() { @@ -233,7 +233,7 @@ export default { return undefined; } } else { - const annotationList = annotations.entries().flatMap(entry => { + const annotationList = Object.entries(annotations).flatMap(entry => { return entry[1].map(value => `"${entry[0]}"=${convertToNexusAnnotationValue(value)}`) }); From a15dbcb19393b164314d374742bc10eb353e630d Mon Sep 17 00:00:00 2001 From: Gaurav Vaidya Date: Sun, 26 Jan 2025 16:33:30 -0500 Subject: [PATCH 14/23] Added a comment explaining supportTreeViewer. --- src/components/phylogeny/Phylotree.vue | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/components/phylogeny/Phylotree.vue b/src/components/phylogeny/Phylotree.vue index 98666a2b..fd89bd74 100644 --- a/src/components/phylogeny/Phylotree.vue +++ b/src/components/phylogeny/Phylotree.vue @@ -77,6 +77,8 @@ export default { supportTreeViewer: { // TreeViewer (https://treeviewer.org/) is a fairly recent phylogenetic tree viewing software that // has a slightly different idea about how annotations should be formatted in NEXUS files. + // Given its newness, we're not going to support it at present, but at some point we might want to + // support it -- in which case this boolean flag will allow you to turn on and off TreeViewer support. type: Boolean, default: false, } From fc7ac437ff712a13ec92a3aae8be708cde81bc42 Mon Sep 17 00:00:00 2001 From: Gaurav Vaidya Date: Sun, 26 Jan 2025 16:33:48 -0500 Subject: [PATCH 15/23] Some cleanup. --- src/components/phylogeny/Phylotree.vue | 13 ------------- 1 file changed, 13 deletions(-) diff --git a/src/components/phylogeny/Phylotree.vue b/src/components/phylogeny/Phylotree.vue index fd89bd74..dfad7fa3 100644 --- a/src/components/phylogeny/Phylotree.vue +++ b/src/components/phylogeny/Phylotree.vue @@ -42,8 +42,6 @@ import jQuery from "jquery"; import { PhylogenyWrapper, PhylorefWrapper } from "@phyloref/phyx"; import { addCustomMenu } from "phylotree/src/render/menus"; import { saveAs } from "filesaver.js-npm"; -import { text } from "@fortawesome/fontawesome-svg-core"; -import convert from "lodash/fp/convert"; /* * Note that this requires the Phylotree Javascript to be loaded in the HTML @@ -121,15 +119,6 @@ export default { tree() { // Set up Phylotree. return new phylotree(this.parsedNewick.json); - - /* - , { - 'internal-names': false, - transitions: false, - 'left-right-spacing': 'fit-to-size', - 'top-bottom-spacing': 'fixed-step', - } - */ }, }, watch: { @@ -486,8 +475,6 @@ export default { const textLabels = []; textLabels.push(data.name); - - // TODO: we should get all the alternate labels for this node. console.log("tunits = ", wrappedPhylogeny.getTaxonomicUnitsForNodeLabel(data.name)); From 41094dea22e5881abf866f706886224987aa7f59 Mon Sep 17 00:00:00 2001 From: Gaurav Vaidya Date: Sun, 26 Jan 2025 16:37:49 -0500 Subject: [PATCH 16/23] Nexus annotations are now combined by key. --- src/components/phylogeny/Phylotree.vue | 24 ++++++++++-------------- 1 file changed, 10 insertions(+), 14 deletions(-) diff --git a/src/components/phylogeny/Phylotree.vue b/src/components/phylogeny/Phylotree.vue index dfad7fa3..8e7f3726 100644 --- a/src/components/phylogeny/Phylotree.vue +++ b/src/components/phylogeny/Phylotree.vue @@ -220,30 +220,26 @@ export default { return '"' + str.replaceAll('"', "'") + '"'; }; + // FigTree allows multiple annotations on a single node, but will only display the last one. + // So we combine annotations with the same name, with the values double-pipe-delimited. + const annotationList = []; + + for (const [key, value] of Object.entries(annotations)) { + if (value.length > 0) { + annotationList.push(`${key.replaceAll(':', '.')}=${convertToNexusAnnotationValue(value.join("||"))}`); + } + } + // There are three differences between TreeViewer and other Nexus tools: // - Nexus wants annotation comments to start with '&', but that confuses TreeViewer. - // - Nexus allows us to have multiple annotations with the same label, but TreeViewer doesn't, so we - // combine annotations for TreeViewer (separated by '||'). // - Nexus allows ':' in the annotation names, while TreeViewer doesn't, so we change them into '.'s. if (this.supportTreeViewer) { - const annotationList = []; - - for (const [key, value] of Object.entries(annotations)) { - if (value.length > 0) { - annotationList.push(`${key.replaceAll(':', '.')}=${convertToNexusAnnotationValue(value.join("||"))}`); - } - } - if (annotationList.length > 0) { return `[${annotationList.join(",")}]`; } else { return undefined; } } else { - const annotationList = Object.entries(annotations).flatMap(entry => { - return entry[1].map(value => `"${entry[0]}"=${convertToNexusAnnotationValue(value)}`) - }); - if (annotationList.length > 0) { return `[&${annotationList.join(",")}]`; } else { From b0dee6127a84022e84c47214e154f6bbf5c5ef6d Mon Sep 17 00:00:00 2001 From: Gaurav Vaidya Date: Sun, 26 Jan 2025 16:47:44 -0500 Subject: [PATCH 17/23] Added support for replacing '.' with ':' in annotation keys. But only if supportTreeViewer is true. --- src/components/phylogeny/Phylotree.vue | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/components/phylogeny/Phylotree.vue b/src/components/phylogeny/Phylotree.vue index 8e7f3726..c1fdd57b 100644 --- a/src/components/phylogeny/Phylotree.vue +++ b/src/components/phylogeny/Phylotree.vue @@ -226,7 +226,9 @@ export default { for (const [key, value] of Object.entries(annotations)) { if (value.length > 0) { - annotationList.push(`${key.replaceAll(':', '.')}=${convertToNexusAnnotationValue(value.join("||"))}`); + // TreeViewer doesn't support ':' in key names, so instead we replace them with '.'. + const keyToUse = (!this.supportTreeViewer ? key : key.replace(":", ".")); + annotationList.push(`${keyToUse}=${convertToNexusAnnotationValue(value.join("||"))}`); } } From eb68dd7428fd8de971be2f4287c8777abd2c9776 Mon Sep 17 00:00:00 2001 From: Gaurav Vaidya Date: Sun, 26 Jan 2025 18:05:52 -0500 Subject: [PATCH 18/23] Improved the phyloref display code. --- src/components/phylogeny/Phylotree.vue | 21 ++++++++++++--------- src/components/phyx/PhyxView.vue | 25 ++++++++++++++++++++++--- 2 files changed, 34 insertions(+), 12 deletions(-) diff --git a/src/components/phylogeny/Phylotree.vue b/src/components/phylogeny/Phylotree.vue index c1fdd57b..248d0ef0 100644 --- a/src/components/phylogeny/Phylotree.vue +++ b/src/components/phylogeny/Phylotree.vue @@ -466,24 +466,27 @@ export default { // Make sure we don't already have an internal label node on this SVG node! let textLabel = element.selectAll("text"); // console.log(`Looking for textLabel for phyloref ${wrappedPhyloref.label} on node ${data['@id']}: `, textLabel); - // if (textLabel.empty()) element.append("text"); - if (!textLabel.empty()) textLabel.remove(); - textLabel = element.append("text"); + if (textLabel.empty()) element.append("text"); + // if (!textLabel.empty()) textLabel.remove(); + // textLabel = element.append("text"); const textLabels = []; textLabels.push(data.name); - // TODO: we should get all the alternate labels for this node. - console.log("tunits = ", wrappedPhylogeny.getTaxonomicUnitsForNodeLabel(data.name)); - - // Add all the phyloref labels for this node. + // Determine all the phyloref labels for this node. const phylorefLabels = [...phylorefsByNodeId[data["@id"]]].map(phyloref => new PhylorefWrapper(phyloref).label).sort(); textLabels.push(...phylorefLabels); console.log(`Found phylorefs for node ${data['@id']}: `, textLabels); - const textLabelText = [...new Set(textLabels + + let sortedTextLabels = [...new Set(textLabels .filter(label => label && label !== '') // Filter out undefined or blank label. .map(label => label.trim()))] // Trim all labels - .join("||"); // Join them with '||'s so we can re-separate them if needed. + .filter(label => label !== data.name); // We're going to promote the actual node label, so don't include it here. + + let textLabelText = data.name; + if (this.phylorefs.length === 1 && sortedTextLabels.length > 0) { + textLabelText = `${data.name} (${sortedTextLabels.join(", ")})`; + } console.log(`Found phyloref ${wrappedPhyloref.label}, assigned label '${textLabelText}' to ${data['@id']} from textLabels: `, textLabels); diff --git a/src/components/phyx/PhyxView.vue b/src/components/phyx/PhyxView.vue index e77bb472..5763e6d5 100644 --- a/src/components/phyx/PhyxView.vue +++ b/src/components/phyx/PhyxView.vue @@ -101,10 +101,10 @@ -
No phyloreferences in this file
+
No phyloreferences in this file
- +