Skip to content

Commit

Permalink
Merge pull request #1749 from nextstrain/james/selected-strain
Browse files Browse the repository at this point in the history
Improve interaction between selected node modal and strain filters
  • Loading branch information
jameshadfield authored Feb 8, 2024
2 parents cc6ef4e + 4dcc7d6 commit c55e902
Show file tree
Hide file tree
Showing 12 changed files with 83 additions and 94 deletions.
5 changes: 3 additions & 2 deletions src/actions/types.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 NODE_MOUSEENTER = "NODE_MOUSEENTER";
export const NODE_MOUSELEAVE = "NODE_MOUSELEAVE";
export const SEARCH_INPUT_CHANGE = "SEARCH_INPUT_CHANGE";
export const CHANGE_LAYOUT = "CHANGE_LAYOUT";
export const CHANGE_BRANCH_LABEL = "CHANGE_BRANCH_LABEL";
Expand Down Expand Up @@ -63,3 +61,6 @@ export const APPLY_MEASUREMENTS_FILTER = "APPLY_MEASUREMENTS_FILTER";
export const UPDATE_MEASUREMENTS_ERROR = "UPDATE_MEASUREMENTS_ERROR";
export const TOGGLE_SHOW_ALL_BRANCH_LABELS = "TOGGLE_SHOW_ALL_BRANCH_LABELS";
export const TOGGLE_MOBILE_DISPLAY = "TOGGLE_MOBILE_DISPLAY";
export const SELECT_NODE = "SELECT_NODE";
export const DESELECT_NODE = "DESELECT_NODE";

2 changes: 1 addition & 1 deletion src/components/info/info.js
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ class Info extends React.Component {
if (!this.props.metadata || !this.props.nodes || !this.props.visibility) return null;
const styles = computeStyles(this.props.width, this.props.browserWidth);
const animating = this.props.animationPlayPauseButton === "Pause";
const showExtended = !animating && !this.props.selectedStrain;
const showExtended = !animating;
return (
<Card center infocard>
<div style={styles.base}>
Expand Down
1 change: 1 addition & 0 deletions src/components/tree/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import UnconnectedTree from "./tree";
const Tree = connect((state) => ({
tree: state.tree,
treeToo: state.treeToo,
selectedNode: state.controls.selectedNode,
dateMinNumeric: state.controls.dateMinNumeric,
dateMaxNumeric: state.controls.dateMaxNumeric,
quickdraw: state.controls.quickdraw,
Expand Down
36 changes: 19 additions & 17 deletions src/components/tree/infoPanels/click.js
Original file line number Diff line number Diff line change
Expand Up @@ -233,43 +233,45 @@ const Trait = ({node, trait, colorings, isTerminal}) => {
/**
* A React component to display information about a tree tip in a modal-overlay style
* @param {Object} props
* @param {Object} props.tip tip node selected
* @param {function} props.goAwayCallback
* @param {object} props.colorings
* @param {Object} props.selectedNode
* @param {Object[]} props.nodes
* @param {function} props.clearSelectedNode
* @param {Object} props.colorings
* @param {Object} props.observedMutations
* @param {function} props.geneSortFn
* @param {function} props.t
*/
const NodeClickedPanel = ({selectedNode, clearSelectedNode, colorings, observedMutations, geneSortFn, t}) => {
if (selectedNode.event!=="click") {return null;}
const NodeClickedPanel = ({selectedNode, nodes, clearSelectedNode, colorings, observedMutations, geneSortFn, t}) => {
if (!selectedNode) return null;
const node = nodes[selectedNode.idx];
const panelStyle = { ...infoPanelStyles.panel};
panelStyle.maxHeight = "70%";
const node = selectedNode.node.n;
const isTip = selectedNode.type === "tip";
const isTerminal = node.fullTipCount===1;

const title = isTip ?
const isTerminal = !node.hasChildren;
const title = isTerminal ?
node.name :
isTerminal ?
`Branch leading to ${node.name}` :
"Internal branch";

return (
<div style={infoPanelStyles.modalContainer} onClick={() => clearSelectedNode(selectedNode)}>
<div style={infoPanelStyles.modalContainer} onClick={() => clearSelectedNode(selectedNode, isTerminal)}>
<div className={"panel"} style={panelStyle} onClick={(e) => stopProp(e)}>
<StrainName>{title}</StrainName>
<table>
<tbody>
{!isTip && item(t("Number of terminal tips"), node.fullTipCount)}
{isTip && <VaccineInfo node={node} t={t}/>}
{!isTerminal && item(t("Number of terminal tips"), node.fullTipCount)}
{isTerminal && <VaccineInfo node={node} t={t}/>}
<SampleDate isTerminal={isTerminal} node={node} t={t}/>
{!isTip && item("Node name", node.name)}
{isTip && <PublicationInfo node={node} t={t}/>}
{!isTerminal && item("Node name", node.name)}
{isTerminal && <PublicationInfo node={node} t={t}/>}
{getTraitsToDisplay(node).map((trait) => (
<Trait node={node} trait={trait} colorings={colorings} key={trait} isTerminal={isTerminal}/>
))}
{isTip && <AccessionAndUrl node={node}/>}
{isTerminal && <AccessionAndUrl node={node}/>}
{item("", "")}
</tbody>
</table>
<MutationTable node={node} geneSortFn={geneSortFn} isTip={isTip} observedMutations={observedMutations}/>
<MutationTable node={node} geneSortFn={geneSortFn} isTip={isTerminal} observedMutations={observedMutations}/>
<p style={infoPanelStyles.comment}>
{t("Click outside this box to go back to the tree")}
</p>
Expand Down
7 changes: 3 additions & 4 deletions src/components/tree/infoPanels/hover.js
Original file line number Diff line number Diff line change
Expand Up @@ -401,13 +401,12 @@ const HoverInfoPanel = ({
observedMutations,
t
}) => {
if (selectedNode.event !== "hover") return null;
const node = selectedNode.node.n;
if (!selectedNode) return null
const node = selectedNode.n;
const idxOfInViewRootNode = getIdxOfInViewRootNode(node);

return (
<Container node={node} panelDims={panelDims}>
{selectedNode.type === "tip" ? (
{node.hasChildren===false ? (
<>
<StrainName name={node.name}/>
<VaccineInfo node={node} t={t}/>
Expand Down
66 changes: 23 additions & 43 deletions src/components/tree/reactD3Interface/callbacks.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { updateVisibleTipsAndBranchThicknesses, applyFilter } from "../../../act
import { NODE_VISIBLE, strainSymbol } from "../../../util/globals";
import { getDomId, getParentBeyondPolytomy, getIdxOfInViewRootNode } from "../phyloTree/helpers";
import { branchStrokeForHover, branchStrokeForLeave } from "../phyloTree/renderers";
import { SELECT_NODE, DESELECT_NODE } from "../../../actions/types";

/* Callbacks used by the tips / branches when hovered / selected */

Expand All @@ -12,25 +13,16 @@ export const onTipHover = function onTipHover(d) {
this.state.treeToo;
phylotree.svg.select("#"+getDomId("tip", d.n.name))
.attr("r", (e) => e["r"] + 4);
this.setState({
selectedNode: {
node: d,
type: "tip",
event: "hover"
}
});
this.setState({hoveredNode: d});
};

export const onTipClick = function onTipClick(d) {
if (d.visibility !== NODE_VISIBLE) return;
if (this.props.narrativeMode) return;
this.setState({
selectedNode: {
node: d,
type: "tip",
event: "click"
}
});
/* 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});
this.props.dispatch(applyFilter("add", strainSymbol, [d.n.name]));
};

Expand All @@ -54,13 +46,7 @@ export const onBranchHover = function onBranchHover(d) {
}

/* Set the hovered state so that an info box can be displayed */
this.setState({
selectedNode: {
node: d,
type: "branch",
event: "hover"
}
});
this.setState({hoveredNode: d});
};

export const onBranchClick = function onBranchClick(d) {
Expand All @@ -69,13 +55,8 @@ export const onBranchClick = function onBranchClick(d) {

/* 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"
}
});
// no need to dispatch a filter action
this.props.dispatch({type: SELECT_NODE, name: d.n.name, idx: d.n.arrayIdx})
return;
}

Expand Down Expand Up @@ -114,7 +95,6 @@ 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);
Expand All @@ -125,31 +105,31 @@ export const onBranchLeave = function onBranchLeave(d) {
tree.removeConfidence();
}
/* Set selectedNode state to an empty object, which will remove the info box */
this.setState({selectedNode: {}});
this.setState({hoveredNode: null});
};

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.selectedNode) {
if (this.state.hoveredNode) {
phylotree.svg.select("#"+getDomId("tip", d.n.name))
.attr("r", (dd) => dd["r"]);
}
this.setState({selectedNode: {}});
this.setState({hoveredNode: null});
};

/* 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", selectedNode.node.n.name))
.attr("r", (dd) => dd["r"]);
this.setState({selectedNode: {}});
if (selectedNode.type==="tip") {
/* restore the tip visibility! */
this.props.dispatch(applyFilter("inactivate", strainSymbol, [selectedNode.node.n.name]));
export const clearSelectedNode = function clearSelectedNode(selectedNode, isTerminal) {
if (isTerminal) {
/* 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});
};
5 changes: 0 additions & 5 deletions src/components/tree/reactD3Interface/change.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,6 @@ export const changePhyloTreeViaPropsComparison = (mainTree, phylotree, oldProps,
Note that updating properties itself won't trigger any visual changes */
phylotree.dateRange = [newProps.dateMinNumeric, newProps.dateMaxNumeric];

/* catch selectedStrain disappearance separately to visibility and remove modal */
if (oldTreeRedux.selectedStrain && !newTreeRedux.selectedStrain) {
/* TODO change back the tip radius */
newState.selectedNode = {};
}
/* colorBy change? */
if (!!newTreeRedux.nodeColorsVersion &&
(oldTreeRedux.nodeColorsVersion !== newTreeRedux.nodeColorsVersion ||
Expand Down
14 changes: 9 additions & 5 deletions src/components/tree/tree.js
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ class Tree extends React.Component {
};
this.tangleRef = undefined;
this.state = {
selectedNode: {},
hoveredNode: null,
tree: null,
treeToo: null
};
Expand All @@ -43,8 +43,11 @@ class Tree extends React.Component {
};
/* pressing the escape key should dismiss an info modal (if one exists) */
this.handlekeydownEvent = (event) => {
if (event.key==="Escape" && this.state.selectedNode?.node) {
this.clearSelectedNode(this.state.selectedNode);
if (event.key==="Escape" && this.props.selectedNode) {
this.clearSelectedNode(
this.props.selectedNode,
!this.props.tree.nodes[this.props.selectedNode.idx].hasChildren
);
}
};
}
Expand Down Expand Up @@ -196,7 +199,7 @@ class Tree extends React.Component {
<Legend width={this.props.width}/>
</ErrorBoundary>
<HoverInfoPanel
selectedNode={this.state.selectedNode}
selectedNode={this.state.hoveredNode}
colorBy={this.props.colorBy}
colorByConfidence={this.props.colorByConfidence}
colorScale={this.props.colorScale}
Expand All @@ -208,7 +211,8 @@ class Tree extends React.Component {
/>
<NodeClickedPanel
clearSelectedNode={this.clearSelectedNode}
selectedNode={this.state.selectedNode}
selectedNode={this.props.selectedNode}
nodes={this.props.tree.nodes}
observedMutations={this.props.tree.observedMutations}
colorings={this.props.colorings}
geneSortFn={this.state.geneSortFn}
Expand Down
1 change: 0 additions & 1 deletion src/middleware/changeURL.js
Original file line number Diff line number Diff line change
Expand Up @@ -185,7 +185,6 @@ export const changeURLMiddleware = (store) => (next) => (action) => {
break;
}
case types.UPDATE_VISIBILITY_AND_BRANCH_THICKNESS: {
// query.s = action.selectedStrain ? action.selectedStrain : undefined;
query.label = action.cladeName ? action.cladeName : undefined;
break;
}
Expand Down
37 changes: 24 additions & 13 deletions src/reducers/controls.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,10 +53,6 @@ export const getDefaultControlsState = () => {
defaults,
available: undefined,
canTogglePanelLayout: true,
selectedNode: null,
region: null,
search: null,
strain: null,
temporalConfidence: { exists: false, display: false, on: false },
layout: defaults.layout,
scatterVariables: {},
Expand All @@ -75,6 +71,7 @@ export const getDefaultControlsState = () => {
explodeAttr: undefined,
selectedBranchLabel: "none",
showAllBranchLabels: false,
selectedNode: null,
canRenderBranchLabels: true,
analysisSlider: false,
geoResolution: defaults.geoResolution,
Expand Down Expand Up @@ -120,14 +117,6 @@ const Controls = (state: ControlsState = getDefaultControlsState(), action): Con
return action.controls;
case types.SET_AVAILABLE:
return Object.assign({}, state, { available: action.data });
case types.NODE_MOUSEENTER:
return Object.assign({}, state, {
selectedNode: action.data
});
case types.NODE_MOUSELEAVE:
return Object.assign({}, state, {
selectedNode: null
});
case types.CHANGE_EXPLODE_ATTR:
return Object.assign({}, state, {
explodeAttr: action.explodeAttr,
Expand Down Expand Up @@ -244,6 +233,16 @@ const Controls = (state: ControlsState = getDefaultControlsState(), action): Con
return Object.assign({}, state, {
geoResolution: action.data
});

case types.SELECT_NODE: {
const existingFilterInfo = (state.filters?.[strainSymbol]||[]).find((info) => info.value===action.name);
const existingFilterState = existingFilterInfo === undefined ? null :
existingFilterInfo.active ? 'active' : 'inactive';
return {...state, selectedNode: {name: action.name, idx: action.idx, existingFilterState}};
}
case types.DESELECT_NODE: {
return {...state, selectedNode: null}
}
case types.APPLY_FILTER: {
// values arrive as array
const filters = Object.assign({}, state.filters, {});
Expand All @@ -252,8 +251,20 @@ const Controls = (state: ControlsState = getDefaultControlsState(), action): Con
} else { // remove if no active+inactive filters
delete filters[action.trait]
}

/* In the situation where a node-selected modal is active + we have
removed or inactivated the corresponding filter, then we want to remove
the modal */
let selectedNode = state.selectedNode
if (selectedNode) {
const filterInfo = filters?.[strainSymbol]?.find((f)=>f.value===selectedNode.name);
if (!filterInfo || !filterInfo.active) {
selectedNode = null;
}
}
return Object.assign({}, state, {
filters
filters,
selectedNode,
});
}
case types.TOGGLE_TEMPORAL_CONF:
Expand Down
2 changes: 0 additions & 2 deletions src/reducers/tree.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,6 @@ export const getDefaultTreeState = () => {
totalStateCounts: {},
observedMutations: {},
availableBranchLabels: [],
selectedStrain: undefined,
selectedClade: undefined
};
};
Expand All @@ -51,7 +50,6 @@ const Tree = (state = getDefaultTreeState(), action) => {
idxOfFilteredRoot: action.idxOfFilteredRoot,
cladeName: action.cladeName,
selectedClade: action.cladeName,
selectedStrain: action.selectedStrain
};
return Object.assign({}, state, newStates);
}
Expand Down
1 change: 0 additions & 1 deletion src/reducers/treeToo.js
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,6 @@ const treeToo = (state = getDefaultTreeState(), action) => {
branchThicknessVersion: action.branchThicknessVersionToo,
idxOfInViewRootNode: action.idxOfInViewRootNodeToo,
idxOfFilteredRoot: action.idxOfFilteredRootToo,
selectedStrain: action.selectedStrain
});
}
return state;
Expand Down

0 comments on commit c55e902

Please sign in to comment.