Skip to content

Commit

Permalink
Merge pull request #1373: tree: Add toggle to focus on selected
Browse files Browse the repository at this point in the history
  • Loading branch information
victorlin authored Oct 21, 2024
2 parents 887624a + b7f8bc9 commit c370552
Show file tree
Hide file tree
Showing 15 changed files with 181 additions and 18 deletions.
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
# Changelog

* Added an experimental "Focus on Selected" toggle in the sidebar.
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.
([#1373](https://github.com/nextstrain/auspice/pull/1373))

## version 2.58.0 - 2024/09/12


Expand Down
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);
13 changes: 12 additions & 1 deletion src/components/controls/toggle.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import React from "react";
import { ImLab } from "react-icons/im";
import styled from 'styled-components';
import { SidebarSubtitle } from "./styles";

Expand Down Expand Up @@ -28,6 +29,11 @@ const ToggleSubtitle = styled(SidebarSubtitle)`
width: 200px;
`;

const ExperimentalIcon = styled.span`
color: ${(props) => props.theme.color};
margin-right: 5px;
`

const Slider = styled.div`
& {
position: absolute;
Expand Down Expand Up @@ -73,11 +79,16 @@ const Input = styled.input`
`;


const Toggle = ({display, on, callback, label, style={}}) => {
const Toggle = ({display, isExperimental = false, on, callback, label, style={}}) => {
if (!display) return null;

return (
<ToggleContainer style={style}>
{isExperimental &&
<ExperimentalIcon>
<ImLab />
</ExperimentalIcon>
}
<ToggleBackground>
<Input type="checkbox" checked={on} onChange={callback}/>
<Slider/>
Expand Down
2 changes: 2 additions & 0 deletions src/components/tree/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ const Tree = connect((state: RootState) => ({
selectedNode: state.controls.selectedNode,
dateMinNumeric: state.controls.dateMinNumeric,
dateMaxNumeric: state.controls.dateMaxNumeric,
filters: state.controls.filters,
quickdraw: state.controls.quickdraw,
colorBy: state.controls.colorBy,
colorByConfidence: state.controls.colorByConfidence,
Expand All @@ -16,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
59 changes: 51 additions & 8 deletions src/components/tree/phyloTree/helpers.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
/* eslint-disable no-param-reassign */
import { max } from "d3-array";
import {getTraitFromNode, getDivFromNode, getBranchMutations} from "../../../util/treeMiscHelpers";
import { NODE_VISIBLE } from "../../../util/globals";
import { timerStart, timerEnd } from "../../../util/perf";

/** 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 +35,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,26 +83,63 @@ 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}) => {
timerStart("setDisplayOrder");

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;
}
}
/* note that nodes[0] is a dummy node holding each subtree */
nodes[0].displayOrder = undefined;
nodes[0].displayOrderRange = [undefined, undefined];

timerEnd("setDisplayOrder");
};


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");
};
Loading

0 comments on commit c370552

Please sign in to comment.