Skip to content

Commit

Permalink
🚧 Vertically focus on filtered nodes
Browse files Browse the repository at this point in the history
This commit adds a focus button in the tree panel to emphasize visible
nodes by expanding their "yValues" to take up 80% of the vertical span
of the panel.
  • Loading branch information
trvrb authored and victorlin committed Oct 8, 2024
1 parent f1c22d1 commit 5cdc564
Show file tree
Hide file tree
Showing 12 changed files with 112 additions and 5 deletions.
1 change: 1 addition & 0 deletions src/actions/tree.js
Original file line number Diff line number Diff line change
Expand Up @@ -294,6 +294,7 @@ export const applyFilter = (mode, trait, values) => {
}
dispatch({type: types.APPLY_FILTER, trait, values: newValues});
dispatch(updateVisibleTipsAndBranchThicknesses());
// FIXME: re-focus with one of the above dispatches or new dispatch
};
};

Expand Down
1 change: 1 addition & 0 deletions src/actions/types.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ export const SEARCH_INPUT_CHANGE = "SEARCH_INPUT_CHANGE";
export const CHANGE_LAYOUT = "CHANGE_LAYOUT";
export const CHANGE_BRANCH_LABEL = "CHANGE_BRANCH_LABEL";
export const CHANGE_DISTANCE_MEASURE = "CHANGE_DISTANCE_MEASURE";
export const TOGGLE_TREE_FOCUS = "TOGGLE_TREE_FOCUS";
export const CHANGE_DATES_VISIBILITY_THICKNESS = "CHANGE_DATES_VISIBILITY_THICKNESS";
export const CHANGE_ABSOLUTE_DATE_MIN = "CHANGE_ABSOLUTE_DATE_MIN";
export const CHANGE_ABSOLUTE_DATE_MAX = "CHANGE_ABSOLUTE_DATE_MAX";
Expand Down
1 change: 1 addition & 0 deletions src/components/tree/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ const Tree = connect((state: RootState) => ({
temporalConfidence: state.controls.temporalConfidence,
distanceMeasure: state.controls.distanceMeasure,
explodeAttr: state.controls.explodeAttr,
treeFocus: state.controls.treeFocus,
colorScale: state.controls.colorScale,
colorings: state.metadata.colorings,
genomeMap: state.entropy.genomeMap,
Expand Down
10 changes: 7 additions & 3 deletions src/components/tree/phyloTree/change.js
Original file line number Diff line number Diff line change
Expand Up @@ -258,6 +258,7 @@ export const change = function change({
/* change these things to provided value (unless undefined) */
newDistance = undefined,
newLayout = undefined,
newTreeFocus = undefined,
updateLayout = undefined, // todo - this seems identical to `newLayout`
newBranchLabellingKey = undefined,
showAllBranchLabels = undefined,
Expand Down Expand Up @@ -313,7 +314,7 @@ export const change = function change({
svgPropsToUpdate.add("stroke-width");
nodePropsToModify["stroke-width"] = branchThickness;
}
if (newDistance || newLayout || updateLayout || zoomIntoClade || svgHasChangedDimensions || changeNodeOrder) {
if (newDistance || newLayout || newTreeFocus || updateLayout || zoomIntoClade || svgHasChangedDimensions || changeNodeOrder) {
elemsToUpdate.add(".tip").add(".branch.S").add(".branch.T").add(".branch");
elemsToUpdate.add(".vaccineCross").add(".vaccineDottedLine").add(".conf");
elemsToUpdate.add('.branchLabel').add('.tipLabel');
Expand Down Expand Up @@ -359,8 +360,10 @@ export const change = function change({
/* run calculations as needed - these update properties on the phylotreeNodes (similar to updateNodesWithNewData) */
/* distance */
if (newDistance || updateLayout) this.setDistance(newDistance);
/* layout (must run after distance) */
if (newDistance || newLayout || updateLayout || changeNodeOrder) {
/* treeFocus */
if (newTreeFocus || updateLayout) this.setTreeFocus(newTreeFocus);
/* layout (must run after distance and treeFocus) */
if (newDistance || newLayout || newTreeFocus || updateLayout || changeNodeOrder) {
this.setLayout(newLayout || this.layout, scatterVariables);
}
/* show confidences - set this param which actually adds the svg paths for
Expand All @@ -377,6 +380,7 @@ export const change = function change({
newDistance ||
newLayout ||
changeNodeOrder ||
newTreeFocus ||
updateLayout ||
zoomIntoClade ||
svgHasChangedDimensions ||
Expand Down
53 changes: 53 additions & 0 deletions src/components/tree/phyloTree/layouts.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { timerStart, timerEnd } from "../../../util/perf";
import { getTraitFromNode, getDivFromNode } from "../../../util/treeMiscHelpers";
import { stemParent, nodeOrdering } from "./helpers";
import { numDate } from "../../../util/colorHelpers";
import { NODE_VISIBLE } from "../../../util/globals";

/**
* assigns the attribute this.layout and calls the function that
Expand Down Expand Up @@ -288,6 +289,58 @@ export const setDistance = function setDistance(distanceAttribute) {
timerEnd("setDistance");
};

/**
* given nodes add y values (node.displayOrder) to every node
* Nodes are the phyloTree nodes (i.e. node.n is the redux node)
* Nodes must have parent child links established (via createChildrenAndParents)
* PhyloTree can subsequently use this information. Accessed by prototypes
* rectangularLayout, radialLayout, createChildrenAndParents
* side effects: node.displayOrder and node.displayOrderRange (i.e. in the phyloTree node)
*/
export const calcYValues = (nodes, focus) => {
// console.log("calcYValues started with ", spacing);
let total = 0; /* cumulative counter of y value at tip */
let calcY; /* fn called calcY(node) to return some amount of y value at a tip */
if (focus && 'visibility' in nodes[0]) {
const numberOfTips = nodes.length;
const numTipsVisible = nodes.filter((d) => !d.hasChildren && d.visibility === NODE_VISIBLE).length;
const yPerVisible = (0.8 * numberOfTips) / numTipsVisible;
const yPerNotVisible = (0.2 * numberOfTips) / (numberOfTips - numTipsVisible);
calcY = (node) => {
total += node.visibility === NODE_VISIBLE ? yPerVisible : yPerNotVisible;
return total;
};
} else { /* fall back to no focus */
calcY = () => ++total;
}

const recurse = (node) => {
const children = node.n.children; // (redux) tree node
if (children && children.length) {
for (let i = children.length - 1; i >= 0; i--) {
recurse(children[i].shell);
}
} else {
node.displayOrder = calcY(node);
node.displayOrderRange = [node.displayOrder, node.displayOrder];
return;
}
/* if here, then all children have yvalues, but we dont. */
node.displayOrder = children.reduce((acc, d) => acc + d.shell.displayOrder, 0) / children.length;
node.displayOrderRange = [children[0].shell.displayOrder, children[children.length - 1].shell.displayOrder];
};
recurse(nodes[0]);
};

/**
* Recalculates y values based on focus setting
* @param treeFocus -- whether to focus on filtered nodes
*/
export const setTreeFocus = function setTreeFocus(treeFocus) {
timerStart("setTreeFocus");
calcYValues(this.nodes, treeFocus || false);
timerEnd("setTreeFocus");
};

/**
* Initializes and sets the range of the scales (this.xScale, this.yScale)
Expand Down
1 change: 1 addition & 0 deletions src/components/tree/phyloTree/phyloTree.js
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ PhyloTree.prototype.updateColorBy = renderers.updateColorBy;
/* C A L C U L A T E G E O M E T R I E S E T C ( M O D I F I E S N O D E S , N O T S V G ) */
PhyloTree.prototype.setDistance = layouts.setDistance;
PhyloTree.prototype.setLayout = layouts.setLayout;
PhyloTree.prototype.setTreeFocus = layouts.setTreeFocus;
PhyloTree.prototype.rectangularLayout = layouts.rectangularLayout;
PhyloTree.prototype.scatterplotLayout = layouts.scatterplotLayout;
PhyloTree.prototype.unrootedLayout = layouts.unrootedLayout;
Expand Down
4 changes: 3 additions & 1 deletion src/components/tree/phyloTree/renderers.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { getEmphasizedColor } from "../../../util/colorHelpers";
* @param {d3 selection} svg -- the svg into which the tree is drawn
* @param {string} layout -- the layout to be used, e.g. "rect"
* @param {string} distance -- the property used as branch length, e.g. div or num_date
* @param {string} treeFocus -- whether to focus on filtered nodes
* @param {object} parameters -- an object that contains options that will be added to this.params
* @param {object} callbacks -- an object with call back function defining mouse behavior
* @param {array} branchThickness -- array of branch thicknesses (same ordering as tree nodes)
Expand All @@ -21,7 +22,7 @@ import { getEmphasizedColor } from "../../../util/colorHelpers";
* @param {object} scatterVariables -- {x, y} properties to map nodes => scatterplot (only used if layout="scatter")
* @return {null}
*/
export const render = function render(svg, layout, distance, parameters, callbacks, branchThickness, visibility, drawConfidence, vaccines, branchStroke, tipStroke, tipFill, tipRadii, dateRange, scatterVariables) {
export const render = function render(svg, layout, distance, treeFocus, parameters, callbacks, branchThickness, visibility, drawConfidence, vaccines, branchStroke, tipStroke, tipFill, tipRadii, dateRange, scatterVariables) {
timerStart("phyloTree render()");
this.svg = svg;
this.params = Object.assign(this.params, parameters);
Expand All @@ -42,6 +43,7 @@ export const render = function render(svg, layout, distance, parameters, callbac
/* set x, y values & scale them to the screen */
setDisplayOrder(this.nodes);
this.setDistance(distance);
this.setTreeFocus(treeFocus);
this.setLayout(layout, scatterVariables);
this.mapToScreen();

Expand Down
9 changes: 9 additions & 0 deletions src/components/tree/reactD3Interface/change.js
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,15 @@ export const changePhyloTreeViaPropsComparison = (mainTree, phylotree, oldProps,
args.changeNodeOrder = true;
}

/* change treeFocus behavior */
// FIXME: re-focus when:
// 1. filters have changed or been removed
// 2. zoom level has changed
if (oldProps.treeFocus !== newProps.treeFocus) {
args.newTreeFocus = newProps.treeFocus;
args.updateLayout = true;
}

/* change in key used to define branch labels, tip labels */
if (oldProps.canRenderBranchLabels===true && newProps.canRenderBranchLabels===false) {
args.newBranchLabellingKey = "none";
Expand Down
1 change: 1 addition & 0 deletions src/components/tree/reactD3Interface/initialRender.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ export const renderTree = (that, main, phylotree, props) => {
select(ref),
props.layout,
props.distanceMeasure,
props.treeFocus,
{ /* parameters (modifies PhyloTree's defaults) */
grid: true,
confidence: props.temporalConfidence.display,
Expand Down
21 changes: 21 additions & 0 deletions src/components/tree/tree.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import React from "react";
import { withTranslation } from "react-i18next";
import { FaSearchMinus } from "react-icons/fa";
import { updateVisibleTipsAndBranchThicknesses } from "../../actions/tree";
import { TOGGLE_TREE_FOCUS } from "../../actions/types";
import Card from "../framework/card";
import Legend from "./legend/legend";
import PhyloTree from "./phyloTree/phyloTree";
Expand Down Expand Up @@ -42,6 +43,7 @@ class Tree extends React.Component {
}

redrawTree = () => {
this.props.dispatch({ type: TOGGLE_TREE_FOCUS, focus: false });
this.props.dispatch(updateVisibleTipsAndBranchThicknesses({
root: [0, 0]
}));
Expand Down Expand Up @@ -127,6 +129,8 @@ class Tree extends React.Component {

const activeResetTreeButton = anyTreeZoomed;

const activeFocusButton = true;

return {
treeButtonsDiv: {
zIndex: 100,
Expand All @@ -148,6 +152,13 @@ class Tree extends React.Component {
color: activeZoomButton ? darkGrey : lightGrey,
pointerEvents: activeZoomButton ? "auto" : "none"
},
focusOnSelectedButton: {
zIndex: 100,
display: "inline-block",
cursor: activeFocusButton ? "pointer" : "auto",
color: activeFocusButton ? darkGrey : lightGrey,
pointerEvents: activeFocusButton ? "auto" : "none"
},
zoomOutButton: {
zIndex: 100,
display: "inline-block",
Expand All @@ -171,6 +182,10 @@ class Tree extends React.Component {
);
}

toggleFocus = () => {
this.props.dispatch({ type: TOGGLE_TREE_FOCUS });
};

zoomToSelected = () => {
this.props.dispatch(updateVisibleTipsAndBranchThicknesses({
root: [this.props.tree.idxOfFilteredRoot, this.props.treeToo.idxOfFilteredRoot]
Expand Down Expand Up @@ -262,6 +277,12 @@ class Tree extends React.Component {
>
{t("Zoom to Selected")}
</button>
<button
style={{...tabSingle, ...styles.focusOnSelectedButton}}
onClick={this.toggleFocus}
>
{this.props.treeFocus ? t("Reset Focus") : t("Focus on Selected")}
</button>
<button
style={{...tabSingle, ...styles.resetTreeButton}}
onClick={this.redrawTree}
Expand Down
14 changes: 13 additions & 1 deletion src/reducers/controls.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { defaultGeoResolution,
defaultDateRange,
defaultDistanceMeasure,
defaultLayout,
defaultTreeFocus,
controlsHiddenWidth,
strainSymbol,
twoColumnBreakpoint } from "../util/globals";
Expand All @@ -17,6 +18,7 @@ export interface BasicControlsState {
panelsToDisplay: string[]
showTreeToo: boolean
canTogglePanelLayout: boolean
treeFocus: boolean

// This allows arbitrary prop names while TypeScript adoption is incomplete.
// TODO: add all other props explicitly and remove this.
Expand All @@ -43,6 +45,7 @@ export const getDefaultControlsState = () => {
const defaults: Partial<ControlsState> = {
distanceMeasure: defaultDistanceMeasure,
layout: defaultLayout,
treeFocus: defaultTreeFocus,
geoResolution: defaultGeoResolution,
filters: {},
filtersInFooter: [],
Expand Down Expand Up @@ -70,6 +73,7 @@ export const getDefaultControlsState = () => {
layout: defaults.layout,
scatterVariables: {},
distanceMeasure: defaults.distanceMeasure,
treeFocus: defaults.treeFocus,
dateMin,
dateMinNumeric,
dateMax,
Expand Down Expand Up @@ -192,7 +196,15 @@ const Controls = (state: ControlsState = getDefaultControlsState(), action): Con
}
return Object.assign({}, state, updatesToState);
}
case types.CHANGE_DATES_VISIBILITY_THICKNESS: {
case types.TOGGLE_TREE_FOCUS:
let newValue = !state.treeFocus;

Check failure on line 200 in src/reducers/controls.ts

View workflow job for this annotation

GitHub Actions / lint (16)

Unexpected lexical declaration in case block

Check failure on line 200 in src/reducers/controls.ts

View workflow job for this annotation

GitHub Actions / lint (18)

Unexpected lexical declaration in case block

Check failure on line 200 in src/reducers/controls.ts

View workflow job for this annotation

GitHub Actions / lint (20)

Unexpected lexical declaration in case block

Check failure on line 200 in src/reducers/controls.ts

View workflow job for this annotation

GitHub Actions / lint (22)

Unexpected lexical declaration in case block
if ("focus" in action) {
newValue = action.focus;
}
return Object.assign({}, state, {
treeFocus: newValue
});
case types.CHANGE_DATES_VISIBILITY_THICKNESS: {
const newDates: Partial<ControlsState> = { quickdraw: action.quickdraw };
if (action.dateMin) {
newDates.dateMin = action.dateMin;
Expand Down
1 change: 1 addition & 0 deletions src/util/globals.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ export const defaultColorBy = "country";
export const defaultGeoResolution = "country";
export const defaultLayout = "rect";
export const defaultDistanceMeasure = "num_date";
export const defaultTreeFocus = false;
export const defaultDateRange = 6;
export const date_select = true;
export const file_prefix = "Zika_";
Expand Down

0 comments on commit 5cdc564

Please sign in to comment.