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

Open
wants to merge 5 commits into
base: victorlin/prep-dynamic-yvalues
Choose a base branch
from
Open
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 .github/workflows/preview_on_downstream_repo.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,11 @@ jobs:
repository: ${{ inputs.repository }}
token: ${{ secrets.GH_TOKEN_NEXTSTRAIN_BOT_REPO }}

- name: auspice build --includeTiming for nextstrain.org
run: |
git apply <(wget -q -O - https://github.com/nextstrain/nextstrain.org/commit/0671e90b0cdfa55d0ee5b01d0b78bc933cedb121.patch)
git add build.sh

- name: Install Auspice from PRs HEAD commit
shell: bash
working-directory: ${{ inputs.directory }}
Expand Down
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/tree.js
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,7 @@ export const updateVisibleTipsAndBranchThicknesses = (
);
const dispatchObj = {
type: types.UPDATE_VISIBILITY_AND_BRANCH_THICKNESS,
filters: controls.filters,
visibility: data.visibility,
visibilityVersion: data.visibilityVersion,
branchThickness: data.branchThickness,
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.
</>
);
55 changes: 55 additions & 0 deletions src/components/controls/toggle-focus.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
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_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>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={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 @@ -16,6 +16,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
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,
newFocus = 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 || newFocus || 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) {
/* focus */
if (newFocus || updateLayout) setDisplayOrder(this.nodes, newFocus);
/* layout (must run after distance and focus) */
if (newDistance || newLayout || newFocus || 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 ||
newFocus ||
updateLayout ||
zoomIntoClade ||
svgHasChangedDimensions ||
Expand Down
68 changes: 61 additions & 7 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,19 +35,20 @@ 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} getDisplayOrder
* @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, getDisplayOrder, 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, getDisplayOrder, yCounter);
}
} else {
node.displayOrder = (node.n.fullTipCount===0 || yCounter===undefined) ? yCounter : ++yCounter;
node.displayOrderRange = [yCounter, yCounter];
node.displayOrder = (node.n.fullTipCount===0 || yCounter===undefined) ? yCounter : yCounter + getDisplayOrder(node);
node.displayOrderRange = [node.displayOrder, node.displayOrder];
return yCounter;
}
/* if here, then all children have displayOrders, but we don't. */
Expand Down Expand Up @@ -78,27 +81,78 @@ function _getSpaceBetweenSubtrees(numSubtrees, numTips) {
* rectangularLayout, radialLayout, createChildrenAndParents
* side effects: <phyloNode>.displayOrder (i.e. in the redux node) and <phyloNode>.displayOrderRange
* @param {Array<PhyloNode>} nodes
* @param {boolean} focus
* @returns {undefined}
*/
export const setDisplayOrder = (nodes) => {
export const setDisplayOrder = (nodes, focus) => {
timerStart("setDisplayOrder");

const getDisplayOrder = getDisplayOrderCallback(nodes, focus);
const numSubtrees = nodes[0].n.children.filter((n) => n.fullTipCount!==0).length;
const numTips = nodes[0].n.fullTipCount;
const spaceBetweenSubtrees = _getSpaceBetweenSubtrees(numSubtrees, numTips);
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], getDisplayOrder, undefined);
} else {
yCounter = setDisplayOrderRecursively(nodes[subtree.arrayIdx], yCounter);
yCounter = setDisplayOrderRecursively(nodes[subtree.arrayIdx], getDisplayOrder, 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");
};

/**
* @param {Array<PhyloNode>} nodes
* @param {boolean} focus
* @returns fn to return a display order (y position) for a node
*/
function getDisplayOrderCallback(nodes, focus) {
/**
* Start at 0 and increase with each node.
* Note that this value is shared across invocations of the callback.
*/
let displayOrder = 0;

/**
* Keep track of whether the previous node was selected
*/
let previousWasVisible;

if (focus) {
const numVisible = nodes.filter((d) => !d.hasChildren && d.visibility === NODE_VISIBLE).length;
const yProportionFocused = Math.max(0.8, numVisible / nodes.length);
const yProportionUnfocused = 1 - yProportionFocused;
const yPerFocused = (yProportionFocused * nodes.length) / numVisible;
const yPerUnfocused = (yProportionUnfocused * nodes.length) / (nodes.length - numVisible);

return (node) => {
// Focus if the current node is visible or if the previous node was visible (for symmetric padding)
if (node.visibility === NODE_VISIBLE || previousWasVisible) {
displayOrder += yPerFocused;
} else {
displayOrder += yPerUnfocused;
}

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

return displayOrder;
};
} else {
// No focus: 1 unit per node
return (_node) => {
displayOrder += 1;
return displayOrder;
};
}
}

export const formatDivergence = (divergence) => {
return divergence > 1 ?
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(this.nodes, focus);
this.setDistance(distance);
this.setLayout(layout, scatterVariables);
this.mapToScreen();
Expand Down
22 changes: 20 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 = 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 +57,17 @@ export const changePhyloTreeViaPropsComparison = (mainTree, phylotree, oldProps,
args.changeNodeOrder = true;
}

/* enable/disable focus */
if (oldProps.focus !== newProps.focus) {
args.newFocus = newProps.focus;
args.updateLayout = true;
}
/* re-focus on changes */
else if (oldProps.focus && (zoomChange || dateRangeChange || filterChange)) {
args.newFocus = 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 +105,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
Loading