Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

tree: Add toggle to focus on selected #1373

Merged
merged 5 commits into from
Oct 21, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 = (
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[commenting on this line to enable a thread, it's not related to the info content]

Animation + focus gets a little crazy. I don't think it's worth resolving this at the moment - it's labelled "experimental" after all - but wanted to note it here.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good note. Focus can be toggled on/off while animation is in progress, so I think fine to leave it as-is.

<>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 <></>;
victorlin marked this conversation as resolved.
Show resolved Hide resolved

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
victorlin marked this conversation as resolved.
Show resolved Hide resolved
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
jameshadfield marked this conversation as resolved.
Show resolved Hide resolved
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