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 toggle in the sidebar 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 9, 2024
1 parent e25699f commit 9ed2880
Show file tree
Hide file tree
Showing 13 changed files with 165 additions and 8 deletions.
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
5 changes: 4 additions & 1 deletion src/components/controls/controls.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,14 @@ import TransmissionLines from './transmission-lines';
import NormalizeFrequencies from "./frequency-normalization";
import AnimationOptions from "./animation-options";
import { PanelSection } from "./panelSection";
import ToggleFocus from "./toggle-focus";
import ToggleTangle from "./toggle-tangle";
import Language from "./language";
import { ControlsContainer } from "./styles";
import FilterData, {FilterInfo} from "./filter";
import {TreeInfo, MapInfo, AnimationOptionsInfo, PanelLayoutInfo,
ExplodeTreeInfo, EntropyInfo, FrequencyInfo, MeasurementsInfo} from "./miscInfoText";
ExplodeTreeInfo, EntropyInfo, FrequencyInfo, MeasurementsInfo,
ToggleFocusInfo} from "./miscInfoText";
import { ControlHeader } from "./controlHeader";
import MeasurementsOptions from "./measurementsOptions";
import { RootState } from "../../store";
Expand Down Expand Up @@ -64,6 +66,7 @@ function Controls() {
tooltip={TreeInfo}
options={<>
<ChooseLayout />
<ToggleFocus tooltip={ToggleFocusInfo} />

Check failure on line 69 in src/components/controls/controls.tsx

View workflow job for this annotation

GitHub Actions / type-check

Type 'ReactElement<any, any>' is missing the following properties from type 'Element': attributes, classList, className, clientHeight, and 163 more.
<ChooseMetric />
<ChooseBranchLabelling />
<ChooseTipLabel />
Expand Down
8 changes: 8 additions & 0 deletions src/components/controls/miscInfoText.js
Original file line number Diff line number Diff line change
Expand Up @@ -64,3 +64,11 @@ export const ExplodeTreeInfo = (
It works best when the trait doesn&apos;t change value too frequently.
</>
);

export const ToggleFocusInfo = (
<>This functionality is experimental and should be treated with caution!
<br/>When focusing on selected nodes, nodes that do not match the
filter will occupy less vertical space on the tree. Only applicable to
rectangular and radial layouts.
</>
);
54 changes: 54 additions & 0 deletions src/components/controls/toggle-focus.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import React from "react";
import { connect } from "react-redux";
import { ImLab } from "react-icons/im";
import { FaInfoCircle } from "react-icons/fa";
import { Dispatch } from "@reduxjs/toolkit";
import Toggle from "./toggle";
import { SidebarIconContainer, StyledTooltip } from "./styles";
import { TOGGLE_TREE_FOCUS } from "../../actions/types";
import { RootState } from "../../store";


function ToggleFocus({ tooltip, treeFocus, layout, dispatch, mobileDisplay }: {
tooltip: Element;
treeFocus: boolean;
layout: "rect" | "radial" | "unrooted" | "clock" | "scatter";
dispatch: Dispatch;
mobileDisplay: boolean;
}) {
const validLayouts = new Set(["rect", "radial"]);
if (!validLayouts.has(layout)) return <></>;

const label = (
<div style={{ display: "flex", alignItems: "center" }}>
<span>Focus on Selected</span>
<ImLab style={{ margin: "0 5px" }} />
{tooltip && !mobileDisplay && (
<>
<SidebarIconContainer style={{ display: "inline-flex" }} data-tip data-for="toggle-focus">
<FaInfoCircle />
</SidebarIconContainer>
<StyledTooltip place="bottom" type="dark" effect="solid" id="toggle-focus">
{tooltip}
</StyledTooltip>
</>
)}
</div>
);

return (
<Toggle
display
on={treeFocus}
callback={() => dispatch({ type: TOGGLE_TREE_FOCUS })}
label={label}
style={{ paddingBottom: "10px" }}
/>
);
}

export default connect((state: RootState) => ({
treeFocus: state.controls.treeFocus,
layout: state.controls.layout,
mobileDisplay: state.general.mobileDisplay,
}))(ToggleFocus);
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
52 changes: 52 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,57 @@ 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) => {
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
19 changes: 17 additions & 2 deletions src/components/tree/reactD3Interface/change.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,11 @@ export const changePhyloTreeViaPropsComparison = (mainTree, phylotree, oldProps,
const oldTreeRedux = mainTree ? oldProps.tree : oldProps.treeToo;
const newTreeRedux = mainTree ? newProps.tree : newProps.treeToo;

/* zoom to a clade / reset zoom to entire tree */
const zoomChange = oldTreeRedux.idxOfInViewRootNode !== newTreeRedux.idxOfInViewRootNode;

const filterChange = oldTreeRedux.filters !== newTreeRedux.filters;

/* do any properties on the tree object need to be updated?
Note that updating properties itself won't trigger any visual changes */
phylotree.dateRange = [newProps.dateMinNumeric, newProps.dateMaxNumeric];
Expand Down Expand Up @@ -49,6 +54,17 @@ export const changePhyloTreeViaPropsComparison = (mainTree, phylotree, oldProps,
args.changeNodeOrder = true;
}

/* enable/disable focus */
if (oldProps.treeFocus !== newProps.treeFocus) {
args.newTreeFocus = newProps.treeFocus;
args.updateLayout = true;
}
/* re-focus on changes */
else if (oldProps.treeFocus && (zoomChange || filterChange)) {
args.newTreeFocus = true;
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 Expand Up @@ -86,8 +102,7 @@ export const changePhyloTreeViaPropsComparison = (mainTree, phylotree, oldProps,
}


/* zoom to a clade / reset zoom to entire tree */
if (oldTreeRedux.idxOfInViewRootNode !== newTreeRedux.idxOfInViewRootNode) {
if (zoomChange) {
const rootNode = phylotree.nodes[newTreeRedux.idxOfInViewRootNode];
args.zoomIntoClade = rootNode;
newState.selectedNode = {};
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
16 changes: 15 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 @@ -19,6 +20,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 @@ -41,6 +43,7 @@ export interface ControlsState extends BasicControlsState, MeasurementsControlSt
interface Defaults {
distanceMeasure: string
layout: string
treeFocus: boolean
geoResolution: string
filters: Record<string, any>
filtersInFooter: any[]
Expand All @@ -58,6 +61,7 @@ export const getDefaultControlsState = () => {
const defaults: Defaults = {
distanceMeasure: defaultDistanceMeasure,
layout: defaultLayout,
treeFocus: defaultTreeFocus,
geoResolution: defaultGeoResolution,
filters: {},
filtersInFooter: [],
Expand Down Expand Up @@ -85,6 +89,7 @@ export const getDefaultControlsState = () => {
layout: defaults.layout,
scatterVariables: {},
distanceMeasure: defaults.distanceMeasure,
treeFocus: defaults.treeFocus,
dateMin,
dateMinNumeric,
dateMax,
Expand Down Expand Up @@ -207,7 +212,16 @@ 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;
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 9ed2880

Please sign in to comment.