From 5cdc5649d6120048d771b27088beacd4709033cf Mon Sep 17 00:00:00 2001 From: Trevor Bedford Date: Sun, 11 Jul 2021 15:34:58 -0700 Subject: [PATCH] =?UTF-8?q?=F0=9F=9A=A7=20Vertically=20focus=20on=20filter?= =?UTF-8?q?ed=20nodes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- src/actions/tree.js | 1 + src/actions/types.js | 1 + src/components/tree/index.ts | 1 + src/components/tree/phyloTree/change.js | 10 ++-- src/components/tree/phyloTree/layouts.js | 53 +++++++++++++++++++ src/components/tree/phyloTree/phyloTree.js | 1 + src/components/tree/phyloTree/renderers.js | 4 +- .../tree/reactD3Interface/change.js | 9 ++++ .../tree/reactD3Interface/initialRender.js | 1 + src/components/tree/tree.js | 21 ++++++++ src/reducers/controls.ts | 14 ++++- src/util/globals.js | 1 + 12 files changed, 112 insertions(+), 5 deletions(-) diff --git a/src/actions/tree.js b/src/actions/tree.js index a5f7de910..d40b51814 100644 --- a/src/actions/tree.js +++ b/src/actions/tree.js @@ -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 }; }; diff --git a/src/actions/types.js b/src/actions/types.js index 10a71d8db..cf40b7aeb 100644 --- a/src/actions/types.js +++ b/src/actions/types.js @@ -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"; diff --git a/src/components/tree/index.ts b/src/components/tree/index.ts index 3bfb1e7ca..db4fef0bb 100644 --- a/src/components/tree/index.ts +++ b/src/components/tree/index.ts @@ -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, diff --git a/src/components/tree/phyloTree/change.js b/src/components/tree/phyloTree/change.js index a3a0c4277..6613d56af 100644 --- a/src/components/tree/phyloTree/change.js +++ b/src/components/tree/phyloTree/change.js @@ -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, @@ -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'); @@ -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 @@ -377,6 +380,7 @@ export const change = function change({ newDistance || newLayout || changeNodeOrder || + newTreeFocus || updateLayout || zoomIntoClade || svgHasChangedDimensions || diff --git a/src/components/tree/phyloTree/layouts.js b/src/components/tree/phyloTree/layouts.js index 226ae2352..aadd61e93 100644 --- a/src/components/tree/phyloTree/layouts.js +++ b/src/components/tree/phyloTree/layouts.js @@ -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 @@ -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) diff --git a/src/components/tree/phyloTree/phyloTree.js b/src/components/tree/phyloTree/phyloTree.js index 9c1129c02..c91224fbd 100644 --- a/src/components/tree/phyloTree/phyloTree.js +++ b/src/components/tree/phyloTree/phyloTree.js @@ -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; diff --git a/src/components/tree/phyloTree/renderers.js b/src/components/tree/phyloTree/renderers.js index c955cc978..c4d2796ba 100644 --- a/src/components/tree/phyloTree/renderers.js +++ b/src/components/tree/phyloTree/renderers.js @@ -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) @@ -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); @@ -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(); diff --git a/src/components/tree/reactD3Interface/change.js b/src/components/tree/reactD3Interface/change.js index 07bff8b1b..5fdd7e31c 100644 --- a/src/components/tree/reactD3Interface/change.js +++ b/src/components/tree/reactD3Interface/change.js @@ -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"; diff --git a/src/components/tree/reactD3Interface/initialRender.js b/src/components/tree/reactD3Interface/initialRender.js index b85dd9917..87b5b4b95 100644 --- a/src/components/tree/reactD3Interface/initialRender.js +++ b/src/components/tree/reactD3Interface/initialRender.js @@ -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, diff --git a/src/components/tree/tree.js b/src/components/tree/tree.js index f53ff72ce..0281102b0 100644 --- a/src/components/tree/tree.js +++ b/src/components/tree/tree.js @@ -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"; @@ -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] })); @@ -127,6 +129,8 @@ class Tree extends React.Component { const activeResetTreeButton = anyTreeZoomed; + const activeFocusButton = true; + return { treeButtonsDiv: { zIndex: 100, @@ -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", @@ -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] @@ -262,6 +277,12 @@ class Tree extends React.Component { > {t("Zoom to Selected")} +