Skip to content

Commit

Permalink
Zoom to visible nodes
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
trvrb committed Jul 11, 2021
1 parent 6fbd8eb commit e4b02c5
Show file tree
Hide file tree
Showing 13 changed files with 112 additions and 14 deletions.
3 changes: 3 additions & 0 deletions src/actions/recomputeReduxState.js
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,9 @@ const modifyStateViaURLQuery = (state, query) => {
if (query.m && state.branchLengthsToDisplay === "divAndDate") {
state["distanceMeasure"] = query.m;
}
if (query.z) {
state["treeZoom"] = query.z;
}
if (query.c) {
state["colorBy"] = query.c;
}
Expand Down
1 change: 1 addition & 0 deletions src/actions/types.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,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 CHANGE_TREE_ZOOM = "CHANGE_TREE_ZOOM";
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.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ const Tree = connect((state) => ({
scatterVariables: state.controls.scatterVariables,
temporalConfidence: state.controls.temporalConfidence,
distanceMeasure: state.controls.distanceMeasure,
treeZoom: state.controls.treeZoom,
mutType: state.controls.mutType,
colorScale: state.controls.colorScale,
metadata: state.metadata,
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 @@ -256,6 +256,7 @@ export const change = function change({
/* change these things to provided value (unless undefined) */
newDistance = undefined,
newLayout = undefined,
newTreeZoom = undefined,
updateLayout = undefined, // todo - this seems identical to `newLayout`
newBranchLabellingKey = undefined,
newTipLabelKey = undefined,
Expand Down Expand Up @@ -310,7 +311,7 @@ export const change = function change({
svgPropsToUpdate.add("stroke-width");
nodePropsToModify["stroke-width"] = branchThickness;
}
if (newDistance || newLayout || updateLayout || zoomIntoClade || svgHasChangedDimensions) {
if (newDistance || newLayout || newTreeZoom || updateLayout || zoomIntoClade || svgHasChangedDimensions) {
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 @@ -344,8 +345,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) {
/* treeZoom */
if (newTreeZoom || updateLayout) this.setTreeZoom(newTreeZoom);
/* layout (must run after distance and treeZoom) */
if (newDistance || newLayout || newTreeZoom || updateLayout) {
this.setLayout(newLayout || this.layout, scatterVariables);
}
/* show confidences - set this param which actually adds the svg paths for
Expand All @@ -356,6 +359,7 @@ export const change = function change({
svgPropsToUpdate.has(["stroke-width"]) ||
newDistance ||
newLayout ||
newTreeZoom ||
updateLayout ||
zoomIntoClade ||
svgHasChangedDimensions ||
Expand Down
61 changes: 60 additions & 1 deletion src/components/tree/phyloTree/layouts.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,11 @@
import { min, max } from "d3-array";
import scaleLinear from "d3-scale/src/linear";
import {point as scalePoint} from "d3-scale/src/band";
import { addLeafCount} from "./helpers";
import { addLeafCount } from "./helpers";
import { calculateRegressionThroughRoot, calculateRegressionWithFreeIntercept } from "./regression";
import { timerStart, timerEnd } from "../../../util/perf";
import { getTraitFromNode, getDivFromNode } from "../../../util/treeMiscHelpers";
import { NODE_VISIBLE } from "../../../util/globals";

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

/**
* given nodes add y values (node.yvalue) 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.n.yvalue (i.e. in the redux node) and node.yRange (i.e. in the phyloTree node)
*/
export const calcYValues = (nodes, spacing = "even") => {
// 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 (spacing.includes("zoom") && 'visibility' in nodes[0]) {
const numberOfTips = nodes.length;
const numTipsVisible = nodes.map((d) => d.terminal && d.visibility === NODE_VISIBLE).filter((x) => x).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 even spacing */
if (spacing !== "even") console.warn("falling back to even spacing of y values. Unknown arg:", spacing);
calcY = () => ++total;
}

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

/**
* assigns the attribute this.treeZoom and calls the function that
* recalculates yvalues based on treeZoom setting
* @param treeZoom -- how to zoom nodes, eg ["even", "zoom"]
*/
export const setTreeZoom = function setTreeZoom(treeZoom) {
timerStart("setTreeZoom");
if (typeof treeZoom === "undefined") {
this.treeZoom = "even";
} else {
this.treeZoom = treeZoom;
}
calcYValues(this.nodes, this.treeZoom);
timerEnd("setTreeZoom");
};

/**
* 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 @@ -65,6 +65,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.setTreeZoom = layouts.setTreeZoom;
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} treeZoom -- how to to treat spread of yValues, e.g. "even" or "zoom"
* @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, treeZoom, 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 @@ -31,6 +32,7 @@ export const render = function render(svg, layout, distance, parameters, callbac

/* set x, y values & scale them to the screen */
this.setDistance(distance);
this.setTreeZoom(treeZoom);
this.setLayout(layout, scatterVariables);
this.mapToScreen();

Expand Down
6 changes: 6 additions & 0 deletions src/components/tree/reactD3Interface/change.js
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,12 @@ export const changePhyloTreeViaPropsComparison = (mainTree, phylotree, oldProps,
args.newDistance = newProps.distanceMeasure;
}

/* change treeZoom behavior */
if (oldProps.treeZoom !== newProps.treeZoom) {
args.newTreeZoom = newProps.treeZoom;
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.treeZoom,
{ /* parameters (modifies PhyloTree's defaults) */
grid: true,
confidence: props.temporalConfidence.display,
Expand Down
26 changes: 17 additions & 9 deletions src/components/tree/tree.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import React from "react";
import { withTranslation } from "react-i18next";
import { updateVisibleTipsAndBranchThicknesses } from "../../actions/tree";
import { CHANGE_TREE_ZOOM } from "../../actions/types";
import Card from "../framework/card";
import Legend from "./legend/legend";
import PhyloTree from "./phyloTree/phyloTree";
Expand Down Expand Up @@ -36,6 +37,7 @@ class Tree extends React.Component {
this.clearSelectedTip = callbacks.clearSelectedTip.bind(this);
// this.handleIconClickHOF = callbacks.handleIconClickHOF.bind(this);
this.redrawTree = () => {
this.props.dispatch({ type: CHANGE_TREE_ZOOM, data: "even" });
this.props.dispatch(updateVisibleTipsAndBranchThicknesses({
root: [0, 0]
}));
Expand Down Expand Up @@ -95,15 +97,8 @@ class Tree extends React.Component {
}

getStyles = () => {
const activeResetTreeButton = this.props.tree.idxOfInViewRootNode !== 0 ||
this.props.treeToo.idxOfInViewRootNode !== 0;

const filteredTree = !!this.props.tree.idxOfFilteredRoot &&
this.props.tree.idxOfInViewRootNode !== this.props.tree.idxOfFilteredRoot;
const filteredTreeToo = !!this.props.treeToo.idxOfFilteredRoot &&
this.props.treeToo.idxOfInViewRootNode !== this.props.treeToo.idxOfFilteredRoot;
const activeZoomButton = filteredTree || filteredTreeToo;

const activeResetTreeButton = true;
const activeZoomButton = true;
return {
treeButtonsDiv: {
zIndex: 100,
Expand Down Expand Up @@ -141,6 +136,19 @@ class Tree extends React.Component {
}

zoomToSelected = () => {
// if currently set to "even", start at "zoom"
let treeZoomData = "zoom";
if (this.props.treeZoom.includes("zoom")) {
// if currently at "zoom", increment to "zoom-2"
if (!this.props.treeZoom.includes("-")) {
treeZoomData = "zoom-2";
} else {
// if currently at "zoom-2", increment to "zoom-3", etc...
const increment = parseInt(this.props.treeZoom.split('-')[1], 10) + 1;
treeZoomData = "zoom-" + increment.toString();
}
}
this.props.dispatch({ type: CHANGE_TREE_ZOOM, data: treeZoomData });
this.props.dispatch(updateVisibleTipsAndBranchThicknesses({
root: [this.props.tree.idxOfFilteredRoot, this.props.treeToo.idxOfFilteredRoot]
}));
Expand Down
4 changes: 4 additions & 0 deletions src/middleware/changeURL.js
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,10 @@ export const changeURLMiddleware = (store) => (next) => (action) => {
query.p = action.notInURLState === true ? undefined : action.data;
break;
}
case types.CHANGE_TREE_ZOOM: {
query.z = action.data === state.controls.defaults.treeZoom ? undefined : action.data;
break;
}
case types.TOGGLE_SIDEBAR: {
// we never add this to the URL on purpose -- it should be manually set as it specifies a world
// where resizes can not open / close the sidebar. The exception is if it's toggled, we
Expand Down
7 changes: 7 additions & 0 deletions src/reducers/controls.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { defaultGeoResolution,
defaultDateRange,
defaultDistanceMeasure,
defaultLayout,
defaultTreeZoom,
defaultMutType,
controlsHiddenWidth,
strainSymbol,
Expand All @@ -19,6 +20,7 @@ export const getDefaultControlsState = () => {
const defaults = {
distanceMeasure: defaultDistanceMeasure,
layout: defaultLayout,
treeZoom: defaultTreeZoom,
geoResolution: defaultGeoResolution,
filters: {},
colorBy: defaultColorBy,
Expand Down Expand Up @@ -51,6 +53,7 @@ export const getDefaultControlsState = () => {
layout: defaults.layout,
scatterVariables: {},
distanceMeasure: defaults.distanceMeasure,
treeZoom: defaults.treeZoom,
dateMin,
dateMinNumeric,
dateMax,
Expand Down Expand Up @@ -153,6 +156,10 @@ const Controls = (state = getDefaultControlsState(), action) => {
});
}
return Object.assign({}, state, updatesToState);
case types.CHANGE_TREE_ZOOM:
return Object.assign({}, state, {
treeZoom: action.data
});
case types.CHANGE_DATES_VISIBILITY_THICKNESS: {
const newDates = { quickdraw: action.quickdraw };
if (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 defaultTreeZoom = "even";
export const defaultDateRange = 6;
export const date_select = true;
export const file_prefix = "Zika_";
Expand Down

0 comments on commit e4b02c5

Please sign in to comment.