UNPKG

auspice

Version:

Web app for visualizing pathogen evolution

201 lines (174 loc) 8.97 kB
import { Selection, select, event as d3event } from "d3-selection"; import { updateVisibleTipsAndBranchThicknesses, applyFilter, Root } from "../../../actions/tree"; import { NODE_VISIBLE, strainSymbol } from "../../../util/globals"; import { getDomId, getParentBeyondPolytomy, getIdxOfInViewRootNode } from "../phyloTree/helpers"; import { branchStrokeForHover, branchStrokeForLeave, LabelDatum, nonHoveredRippleOpacity } from "../phyloTree/renderers"; import { PhyloNode } from "../phyloTree/types"; import { SELECT_NODE, DESELECT_NODE } from "../../../actions/types"; import { SelectedNode } from "../../../reducers/controls"; import { TreeComponent } from "../tree"; import { getEmphasizedColor } from "../../../util/colorHelpers"; /* Callbacks used by the tips / branches when hovered / selected */ export const onTipHover = function onTipHover(this: TreeComponent, d: PhyloNode): void { if (d.visibility !== NODE_VISIBLE) return; const phylotree = d.that.params.orientation[0] === 1 ? this.state.tree : this.state.treeToo; phylotree.svg.select("#"+getDomId("tip", d.n.name)) .attr("r", (e) => e["r"] + 4); this.setState({ hoveredNode: {node: d, isBranch: false} }); }; export const onTipClick = function onTipClick(this: TreeComponent, d: PhyloNode): void { if (d.visibility !== NODE_VISIBLE) return; if (this.props.narrativeMode) return; /* The order of these two dispatches is important: the reducer handling `SELECT_NODE` must have access to the filtering state _prior_ to these filters being applied */ this.props.dispatch({type: SELECT_NODE, name: d.n.name, idx: d.n.arrayIdx, isBranch: false, treeId: d.that.id}); this.props.dispatch(applyFilter("add", strainSymbol, [d.n.name])); }; export const onBranchHover = function onBranchHover(this: TreeComponent, d: PhyloNode): void { if (d.visibility !== NODE_VISIBLE) return; branchStrokeForHover(d); /* if temporal confidence bounds are defined for this branch, then display them on hover */ if (this.props.temporalConfidence.exists && this.props.temporalConfidence.display && !this.props.temporalConfidence.on) { const tree = d.that.params.orientation[0] === 1 ? this.state.tree : this.state.treeToo; if (!("confidenceIntervals" in tree.groups)) { tree.groups.confidenceIntervals = tree.svg.append("g").attr("id", "confidenceIntervals"); } tree.groups.confidenceIntervals .selectAll(".conf") .data([d]) .enter() .call((sel) => tree.drawSingleCI(sel, 0.5)); } /* Set the hovered state so that an info box can be displayed */ this.setState({ hoveredNode: {node: d, isBranch: true} }); }; export const onBranchClick = function onBranchClick(this: TreeComponent, d: PhyloNode): void { 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 */ /* NOTE: window.event is deprecated, however the version of d3-selection we're using doesn't supply the event as an argument */ if (window.event instanceof PointerEvent && window.event.shiftKey) { // no need to dispatch a filter action this.props.dispatch({type: SELECT_NODE, name: d.n.name, idx: d.n.arrayIdx, isBranch: true, treeId: d.that.id}) return; } const LHSTree = d.that.params.orientation[0] === 1; const zoomBackwards = getIdxOfInViewRootNode(d.n) === d.n.arrayIdx; /** Handle the case where we are clicking on the in-view root which is also a stream, i.e. * we want to zoom back to the parent stream */ if (zoomBackwards && this.props.showStreamTrees) { // Note: streamtrees only (currently) work for single trees const parentStreamName = this.props.tree.streams[d.n.streamName].parentStreamName; if (parentStreamName) { // if this is false we are zooming back into the "normal" tree, so use the non-stream-tree code path const parentStreamIndex = this.props.tree.streams[parentStreamName].startNode return this.props.dispatch(updateVisibleTipsAndBranchThicknesses({ root: [parentStreamIndex, undefined], })); } } /* Clicking on a branch means we want to zoom into the clade defined by that branch _except_ when it's the "in-view" root branch, in which case we want to zoom out */ const arrayIdxToZoomTo = zoomBackwards ? getParentBeyondPolytomy(d.n, this.props.distanceMeasure, LHSTree ? this.props.tree.observedMutations : this.props.treeToo.observedMutations).arrayIdx : d.n.arrayIdx; const root: Root = LHSTree ? [arrayIdxToZoomTo, undefined] : [undefined, arrayIdxToZoomTo]; /* clade selected (as used in the URL query) is only designed to work for the main tree, not the RHS tree */ this.props.dispatch(updateVisibleTipsAndBranchThicknesses({root})); }; /* onBranchLeave called when mouse-off, i.e. anti-hover */ export const onBranchLeave = function onBranchLeave(this: TreeComponent, d: PhyloNode): void { /* Reset the stroke back to what it was before */ branchStrokeForLeave(d); /* Remove the temporal confidence bar unless it's meant to be displayed */ if (this.props.temporalConfidence.exists && this.props.temporalConfidence.display && !this.props.temporalConfidence.on) { const tree = d.that.params.orientation[0] === 1 ? this.state.tree : this.state.treeToo; tree.removeConfidence(); } /* Set selectedNode state to an empty object, which will remove the info box */ this.setState({hoveredNode: null}); }; export const onTipLeave = function onTipLeave(this: TreeComponent, d: PhyloNode): void { const phylotree = d.that.params.orientation[0] === 1 ? this.state.tree : this.state.treeToo; if (this.state.hoveredNode) { phylotree.svg.select("#"+getDomId("tip", d.n.name)) .attr("r", (dd) => dd["r"]); } this.setState({hoveredNode: null}); }; /* clearSelectedNode when clicking to remove the node-selected modal */ export const clearSelectedNode = function clearSelectedNode(this: TreeComponent, selectedNode: SelectedNode): void { if (!selectedNode.isBranch) { /* perform the filtering action (if necessary) that will restore the filtering state of the node prior to the selection */ if (!selectedNode.existingFilterState) { this.props.dispatch(applyFilter("remove", strainSymbol, [selectedNode.name])); } else if (selectedNode.existingFilterState==='inactive') { this.props.dispatch(applyFilter("inactivate", strainSymbol, [selectedNode.name])); } /* else the filter was already active, so leave it unchanged */ } this.props.dispatch({type: DESELECT_NODE}); }; export function onStreamHover(this: TreeComponent, node: PhyloNode, categoryIndex: number, paths: SVGPathElement[], isBranch: boolean): void { /** For each ripple (SVGPathElement) _not_ hovered, lower the opacity so that we focus attention on the hovered ribbon */ if (isBranch) { select(paths[0]).style("stroke", getEmphasizedColor(node.branchStroke)) } else { paths.forEach((path, i) => { if (i===categoryIndex) { select(path).attr("fill", getEmphasizedColor(node.n.streamCategories[categoryIndex].color)) } else { select(path).style('opacity', nonHoveredRippleOpacity) } }) } /* Ensure the label is visible & enlarged */ const selection = selectStreamLabel(node); if (selection.data()?.at(0)?.visibility==='hidden') { selection.attr("visibility", "visible") selection.attr("font-size", 16) } this.setState({hoveredNode: { node, isBranch, streamDetails: {x: d3event.layerX, y: d3event.layerY, categoryIndex} }}); } export function onStreamLeave(this: TreeComponent, node: PhyloNode, categoryIndex: number, paths: SVGPathElement[], isBranch): void { if (isBranch) { /* return branch colour back to normal */ select(paths[0]).style("stroke", node.branchStroke) } else { /** ensure each ripple's opacity is reset back to 1 */ paths.forEach((path, i) => { if (i===categoryIndex) { select(path).attr("fill", node.n.streamCategories[categoryIndex].color) } else { select(path).style('opacity', 1) } }) } /* Ensure the label goes back to its previous state */ const selection = selectStreamLabel(node); if (selection.data()?.at(0)?.visibility==='hidden') { selection.attr("visibility", "hidden") } this.setState({hoveredNode: null}); } function selectStreamLabel(node: PhyloNode): Selection<SVGTextElement, LabelDatum, null, unknown> { // When `groups.streamsLabels` is created we haven't bound any data so it's datum type is `unknown` // We subsequently bind `LabelDatum` elements, but we can't change the underlying type without an assertion // eslint-disable-next-line @typescript-eslint/consistent-type-assertions return node.that.groups.streamsLabels .select(`#${CSS.escape(`label${node.n.streamName}`)}`) as Selection<SVGTextElement, LabelDatum, null, unknown>; }