Skip to content

Commit fe9d629

Browse files
trvrbvictorlin
authored andcommitted
Zoom to visible nodes
This commit re-implements the "zoom to selected" function in the tree panel to emphasize visible nodes by expanding their "yValues" to take up 80% of the vertical span of the panel. Notes on implementation details: - I mirrored redux dataflow of "distanceMeasure" and "layout" to create a new redux variable of "treeZoom". This is defaults to "even" but is updated to "zoom" when clicking the "zoom to selected" tab. Further clicks increment the redux variable to "zoom-2", "zoom-3", etc... and clicking "reset layout" restores it to "even". - A PhyloTree redraw is triggered when redux treeZoom variable is updated. This allows filters to change, etc... without triggering immediate changes to layout, but then clicking "zoom to selected" will redraw layout to emphasize currently selected nodes. - phylotree.layouts contains the actual logic in the calcYValues function. This dynamically sets node.n.yValue based on node.visibility, so that calls to other layout functions like rectangularLayout will have updated node.n.yValue from which to construct node.y.
1 parent 2bca944 commit fe9d629

File tree

13 files changed

+113
-15
lines changed

13 files changed

+113
-15
lines changed

src/actions/recomputeReduxState.js

+3
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,9 @@ const modifyStateViaURLQuery = (state, query) => {
6464
if (query.m && state.branchLengthsToDisplay === "divAndDate") {
6565
state["distanceMeasure"] = query.m;
6666
}
67+
if (query.z) {
68+
state["treeZoom"] = query.z;
69+
}
6770
if (query.c) {
6871
state["colorBy"] = query.c;
6972
}

src/actions/types.js

+1
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ export const SEARCH_INPUT_CHANGE = "SEARCH_INPUT_CHANGE";
77
export const CHANGE_LAYOUT = "CHANGE_LAYOUT";
88
export const CHANGE_BRANCH_LABEL = "CHANGE_BRANCH_LABEL";
99
export const CHANGE_DISTANCE_MEASURE = "CHANGE_DISTANCE_MEASURE";
10+
export const CHANGE_TREE_ZOOM = "CHANGE_TREE_ZOOM";
1011
export const CHANGE_DATES_VISIBILITY_THICKNESS = "CHANGE_DATES_VISIBILITY_THICKNESS";
1112
export const CHANGE_ABSOLUTE_DATE_MIN = "CHANGE_ABSOLUTE_DATE_MIN";
1213
export const CHANGE_ABSOLUTE_DATE_MAX = "CHANGE_ABSOLUTE_DATE_MAX";

src/components/tree/index.js

+1
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ const Tree = connect((state) => ({
1515
temporalConfidence: state.controls.temporalConfidence,
1616
distanceMeasure: state.controls.distanceMeasure,
1717
explodeAttr: state.controls.explodeAttr,
18+
treeZoom: state.controls.treeZoom,
1819
colorScale: state.controls.colorScale,
1920
colorings: state.metadata.colorings,
2021
genomeMap: state.entropy.genomeMap,

src/components/tree/phyloTree/change.js

+7-3
Original file line numberDiff line numberDiff line change
@@ -258,6 +258,7 @@ export const change = function change({
258258
/* change these things to provided value (unless undefined) */
259259
newDistance = undefined,
260260
newLayout = undefined,
261+
newTreeZoom = undefined,
261262
updateLayout = undefined, // todo - this seems identical to `newLayout`
262263
newBranchLabellingKey = undefined,
263264
showAllBranchLabels = undefined,
@@ -313,7 +314,7 @@ export const change = function change({
313314
svgPropsToUpdate.add("stroke-width");
314315
nodePropsToModify["stroke-width"] = branchThickness;
315316
}
316-
if (newDistance || newLayout || updateLayout || zoomIntoClade || svgHasChangedDimensions || changeNodeOrder) {
317+
if (newDistance || newLayout || newTreeZoom || updateLayout || zoomIntoClade || svgHasChangedDimensions || changeNodeOrder) {
317318
elemsToUpdate.add(".tip").add(".branch.S").add(".branch.T").add(".branch");
318319
elemsToUpdate.add(".vaccineCross").add(".vaccineDottedLine").add(".conf");
319320
elemsToUpdate.add('.branchLabel').add('.tipLabel');
@@ -359,8 +360,10 @@ export const change = function change({
359360
/* run calculations as needed - these update properties on the phylotreeNodes (similar to updateNodesWithNewData) */
360361
/* distance */
361362
if (newDistance || updateLayout) this.setDistance(newDistance);
362-
/* layout (must run after distance) */
363-
if (newDistance || newLayout || updateLayout || changeNodeOrder) {
363+
/* treeZoom */
364+
if (newTreeZoom || updateLayout) this.setTreeZoom(newTreeZoom);
365+
/* layout (must run after distance and treeZoom) */
366+
if (newDistance || newLayout || newTreeZoom || updateLayout || changeNodeOrder) {
364367
this.setLayout(newLayout || this.layout, scatterVariables);
365368
}
366369
/* show confidences - set this param which actually adds the svg paths for
@@ -377,6 +380,7 @@ export const change = function change({
377380
newDistance ||
378381
newLayout ||
379382
changeNodeOrder ||
383+
newTreeZoom ||
380384
updateLayout ||
381385
zoomIntoClade ||
382386
svgHasChangedDimensions ||

src/components/tree/phyloTree/layouts.js

+59
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import { timerStart, timerEnd } from "../../../util/perf";
77
import { getTraitFromNode, getDivFromNode } from "../../../util/treeMiscHelpers";
88
import { stemParent, nodeOrdering } from "./helpers";
99
import { numDate } from "../../../util/colorHelpers";
10+
import { NODE_VISIBLE } from "../../../util/globals";
1011

1112
/**
1213
* assigns the attribute this.layout and calls the function that
@@ -288,6 +289,64 @@ export const setDistance = function setDistance(distanceAttribute) {
288289
timerEnd("setDistance");
289290
};
290291

292+
/**
293+
* given nodes add y values (node.yvalue) to every node
294+
* Nodes are the phyloTree nodes (i.e. node.n is the redux node)
295+
* Nodes must have parent child links established (via createChildrenAndParents)
296+
* PhyloTree can subsequently use this information. Accessed by prototypes
297+
* rectangularLayout, radialLayout, createChildrenAndParents
298+
* side effects: node.n.yvalue (i.e. in the redux node) and node.yRange (i.e. in the phyloTree node)
299+
*/
300+
export const calcYValues = (nodes, spacing = "even") => {
301+
// console.log("calcYValues started with ", spacing);
302+
let total = 0; /* cumulative counter of y value at tip */
303+
let calcY; /* fn called calcY(node) to return some amount of y value at a tip */
304+
if (spacing.includes("zoom") && 'visibility' in nodes[0]) {
305+
const numberOfTips = nodes.length;
306+
const numTipsVisible = nodes.map((d) => d.terminal && d.visibility === NODE_VISIBLE).filter((x) => x).length;
307+
const yPerVisible = (0.8 * numberOfTips) / numTipsVisible;
308+
const yPerNotVisible = (0.2 * numberOfTips) / (numberOfTips - numTipsVisible);
309+
calcY = (node) => {
310+
total += node.visibility === NODE_VISIBLE ? yPerVisible : yPerNotVisible;
311+
return total;
312+
};
313+
} else { /* fall back to even spacing */
314+
if (spacing !== "even") console.warn("falling back to even spacing of y values. Unknown arg:", spacing);
315+
calcY = () => ++total;
316+
}
317+
318+
const recurse = (node) => {
319+
if (node.children) {
320+
for (let i = node.children.length - 1; i >= 0; i--) {
321+
recurse(node.children[i]);
322+
}
323+
} else {
324+
node.n.yvalue = calcY(node);
325+
node.yRange = [node.n.yvalue, node.n.yvalue];
326+
return;
327+
}
328+
/* if here, then all children have yvalues, but we dont. */
329+
node.n.yvalue = node.children.reduce((acc, d) => acc + d.n.yvalue, 0) / node.children.length;
330+
node.yRange = [node.n.children[0].yvalue, node.n.children[node.n.children.length - 1].yvalue];
331+
};
332+
recurse(nodes[0]);
333+
};
334+
335+
/**
336+
* assigns the attribute this.treeZoom and calls the function that
337+
* recalculates yvalues based on treeZoom setting
338+
* @param treeZoom -- how to zoom nodes, eg ["even", "zoom"]
339+
*/
340+
export const setTreeZoom = function setTreeZoom(treeZoom) {
341+
timerStart("setTreeZoom");
342+
if (typeof treeZoom === "undefined") {
343+
this.treeZoom = "even";
344+
} else {
345+
this.treeZoom = treeZoom;
346+
}
347+
calcYValues(this.nodes, this.treeZoom);
348+
timerEnd("setTreeZoom");
349+
};
291350

292351
/**
293352
* Initializes and sets the range of the scales (this.xScale, this.yScale)

src/components/tree/phyloTree/phyloTree.js

+1
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@ PhyloTree.prototype.updateColorBy = renderers.updateColorBy;
6363
/* 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 ) */
6464
PhyloTree.prototype.setDistance = layouts.setDistance;
6565
PhyloTree.prototype.setLayout = layouts.setLayout;
66+
PhyloTree.prototype.setTreeZoom = layouts.setTreeZoom;
6667
PhyloTree.prototype.rectangularLayout = layouts.rectangularLayout;
6768
PhyloTree.prototype.scatterplotLayout = layouts.scatterplotLayout;
6869
PhyloTree.prototype.unrootedLayout = layouts.unrootedLayout;

src/components/tree/phyloTree/renderers.js

+3-1
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import { getEmphasizedColor } from "../../../util/colorHelpers";
77
* @param {d3 selection} svg -- the svg into which the tree is drawn
88
* @param {string} layout -- the layout to be used, e.g. "rect"
99
* @param {string} distance -- the property used as branch length, e.g. div or num_date
10+
* @param {string} treeZoom -- how to to treat spread of yValues, e.g. "even" or "zoom"
1011
* @param {object} parameters -- an object that contains options that will be added to this.params
1112
* @param {object} callbacks -- an object with call back function defining mouse behavior
1213
* @param {array} branchThickness -- array of branch thicknesses (same ordering as tree nodes)
@@ -21,7 +22,7 @@ import { getEmphasizedColor } from "../../../util/colorHelpers";
2122
* @param {object} scatterVariables -- {x, y} properties to map nodes => scatterplot (only used if layout="scatter")
2223
* @return {null}
2324
*/
24-
export const render = function render(svg, layout, distance, parameters, callbacks, branchThickness, visibility, drawConfidence, vaccines, branchStroke, tipStroke, tipFill, tipRadii, dateRange, scatterVariables) {
25+
export const render = function render(svg, layout, distance, treeZoom, parameters, callbacks, branchThickness, visibility, drawConfidence, vaccines, branchStroke, tipStroke, tipFill, tipRadii, dateRange, scatterVariables) {
2526
timerStart("phyloTree render()");
2627
this.svg = svg;
2728
this.params = Object.assign(this.params, parameters);
@@ -42,6 +43,7 @@ export const render = function render(svg, layout, distance, parameters, callbac
4243
/* set x, y values & scale them to the screen */
4344
setDisplayOrder(this.nodes);
4445
this.setDistance(distance);
46+
this.setTreeZoom(treeZoom);
4547
this.setLayout(layout, scatterVariables);
4648
this.mapToScreen();
4749

src/components/tree/reactD3Interface/change.js

+6
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,12 @@ export const changePhyloTreeViaPropsComparison = (mainTree, phylotree, oldProps,
4949
args.changeNodeOrder = true;
5050
}
5151

52+
/* change treeZoom behavior */
53+
if (oldProps.treeZoom !== newProps.treeZoom) {
54+
args.newTreeZoom = newProps.treeZoom;
55+
args.updateLayout = true;
56+
}
57+
5258
/* change in key used to define branch labels, tip labels */
5359
if (oldProps.canRenderBranchLabels===true && newProps.canRenderBranchLabels===false) {
5460
args.newBranchLabellingKey = "none";

src/components/tree/reactD3Interface/initialRender.js

+1
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ export const renderTree = (that, main, phylotree, props) => {
2222
select(ref),
2323
props.layout,
2424
props.distanceMeasure,
25+
props.treeZoom,
2526
{ /* parameters (modifies PhyloTree's defaults) */
2627
grid: true,
2728
confidence: props.temporalConfidence.display,

src/components/tree/tree.js

+18-9
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import React from "react";
22
import { withTranslation } from "react-i18next";
33
import { FaSearchMinus } from "react-icons/fa";
44
import { updateVisibleTipsAndBranchThicknesses } from "../../actions/tree";
5+
import { CHANGE_TREE_ZOOM } from "../../actions/types";
56
import Card from "../framework/card";
67
import Legend from "./legend/legend";
78
import PhyloTree from "./phyloTree/phyloTree";
@@ -39,6 +40,7 @@ class Tree extends React.Component {
3940
this.clearSelectedNode = callbacks.clearSelectedNode.bind(this);
4041
// this.handleIconClickHOF = callbacks.handleIconClickHOF.bind(this);
4142
this.redrawTree = () => {
43+
this.props.dispatch({ type: CHANGE_TREE_ZOOM, data: "even" });
4244
this.props.dispatch(updateVisibleTipsAndBranchThicknesses({
4345
root: [0, 0]
4446
}));
@@ -110,15 +112,9 @@ class Tree extends React.Component {
110112
}
111113

112114
getStyles = () => {
113-
const activeResetTreeButton = this.props.tree.idxOfInViewRootNode !== 0 ||
114-
this.props.treeToo.idxOfInViewRootNode !== 0;
115-
116-
const filteredTree = !!this.props.tree.idxOfFilteredRoot &&
117-
this.props.tree.idxOfInViewRootNode !== this.props.tree.idxOfFilteredRoot;
118-
const filteredTreeToo = !!this.props.treeToo.idxOfFilteredRoot &&
119-
this.props.treeToo.idxOfInViewRootNode !== this.props.treeToo.idxOfFilteredRoot;
120-
const activeZoomButton = filteredTree || filteredTreeToo;
121-
115+
// FIXME: double-check this
116+
const activeResetTreeButton = true;
117+
const activeZoomButton = true;
122118
const treeIsZoomed = this.props.tree.idxOfInViewRootNode !== 0 ||
123119
this.props.treeToo.idxOfInViewRootNode !== 0;
124120

@@ -167,6 +163,19 @@ class Tree extends React.Component {
167163
}
168164

169165
zoomToSelected = () => {
166+
// if currently set to "even", start at "zoom"
167+
let treeZoomData = "zoom";
168+
if (this.props.treeZoom.includes("zoom")) {
169+
// if currently at "zoom", increment to "zoom-2"
170+
if (!this.props.treeZoom.includes("-")) {
171+
treeZoomData = "zoom-2";
172+
} else {
173+
// if currently at "zoom-2", increment to "zoom-3", etc...
174+
const increment = parseInt(this.props.treeZoom.split('-')[1], 10) + 1;
175+
treeZoomData = "zoom-" + increment.toString();
176+
}
177+
}
178+
this.props.dispatch({ type: CHANGE_TREE_ZOOM, data: treeZoomData });
170179
this.props.dispatch(updateVisibleTipsAndBranchThicknesses({
171180
root: [this.props.tree.idxOfFilteredRoot, this.props.treeToo.idxOfFilteredRoot]
172181
}));

src/middleware/changeURL.js

+4
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,10 @@ export const changeURLMiddleware = (store) => (next) => (action) => {
140140
query.p = action.notInURLState === true ? undefined : action.data;
141141
break;
142142
}
143+
case types.CHANGE_TREE_ZOOM: {
144+
query.z = action.data === state.controls.defaults.treeZoom ? undefined : action.data;
145+
break;
146+
}
143147
case types.TOGGLE_SIDEBAR: {
144148
// we never add this to the URL on purpose -- it should be manually set as it specifies a world
145149
// where resizes can not open / close the sidebar. The exception is if it's toggled, we

src/reducers/controls.ts

+8-2
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { defaultGeoResolution,
44
defaultDateRange,
55
defaultDistanceMeasure,
66
defaultLayout,
7+
defaultTreeZoom,
78
controlsHiddenWidth,
89
strainSymbol,
910
twoColumnBreakpoint } from "../util/globals";
@@ -43,6 +44,7 @@ export const getDefaultControlsState = () => {
4344
const defaults: Partial<ControlsState> = {
4445
distanceMeasure: defaultDistanceMeasure,
4546
layout: defaultLayout,
47+
treeZoom: defaultTreeZoom,
4648
geoResolution: defaultGeoResolution,
4749
filters: {},
4850
filtersInFooter: [],
@@ -70,6 +72,7 @@ export const getDefaultControlsState = () => {
7072
layout: defaults.layout,
7173
scatterVariables: {},
7274
distanceMeasure: defaults.distanceMeasure,
75+
treeZoom: defaults.treeZoom,
7376
dateMin,
7477
dateMinNumeric,
7578
dateMax,
@@ -191,8 +194,11 @@ const Controls = (state: ControlsState = getDefaultControlsState(), action): Con
191194
});
192195
}
193196
return Object.assign({}, state, updatesToState);
194-
}
195-
case types.CHANGE_DATES_VISIBILITY_THICKNESS: {
197+
case types.CHANGE_TREE_ZOOM:
198+
return Object.assign({}, state, {
199+
treeZoom: action.data
200+
});
201+
case types.CHANGE_DATES_VISIBILITY_THICKNESS: {
196202
const newDates: Partial<ControlsState> = { quickdraw: action.quickdraw };
197203
if (action.dateMin) {
198204
newDates.dateMin = action.dateMin;

src/util/globals.js

+1
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ export const defaultColorBy = "country";
2727
export const defaultGeoResolution = "country";
2828
export const defaultLayout = "rect";
2929
export const defaultDistanceMeasure = "num_date";
30+
export const defaultTreeZoom = "even";
3031
export const defaultDateRange = 6;
3132
export const date_select = true;
3233
export const file_prefix = "Zika_";

0 commit comments

Comments
 (0)