Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improve interaction between selected node modal and strain filters #1749

Merged
merged 5 commits into from
Feb 8, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading