Skip to content

Commit

Permalink
Add toggle to focus on selected
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 them to take up 80% of the vertical span of the panel.
  • Loading branch information
trvrb authored and victorlin committed Oct 21, 2024
1 parent cb65841 commit 8a65633
Show file tree
Hide file tree
Showing 13 changed files with 158 additions and 17 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_FOCUS = "TOGGLE_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} />
<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 { FaInfoCircle } from "react-icons/fa";
import { Dispatch } from "@reduxjs/toolkit";
import Toggle from "./toggle";
import { SidebarIconContainer, StyledTooltip } from "./styles";
import { TOGGLE_FOCUS } from "../../actions/types";
import { RootState } from "../../store";


function ToggleFocus({ tooltip, focus, layout, dispatch, mobileDisplay }: {
tooltip: React.ReactElement;
focus: boolean;
layout: "rect" | "radial" | "unrooted" | "clock" | "scatter";
dispatch: Dispatch;
mobileDisplay: boolean;
}) {
// Focus functionality is only available to layouts that have the concept of a unitless y-axis
const validLayouts = new Set(["rect", "radial"]);
if (!validLayouts.has(layout)) return <></>;

const label = (
<div style={{ display: "flex", alignItems: "center" }}>
<span style={{ marginRight: "5px" }}>Focus on Selected</span>
{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
isExperimental={true}
on={focus}
callback={() => dispatch({ type: TOGGLE_FOCUS })}
label={label}
style={{ paddingBottom: "10px" }}
/>
);
}

export default connect((state: RootState) => ({
focus: state.controls.focus,
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 @@ -17,6 +17,7 @@ const Tree = connect((state: RootState) => ({
temporalConfidence: state.controls.temporalConfidence,
distanceMeasure: state.controls.distanceMeasure,
explodeAttr: state.controls.explodeAttr,
focus: state.controls.focus,
colorScale: state.controls.colorScale,
colorings: state.metadata.colorings,
genomeMap: state.entropy.genomeMap,
Expand Down
7 changes: 5 additions & 2 deletions src/components/tree/phyloTree/change.js
Original file line number Diff line number Diff line change
Expand Up @@ -270,6 +270,7 @@ export const change = function change({
tipRadii = undefined,
branchThickness = undefined,
/* other data */
focus = undefined,
scatterVariables = undefined
}) {
// console.log("\n** phylotree.change() (time since last run:", Date.now() - this.timeLastRenderRequested, "ms) **\n\n");
Expand Down Expand Up @@ -323,7 +324,7 @@ export const change = function change({
}

if (changeNodeOrder) {
setDisplayOrder(this.nodes);
setDisplayOrder({nodes: this.nodes, focus});
this.setDistance();
}

Expand Down Expand Up @@ -359,7 +360,9 @@ 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) */
/* focus */
if (updateLayout) setDisplayOrder({nodes: this.nodes, focus});
/* layout (must run after distance and focus) */
if (newDistance || newLayout || updateLayout || changeNodeOrder) {
this.setLayout(newLayout || this.layout, scatterVariables);
}
Expand Down
54 changes: 46 additions & 8 deletions src/components/tree/phyloTree/helpers.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
/* eslint-disable no-param-reassign */
import { max } from "d3-array";
import {getTraitFromNode, getDivFromNode, getBranchMutations} from "../../../util/treeMiscHelpers";
import { NODE_VISIBLE } from "../../../util/globals";

/** get a string to be used as the DOM element ID
* Note that this cannot have any "special" characters
Expand Down Expand Up @@ -33,18 +34,22 @@ export const applyToChildren = (phyloNode, func) => {
* of nodes in a rectangular tree.
* If `yCounter` is undefined then we wish to hide the node and all descendants of it
* @param {PhyloNode} node
* @param {function} incrementer
* @param {int|undefined} yCounter
* @sideeffect modifies node.displayOrder and node.displayOrderRange
* @returns {int|undefined} current yCounter after assignment to the tree originating from `node`
*/
export const setDisplayOrderRecursively = (node, yCounter) => {
export const setDisplayOrderRecursively = (node, incrementer, yCounter) => {
const children = node.n.children; // (redux) tree node
if (children && children.length) {
for (let i = children.length - 1; i >= 0; i--) {
yCounter = setDisplayOrderRecursively(children[i].shell, yCounter);
yCounter = setDisplayOrderRecursively(children[i].shell, incrementer, yCounter);
}
} else {
node.displayOrder = (node.n.fullTipCount===0 || yCounter===undefined) ? yCounter : ++yCounter;
if (node.n.fullTipCount !== 0 && yCounter !== undefined) {
yCounter += incrementer(node);
}
node.displayOrder = yCounter;
node.displayOrderRange = [yCounter, yCounter];
return yCounter;
}
Expand Down Expand Up @@ -77,20 +82,53 @@ function _getSpaceBetweenSubtrees(numSubtrees, numTips) {
* PhyloTree can subsequently use this information. Accessed by prototypes
* rectangularLayout, radialLayout, createChildrenAndParents
* side effects: <phyloNode>.displayOrder (i.e. in the redux node) and <phyloNode>.displayOrderRange
* @param {Array<PhyloNode>} nodes
* @param {Object} props
* @param {Array<PhyloNode>} props.nodes
* @param {boolean} props.focus
* @returns {undefined}
*/
export const setDisplayOrder = (nodes) => {
export const setDisplayOrder = ({nodes, focus}) => {
const numSubtrees = nodes[0].n.children.filter((n) => n.fullTipCount!==0).length;
const numTips = nodes[0].n.fullTipCount;
const numTips = focus ? nodes[0].n.tipCount : nodes[0].n.fullTipCount;
const spaceBetweenSubtrees = _getSpaceBetweenSubtrees(numSubtrees, numTips);

// No focus: 1 unit per node
let incrementer = (_node) => 1;

if (focus) {
const nVisible = nodes[0].n.tipCount;
const nTotal = nodes[0].n.fullTipCount;

let yProportionFocused = 0.8;
// Adjust for a small number of visible tips (n<4)
yProportionFocused = Math.min(yProportionFocused, nVisible / 5);
// Adjust for a large number of visible tips (>80% of all tips)
yProportionFocused = Math.max(yProportionFocused, nVisible / nTotal);

const yPerFocused = (yProportionFocused * nTotal) / nVisible;
const yPerUnfocused = ((1 - yProportionFocused) * nTotal) / (nTotal - nVisible);

incrementer = (() => {
let previousWasVisible = false;
return (node) => {
// Focus if the current node is visible or if the previous node was visible (for symmetric padding)
const y = (node.visibility === NODE_VISIBLE || previousWasVisible) ? yPerFocused : yPerUnfocused;

// Update for the next node
previousWasVisible = node.visibility === NODE_VISIBLE;

return y;
}
})();
}

let yCounter = 0;
/* iterate through each subtree, and add padding between each */
for (const subtree of nodes[0].n.children) {
if (subtree.fullTipCount===0) { // don't use screen space for this subtree
setDisplayOrderRecursively(nodes[subtree.arrayIdx], undefined);
setDisplayOrderRecursively(nodes[subtree.arrayIdx], incrementer, undefined);
} else {
yCounter = setDisplayOrderRecursively(nodes[subtree.arrayIdx], yCounter);
yCounter = setDisplayOrderRecursively(nodes[subtree.arrayIdx], incrementer, yCounter);
yCounter+=spaceBetweenSubtrees;
}
}
Expand Down
5 changes: 3 additions & 2 deletions 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} focus -- 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, focus, 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 @@ -40,7 +41,7 @@ export const render = function render(svg, layout, distance, parameters, callbac
});

/* set x, y values & scale them to the screen */
setDisplayOrder(this.nodes);
setDisplayOrder({nodes: this.nodes, focus});
this.setDistance(distance);
this.setLayout(layout, scatterVariables);
this.mapToScreen();
Expand Down
25 changes: 23 additions & 2 deletions src/components/tree/reactD3Interface/change.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,14 @@ 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 dateRangeChange = oldProps.dateMinNumeric !== newProps.dateMinNumeric ||
oldProps.dateMaxNumeric !== newProps.dateMaxNumeric;

const filterChange = oldProps.filters !== newProps.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 @@ -47,6 +55,20 @@ export const changePhyloTreeViaPropsComparison = (mainTree, phylotree, oldProps,
/* explode! */
if (oldProps.explodeAttr !== newProps.explodeAttr) {
args.changeNodeOrder = true;
args.focus = newProps.focus;
}

/* enable/disable focus */
if (oldProps.focus !== newProps.focus) {
args.focus = newProps.focus;
args.updateLayout = true;
}
/* re-focus on changes */
else if (oldProps.focus === true &&
newProps.focus === true &&
(zoomChange || dateRangeChange || filterChange)) {
args.focus = true;
args.updateLayout = true;
}

/* change in key used to define branch labels, tip labels */
Expand Down Expand Up @@ -86,8 +108,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.focus,
{ /* parameters (modifies PhyloTree's defaults) */
grid: true,
confidence: props.temporalConfidence.display,
Expand Down
3 changes: 2 additions & 1 deletion src/components/tree/tangle/untangling.js
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@ export const untangleTreeToo = (phylotree1, phylotree2) => {
// const init_corr = calculatePearsonCorrelationCoefficient(phylotree1, phylotree2);
flipChildrenPostorder(phylotree1, phylotree2);
// console.log(`Untangling ${init_corr} -> ${calculatePearsonCorrelationCoefficient(phylotree1, phylotree2)}`);
setDisplayOrder(phylotree2.nodes);
// TODO: check the value of focus
setDisplayOrder({nodes: phylotree2.nodes, focus: false});
// console.timeEnd("untangle");
};
10 changes: 9 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,
defaultFocus,
controlsHiddenWidth,
strainSymbol,
twoColumnBreakpoint } from "../util/globals";
Expand All @@ -17,6 +18,7 @@ type Layout = "rect" | "radial" | "unrooted" | "clock" | "scatter"
interface Defaults {
distanceMeasure: string
layout: Layout
focus: boolean
geoResolution: string
filters: Record<string, any>
filtersInFooter: string[]
Expand All @@ -34,6 +36,7 @@ export interface BasicControlsState {
panelsToDisplay: string[]
showTreeToo: boolean
canTogglePanelLayout: boolean
focus: boolean

// This allows arbitrary prop names while TypeScript adoption is incomplete.
// TODO: add all other props explicitly and remove this.
Expand All @@ -60,6 +63,7 @@ export const getDefaultControlsState = () => {
const defaults: Defaults = {
distanceMeasure: defaultDistanceMeasure,
layout: defaultLayout,
focus: defaultFocus,
geoResolution: defaultGeoResolution,
filters: {},
filtersInFooter: [],
Expand Down Expand Up @@ -87,6 +91,7 @@ export const getDefaultControlsState = () => {
layout: defaults.layout,
scatterVariables: {},
distanceMeasure: defaults.distanceMeasure,
focus: defaults.focus,
dateMin,
dateMinNumeric,
dateMax,
Expand Down Expand Up @@ -209,7 +214,10 @@ const Controls = (state: ControlsState = getDefaultControlsState(), action): Con
}
return Object.assign({}, state, updatesToState);
}
case types.CHANGE_DATES_VISIBILITY_THICKNESS: {
case types.TOGGLE_FOCUS: {
return {...state, focus: !state.focus}
}
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 defaultFocus = false;
export const defaultDateRange = 6;
export const date_select = true;
export const file_prefix = "Zika_";
Expand Down

0 comments on commit 8a65633

Please sign in to comment.