From 926f6ff7ef565415794e65c6ac1fada20e71ffe5 Mon Sep 17 00:00:00 2001 From: James Hadfield Date: Mon, 18 Oct 2021 12:11:04 +1300 Subject: [PATCH 1/5] Remove unused state This is in preparation for enabling branch-selected modals --- src/actions/types.js | 2 -- src/components/tree/reactD3Interface/change.js | 1 - src/components/tree/tree.js | 1 - src/reducers/controls.js | 9 --------- 4 files changed, 13 deletions(-) diff --git a/src/actions/types.js b/src/actions/types.js index 130540fa4..e65ca880f 100644 --- a/src/actions/types.js +++ b/src/actions/types.js @@ -3,8 +3,6 @@ export const NEW_COLORS = "NEW_COLORS"; export const LOAD_FREQUENCIES = "LOAD_FREQUENCIES"; export const FREQUENCY_MATRIX = "FREQUENCY_MATRIX"; export const BROWSER_DIMENSIONS = "BROWSER_DIMENSIONS"; -export const BRANCH_MOUSEENTER = "BRANCH_MOUSEENTER"; -export const BRANCH_MOUSELEAVE = "BRANCH_MOUSELEAVE"; export const NODE_MOUSEENTER = "NODE_MOUSEENTER"; export const NODE_MOUSELEAVE = "NODE_MOUSELEAVE"; export const SEARCH_INPUT_CHANGE = "SEARCH_INPUT_CHANGE"; diff --git a/src/components/tree/reactD3Interface/change.js b/src/components/tree/reactD3Interface/change.js index 085da07e2..477789f2c 100644 --- a/src/components/tree/reactD3Interface/change.js +++ b/src/components/tree/reactD3Interface/change.js @@ -88,7 +88,6 @@ export const changePhyloTreeViaPropsComparison = (mainTree, phylotree, oldProps, if (oldTreeRedux.idxOfInViewRootNode !== newTreeRedux.idxOfInViewRootNode) { const rootNode = phylotree.nodes[newTreeRedux.idxOfInViewRootNode]; args.zoomIntoClade = rootNode; - newState.selectedBranch = newTreeRedux.idxOfInViewRootNode === 0 ? null : rootNode; newState.selectedTip = null; newState.hovered = null; if (newProps.layout === "unrooted") { diff --git a/src/components/tree/tree.js b/src/components/tree/tree.js index d810c8cf1..13106a365 100644 --- a/src/components/tree/tree.js +++ b/src/components/tree/tree.js @@ -29,7 +29,6 @@ class Tree extends React.Component { this.tangleRef = undefined; this.state = { hover: null, - selectedBranch: null, selectedTip: null, tree: null, treeToo: null diff --git a/src/reducers/controls.js b/src/reducers/controls.js index a3cc7525d..efa31e83c 100644 --- a/src/reducers/controls.js +++ b/src/reducers/controls.js @@ -40,7 +40,6 @@ export const getDefaultControlsState = () => { defaults, available: undefined, canTogglePanelLayout: true, - selectedBranch: null, selectedNode: null, region: null, search: null, @@ -102,14 +101,6 @@ const Controls = (state = getDefaultControlsState(), action) => { return action.controls; case types.SET_AVAILABLE: return Object.assign({}, state, { available: action.data }); - case types.BRANCH_MOUSEENTER: - return Object.assign({}, state, { - selectedBranch: action.data - }); - case types.BRANCH_MOUSELEAVE: - return Object.assign({}, state, { - selectedBranch: null - }); case types.NODE_MOUSEENTER: return Object.assign({}, state, { selectedNode: action.data From cffb38207bbe5f2ed253c78efaaae1e012836428 Mon Sep 17 00:00:00 2001 From: James Hadfield Date: Mon, 18 Oct 2021 14:04:45 +1300 Subject: [PATCH 2/5] Shift-click branch to display more info This paves the way to display more branch-specific info in a user-friendly way, as requested in #1317 [1]. Content of the panel will be improved in further commits. Other UI implementations may be explored, but detecting if the shift-key was pressed is the simplest for now. [1] https://github.com/nextstrain/auspice/issues/1417 --- src/components/tree/infoPanels/click.js | 10 +-- src/components/tree/infoPanels/hover.js | 9 +-- .../tree/reactD3Interface/callbacks.js | 63 +++++++++++++------ .../tree/reactD3Interface/change.js | 6 +- src/components/tree/tree.js | 15 +++-- 5 files changed, 63 insertions(+), 40 deletions(-) diff --git a/src/components/tree/infoPanels/click.js b/src/components/tree/infoPanels/click.js index b7585705b..b76454309 100644 --- a/src/components/tree/infoPanels/click.js +++ b/src/components/tree/infoPanels/click.js @@ -255,14 +255,14 @@ const Trait = ({node, trait, colorings}) => { * @param {function} props.goAwayCallback * @param {object} props.colorings */ -const TipClickedPanel = ({tip, goAwayCallback, colorings, t}) => { - if (!tip) {return null;} +const NodeClickedPanel = ({selectedNode, clearSelectedNode, colorings, t}) => { + if (selectedNode.event!=="click") {return null;} const panelStyle = { ...infoPanelStyles.panel}; panelStyle.maxHeight = "70%"; - const node = tip.n; + const node = selectedNode.node.n; const mutationsToRoot = collectMutations(node); return ( -
goAwayCallback(tip)}> +
clearSelectedNode(selectedNode)}>
stopProp(e)}> {node.name} @@ -286,4 +286,4 @@ const TipClickedPanel = ({tip, goAwayCallback, colorings, t}) => { ); }; -export default TipClickedPanel; +export default NodeClickedPanel; diff --git a/src/components/tree/infoPanels/hover.js b/src/components/tree/infoPanels/hover.js index 80e72a302..2656ee387 100644 --- a/src/components/tree/infoPanels/hover.js +++ b/src/components/tree/infoPanels/hover.js @@ -349,7 +349,7 @@ const Comment = ({children}) => ( ); const HoverInfoPanel = ({ - hovered, + selectedNode, colorBy, colorByConfidence, colorScale, @@ -357,13 +357,13 @@ const HoverInfoPanel = ({ colorings, t }) => { - if (!hovered) return null; - const node = hovered.d.n; + if (selectedNode.event !== "hover") return null; + const node = selectedNode.node.n; const idxOfInViewRootNode = getIdxOfInViewRootNode(node); return ( - {hovered.type === ".tip" ? ( + {selectedNode.type === "tip" ? ( <> @@ -382,6 +382,7 @@ const HoverInfoPanel = ({ {idxOfInViewRootNode === node.arrayIdx ? t('Click to zoom out to parent clade') : t('Click to zoom into clade')} + {t("Shift + Click to display more info")} )} diff --git a/src/components/tree/reactD3Interface/callbacks.js b/src/components/tree/reactD3Interface/callbacks.js index 2a7267787..d9b1fddf2 100644 --- a/src/components/tree/reactD3Interface/callbacks.js +++ b/src/components/tree/reactD3Interface/callbacks.js @@ -13,7 +13,11 @@ export const onTipHover = function onTipHover(d) { phylotree.svg.select(getDomId("#tip", d.n.name)) .attr("r", (e) => e["r"] + 4); this.setState({ - hovered: {d, type: ".tip"} + selectedNode: { + node: d, + type: "tip", + event: "hover" + } }); }; @@ -21,8 +25,11 @@ export const onTipClick = function onTipClick(d) { if (d.visibility !== NODE_VISIBLE) return; if (this.props.narrativeMode) return; this.setState({ - hovered: null, - selectedTip: d + selectedNode: { + node: d, + type: "tip", + event: "click" + } }); this.props.dispatch(applyFilter("add", strainSymbol, [d.n.name])); }; @@ -48,13 +55,30 @@ export const onBranchHover = function onBranchHover(d) { /* Set the hovered state so that an info box can be displayed */ this.setState({ - hovered: {d, type: ".branch"} + selectedNode: { + node: d, + type: "branch", + event: "hover" + } }); }; export const onBranchClick = function onBranchClick(d) { if (d.visibility !== NODE_VISIBLE) return; if (this.props.narrativeMode) return; + + /* if a branch was clicked while holding the shift key, we instead display a node-clicked modal */ + if (window.event.shiftKey) { + this.setState({ + selectedNode: { + node: d, + type: "branch", + event: "click" + } + }); + return; + } + const root = [undefined, undefined]; let cladeSelected; // Branches with multiple labels will be used in the order specified by this.props.tree.availableBranchLabels @@ -89,6 +113,8 @@ export const onBranchClick = function onBranchClick(d) { /* onBranchLeave called when mouse-off, i.e. anti-hover */ export const onBranchLeave = function onBranchLeave(d) { + if (this.state.selectedNode.event!=="hover") return; + /* Reset the stroke back to what it was before */ branchStrokeForLeave(d); @@ -97,33 +123,32 @@ export const onBranchLeave = function onBranchLeave(d) { const tree = d.that.params.orientation[0] === 1 ? this.state.tree : this.state.treeToo; tree.removeConfidence(); } - /* Set hovered state to `null`, which will remove the info box */ - if (this.state.hovered) { - this.setState({hovered: null}); - } + /* Set selectedNode state to an empty object, which will remove the info box */ + this.setState({selectedNode: {}}); }; export const onTipLeave = function onTipLeave(d) { + if (this.state.selectedNode.event!=="hover") return; const phylotree = d.that.params.orientation[0] === 1 ? this.state.tree : this.state.treeToo; - if (!this.state.selectedTip) { + if (!this.state.selectedNode) { phylotree.svg.select(getDomId("#tip", d.n.name)) .attr("r", (dd) => dd["r"]); } - if (this.state.hovered) { - this.setState({hovered: null}); - } + this.setState({selectedNode: {}}); }; -/* clearSelectedTip when clicking to go away */ -export const clearSelectedTip = function clearSelectedTip(d) { - const phylotree = d.that.params.orientation[0] === 1 ? +/* clearSelectedNode when clicking to remove the node-selected modal */ +export const clearSelectedNode = function clearSelectedNode(selectedNode) { + const phylotree = selectedNode.node.that.params.orientation[0] === 1 ? this.state.tree : this.state.treeToo; - phylotree.svg.select(getDomId("#tip", d.n.name)) + phylotree.svg.select(getDomId("#tip", selectedNode.node.n.name)) .attr("r", (dd) => dd["r"]); - this.setState({selectedTip: null, hovered: null}); - /* restore the tip visibility! */ - this.props.dispatch(applyFilter("inactivate", strainSymbol, [d.n.name])); + this.setState({selectedNode: {}}); + if (selectedNode.type==="tip") { + /* restore the tip visibility! */ + this.props.dispatch(applyFilter("inactivate", strainSymbol, [selectedNode.node.n.name])); + } }; diff --git a/src/components/tree/reactD3Interface/change.js b/src/components/tree/reactD3Interface/change.js index 477789f2c..16c57e664 100644 --- a/src/components/tree/reactD3Interface/change.js +++ b/src/components/tree/reactD3Interface/change.js @@ -15,8 +15,7 @@ export const changePhyloTreeViaPropsComparison = (mainTree, phylotree, oldProps, /* catch selectedStrain dissapearence seperately to visibility and remove modal */ if (oldTreeRedux.selectedStrain && !newTreeRedux.selectedStrain) { /* TODO change back the tip radius */ - newState.selectedTip = null; - newState.hovered = null; + newState.selectedNode = {}; } /* colorBy change? */ if (!!newTreeRedux.nodeColorsVersion && @@ -88,8 +87,7 @@ export const changePhyloTreeViaPropsComparison = (mainTree, phylotree, oldProps, if (oldTreeRedux.idxOfInViewRootNode !== newTreeRedux.idxOfInViewRootNode) { const rootNode = phylotree.nodes[newTreeRedux.idxOfInViewRootNode]; args.zoomIntoClade = rootNode; - newState.selectedTip = null; - newState.hovered = null; + newState.selectedNode = {}; if (newProps.layout === "unrooted") { args.updateLayout = true; } diff --git a/src/components/tree/tree.js b/src/components/tree/tree.js index 13106a365..7e3f72e68 100644 --- a/src/components/tree/tree.js +++ b/src/components/tree/tree.js @@ -7,7 +7,7 @@ import Legend from "./legend/legend"; import PhyloTree from "./phyloTree/phyloTree"; import { getParentBeyondPolytomy } from "./phyloTree/helpers"; import HoverInfoPanel from "./infoPanels/hover"; -import TipClickedPanel from "./infoPanels/click"; +import NodeClickedPanel from "./infoPanels/click"; import { changePhyloTreeViaPropsComparison } from "./reactD3Interface/change"; import * as callbacks from "./reactD3Interface/callbacks"; import { tabSingle, darkGrey, lightGrey } from "../../globalStyles"; @@ -28,13 +28,12 @@ class Tree extends React.Component { }; this.tangleRef = undefined; this.state = { - hover: null, - selectedTip: null, + selectedNode: {}, tree: null, treeToo: null }; /* bind callbacks */ - this.clearSelectedTip = callbacks.clearSelectedTip.bind(this); + this.clearSelectedNode = callbacks.clearSelectedNode.bind(this); // this.handleIconClickHOF = callbacks.handleIconClickHOF.bind(this); this.redrawTree = () => { this.props.dispatch(updateVisibleTipsAndBranchThicknesses({ @@ -184,7 +183,7 @@ class Tree extends React.Component { - From 073f372b62a49aafc05254f26c193b203ee75d7e Mon Sep 17 00:00:00 2001 From: James Hadfield Date: Mon, 18 Oct 2021 17:49:05 +1300 Subject: [PATCH 3/5] Improve display for node-clicked info panel The previous info-panel was only available for terminal nodes (tips). This adapts the component to render information relevant to a tip or a branch, as required. --- src/components/tree/infoPanels/click.js | 69 ++++++++++++++++++------- 1 file changed, 49 insertions(+), 20 deletions(-) diff --git a/src/components/tree/infoPanels/click.js b/src/components/tree/infoPanels/click.js index b76454309..4fbf01203 100644 --- a/src/components/tree/infoPanels/click.js +++ b/src/components/tree/infoPanels/click.js @@ -58,7 +58,7 @@ const Link = ({url, title, value}) => ( * todo: sort genes by position in genome * todo: provide in-app links from mutations to color-bys? filters? */ -const MutationTable = ({mutations}) => { +const MutationTable = ({node, isTip}) => { const geneSortFn = (a, b) => { if (a[0]==="nuc") return 1; if (b[0]==="nuc") return -1; @@ -68,10 +68,21 @@ const MutationTable = ({mutations}) => { const [aa, bb] = [parseInt(a.slice(1, -1), 10), parseInt(b.slice(1, -1), 10)]; return aa - + ); +const Button = styled.button` + border: 0px; + background-color: inherit; + cursor: pointer; + outline: 0; + text-decoration: underline; +`; + /** * Render a 2-column table of gene -> mutations. * Rows are sorted by gene name, alphabetically, with "nuc" last. @@ -68,16 +77,32 @@ const MutationTable = ({node, isTip}) => { const [aa, bb] = [parseInt(a.slice(1, -1), 10), parseInt(b.slice(1, -1), 10)]; return aa { + if (gene==="nuc" && isTip && muts.length>10) { + return ( +
+ +
+ ); + } + return ( +
+ {gene}: {muts.sort(mutSortFn).join(", ")} +
+ ); + }; let mutations; if (isTip) { - mutations = collectMutations(node); + mutations = collectMutations(node, true); } else if (node.branch_attrs && node.branch_attrs.mutations && Object.keys(node.branch_attrs.mutations).length) { mutations = node.branch_attrs.mutations; } if (!mutations) return null; - const title = isTip ? "Amino acid changes from root" : "Mutations on branch"; + const title = isTip ? "Mutations from root" : "Mutations on branch"; // we encode the table here (rather than via `item()`) to set component keys appropriately return ( @@ -86,11 +111,7 @@ const MutationTable = ({node, isTip}) => {
); From 9aafd61209a65082af48d7a4e4d3da73047df193 Mon Sep 17 00:00:00 2001 From: James Hadfield Date: Tue, 19 Oct 2021 12:09:37 +1300 Subject: [PATCH 5/5] Sort displayed genes by genome position This sorting is used in hover/click info-panels in the tree. --- src/components/tree/infoPanels/click.js | 17 +++++---------- src/components/tree/infoPanels/hover.js | 12 ++++++---- src/components/tree/tree.js | 4 ++++ src/util/treeMiscHelpers.js | 29 +++++++++++++++++++++++++ 4 files changed, 47 insertions(+), 15 deletions(-) diff --git a/src/components/tree/infoPanels/click.js b/src/components/tree/infoPanels/click.js index 6108e3b3b..5cd339297 100644 --- a/src/components/tree/infoPanels/click.js +++ b/src/components/tree/infoPanels/click.js @@ -67,17 +67,12 @@ const Button = styled.button` * todo: sort genes by position in genome * todo: provide in-app links from mutations to color-bys? filters? */ -const MutationTable = ({node, isTip}) => { - const geneSortFn = (a, b) => { - if (a[0]==="nuc") return 1; - if (b[0]==="nuc") return -1; - return a[0] { const mutSortFn = (a, b) => { const [aa, bb] = [parseInt(a.slice(1, -1), 10), parseInt(b.slice(1, -1), 10)]; return aa { + const displayGeneMutations = (gene, muts) => { if (gene==="nuc" && isTip && muts.length>10) { return (
@@ -109,9 +104,9 @@ const MutationTable = ({node, isTip}) => {
); @@ -295,7 +290,7 @@ const Trait = ({node, trait, colorings, isTerminal}) => { * @param {function} props.goAwayCallback * @param {object} props.colorings */ -const NodeClickedPanel = ({selectedNode, clearSelectedNode, colorings, t}) => { +const NodeClickedPanel = ({selectedNode, clearSelectedNode, colorings, geneSortFn, t}) => { if (selectedNode.event!=="click") {return null;} const panelStyle = { ...infoPanelStyles.panel}; panelStyle.maxHeight = "70%"; @@ -325,7 +320,7 @@ const NodeClickedPanel = ({selectedNode, clearSelectedNode, colorings, t}) => { ))} {isTip && } {item("", "")} - +
{"Mutations from root"}{title} { Object.entries(mutations) .sort(geneSortFn) @@ -204,7 +215,7 @@ const StrainName = ({children}) => (

{children}

); -const SampleDate = ({node, t}) => { +const SampleDate = ({isTerminal, node, t}) => { const date = getTraitFromNode(node, "num_date"); if (!date) return null; @@ -212,13 +223,13 @@ const SampleDate = ({node, t}) => { if (date && dateUncertainty && dateUncertainty[0] !== dateUncertainty[1]) { return ( <> - {item(t("Inferred collection date"), numericToCalendar(date))} + {item(t(isTerminal ? "Inferred collection date" : "Inferred date"), numericToCalendar(date))} {item(t("Date Confidence Interval"), `(${numericToCalendar(dateUncertainty[0])}, ${numericToCalendar(dateUncertainty[1])})`)} ); } - - return item(t("Collection date"), numericToCalendar(date)); + /* internal nodes are always inferred, regardless of whether uncertainty bounds are present */ + return item(t(isTerminal ? "Collection date" : "Inferred date"), numericToCalendar(date)); }; const getTraitsToDisplay = (node) => { @@ -228,16 +239,24 @@ const getTraitsToDisplay = (node) => { return Object.keys(node.node_attrs).filter((k) => !ignore.includes(k)); }; -const Trait = ({node, trait, colorings}) => { - const value_tmp = getTraitFromNode(node, trait); - let value = value_tmp; - if (typeof value_tmp === "number") { - if (!Number.isInteger(value_tmp)) { - value = Number.parseFloat(value_tmp).toPrecision(3); +const Trait = ({node, trait, colorings, isTerminal}) => { + let value = getTraitFromNode(node, trait); + const confidence = getTraitFromNode(node, trait, {confidence: true}); + + if (typeof value === "number") { + if (!Number.isInteger(value)) { + value = Number.parseFloat(value).toPrecision(3); } } if (!isValueValid(value)) return null; + if (confidence && value in confidence) { + /* if it's a tip with one confidence value > 0.99 then we interpret this as a known (i.e. not inferred) state */ + if (!isTerminal || confidence[value]<0.99) { + value = `${value} (${(100 * confidence[value]).toFixed(0)}%)`; + } + } + const name = (colorings && colorings[trait] && colorings[trait].title) ? colorings[trait].title : trait; @@ -260,22 +279,32 @@ const NodeClickedPanel = ({selectedNode, clearSelectedNode, colorings, t}) => { const panelStyle = { ...infoPanelStyles.panel}; panelStyle.maxHeight = "70%"; const node = selectedNode.node.n; - const mutationsToRoot = collectMutations(node); + const isTip = selectedNode.type === "tip"; + const isTerminal = node.fullTipCount===1; + + const title = isTip ? + node.name : + isTerminal ? + `Branch leading to ${node.name}` : + "Internal branch"; + return (
clearSelectedNode(selectedNode)}>
stopProp(e)}> - {node.name} + {title} - - - + {!isTip && item(t("Number of terminal tips"), node.fullTipCount)} + {isTip && } + + {!isTip && item("Node name", node.name)} + {isTip && } {getTraitsToDisplay(node).map((trait) => ( - + ))} - + {isTip && } {item("", "")} - +

From f3fa9b880fac1f6ddb8b6972e8b1c514eed25067 Mon Sep 17 00:00:00 2001 From: James Hadfield Date: Tue, 19 Oct 2021 11:19:31 +1300 Subject: [PATCH 4/5] Provide nucleotide mutations on tip-click The original reason for excluding these was due to the large number of mutations causing the modal box to be too large. Using a "click to copy" button here solves this problem, and allows us to interrogate all mutations in a strain which is often very useful. --- src/components/tree/infoPanels/click.js | 35 ++++++++++++++++++++----- 1 file changed, 28 insertions(+), 7 deletions(-) diff --git a/src/components/tree/infoPanels/click.js b/src/components/tree/infoPanels/click.js index 4fbf01203..6108e3b3b 100644 --- a/src/components/tree/infoPanels/click.js +++ b/src/components/tree/infoPanels/click.js @@ -1,4 +1,5 @@ import React from "react"; +import styled from 'styled-components'; import { isValueValid } from "../../../util/globals"; import { infoPanelStyles } from "../../../globalStyles"; import { numericToCalendar } from "../../../util/dateHelpers"; @@ -51,6 +52,14 @@ const Link = ({url, title, value}) => (

{ Object.entries(mutations) .sort(geneSortFn) - .map(([gene, muts]) => ( -
- {gene}: {muts.sort(mutSortFn).join(", ")} -
- )) + .map(displayGeneMutations) }
{title} { - Object.entries(mutations) + Object.keys(mutations) .sort(geneSortFn) - .map(displayGeneMutations) + .map((gene) => displayGeneMutations(gene, mutations[gene])) }

diff --git a/src/components/tree/infoPanels/hover.js b/src/components/tree/infoPanels/hover.js index 2656ee387..52d34bc64 100644 --- a/src/components/tree/infoPanels/hover.js +++ b/src/components/tree/infoPanels/hover.js @@ -141,8 +141,9 @@ const ColorBy = ({node, colorBy, colorByConfidence, colorScale, colorings}) => { * A React Component to Display AA / NT mutations, if present. * @param {Object} props * @param {Object} props.node branch node which is currently highlighted + * @param {Object} props.geneSortFn function to sort a list of genes */ -const Mutations = ({node, t}) => { +const Mutations = ({node, geneSortFn, t}) => { if (!node.branch_attrs || !node.branch_attrs.mutations) return null; const elements = []; // elements to render const mutations = node.branch_attrs.mutations; @@ -183,7 +184,9 @@ const Mutations = ({node, t}) => { /* --------- AMINO ACID MUTATIONS --------------- */ /* AA mutations are found at `mutations[prot_name]` -> Array of strings */ - const prots = Object.keys(mutations).filter((v) => v !== "nuc"); + const prots = Object.keys(mutations) + .sort(geneSortFn) + .filter((v) => v !== "nuc"); const mutationsToDisplay = {}; let shouldDisplay = false; @@ -355,6 +358,7 @@ const HoverInfoPanel = ({ colorScale, panelDims, colorings, + geneSortFn, t }) => { if (selectedNode.event !== "hover") return null; @@ -367,7 +371,7 @@ const HoverInfoPanel = ({ <> - + @@ -376,7 +380,7 @@ const HoverInfoPanel = ({ ) : ( <> - + diff --git a/src/components/tree/tree.js b/src/components/tree/tree.js index 7e3f72e68..a0011d201 100644 --- a/src/components/tree/tree.js +++ b/src/components/tree/tree.js @@ -16,6 +16,7 @@ import Tangle from "./tangle"; import { attemptUntangle } from "../../util/globals"; import ErrorBoundary from "../../util/errorBoundry"; import { untangleTreeToo } from "./tangle/untangling"; +import { sortByGeneOrder } from "../../util/treeMiscHelpers"; export const spaceBetweenTrees = 100; @@ -58,6 +59,7 @@ class Tree extends React.Component { if (this.props.showTreeToo) { this.setUpAndRenderTreeToo(this.props, newState); /* modifies newState in place */ } + newState.geneSortFn = sortByGeneOrder(this.props.metadata.genomeAnnotations); this.setState(newState); /* this will trigger an unneccessary CDU :( */ } } @@ -188,6 +190,7 @@ class Tree extends React.Component { colorByConfidence={this.props.colorByConfidence} colorScale={this.props.colorScale} colorings={this.props.metadata.colorings} + geneSortFn={this.state.geneSortFn} panelDims={{width: this.props.width, height: this.props.height, spaceBetweenTrees}} t={t} /> @@ -195,6 +198,7 @@ class Tree extends React.Component { clearSelectedNode={this.clearSelectedNode} selectedNode={this.state.selectedNode} colorings={this.props.metadata.colorings} + geneSortFn={this.state.geneSortFn} t={t} /> {this.props.showTangle && this.state.tree && this.state.treeToo ? ( diff --git a/src/util/treeMiscHelpers.js b/src/util/treeMiscHelpers.js index 82f4af346..696cadbca 100644 --- a/src/util/treeMiscHelpers.js +++ b/src/util/treeMiscHelpers.js @@ -171,3 +171,32 @@ export const collectMutations = (fromNode, include_nuc=false) => { }); return mutations; }; + + +/** + * Returns a function which will sort a list, where each element in the list + * is a gene name. Sorted by start position of the gene, with "nuc" last. + */ +export const sortByGeneOrder = (genomeAnnotations) => { + if (!(genomeAnnotations instanceof Object)) { + return (a, b) => { + if (a==="nuc") return 1; + if (b==="nuc") return -1; + return 0; + }; + } + const geneOrder = Object.entries(genomeAnnotations) + .sort((a, b) => { + if (b[0]==="nuc") return -1; // show nucleotide "gene" last + if (a[1].start < b[1].start) return -1; + if (a[1].start > b[1].start) return 1; + return 0; + }) + .map(([name]) => name); + + return (a, b) => { + if (geneOrder.indexOf(a) < geneOrder.indexOf(b)) return -1; + if (geneOrder.indexOf(a) > geneOrder.indexOf(b)) return 1; + return 0; + }; +};