Skip to content

Commit a9b74fe

Browse files
Merge pull request nextstrain#1321 from nextstrain/bug-narratives-zoom-to-selected
Zoom to selected is stored as a URL query
2 parents 38f5188 + da39a25 commit a9b74fe

6 files changed

Lines changed: 59 additions & 16 deletions

File tree

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
# Changelog
22

3+
* Added a `treeZoom=selected` query to load trees at the same zoom level after the "zoom to selected" button has been pressed, where applicable. See [the view settings docs](https://docs.nextstrain.org/projects/auspice/en/latest/advanced-functionality/view-settings.html) for more details. ([#1321](https://github.com/nextstrain/auspice/pull/1321))
4+
35
## version 2.63.1 - 2025/06/04
46

57

docs/advanced-functionality/view-settings.rst

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,12 @@ These are exported as the (optional) property of the dataset JSON ``meta.display
5656
+---------------------------+-----------------------------------------------------------------------+----------------------------------------------------+
5757
| ``label`` | Labelled branch that tree starts zoomed to | "Sublineage:J" or "clade:2a.3a" |
5858
+---------------------------+-----------------------------------------------------------------------+----------------------------------------------------+
59+
| ``treeZoom`` | Controls how a tree is initially zoomed (see note below) | "treeZoom=selected" |
60+
+---------------------------+-----------------------------------------------------------------------+----------------------------------------------------+
61+
62+
63+
* The `treeZoom=selected` query is only applicable in certain circumstances and acts as if we had applied all other view settings and then clicked the "zoom to selected" button. The behaviour of the "zoom to selected" button can differ depending on the current zoom level of the tree, and the URL query will only be set if the same zoom level is obtaibavle if you click the button from a fully zoomed out state; see [this GitHub thread for more details](https://github.com/nextstrain/auspice/pull/1321#issuecomment-2914923800). Finally, if a `label` query is in the URL then that will be used instead as it has higher specificity.
64+
5965

6066
Certain other properties on the JSON's ``meta`` dictionary also affect the view settings.
6167
See the `JSON schema <https://github.com/nextstrain/augur/blob/master/augur/data/schema-export-v2.json>`__ for more details.

narratives/test_simultaneous-tree-updates.md

Lines changed: 17 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -5,36 +5,43 @@ authors:
55
authorLinks:
66
- https://twitter.com/hamesjadfield
77
affiliations: "Fred Hutch"
8-
dataset: "http://localhost:4000/flu/seasonal/h3n2/ha/3y?d=tree"
8+
dataset: "http://localhost:4000/flu/seasonal/h3n2/ha/3y?d=tree&c=num_date"
99
date: June 2020
1010
abstract: "A narrative to explore simultaneous changes to tree state. The aim is both to reveal what's possible, and to prevent future regressions via automated testing."
1111
---
1212

1313

14-
# [P1: Change coloring to sampling date](http://localhost:4000/flu/seasonal/h3n2/ha/3y?d=tree&c=num_date&branchLabel=aa)
14+
# [P1: Change coloring to clade](http://localhost:4000/flu/seasonal/h3n2/ha/3y?d=tree&branchLabel=aa)
1515

16-
Check that both the branches and tips update.
16+
Check that both the branches and tips update. (The clade coloring is the default, so this slide removes the previous `c=num_date` query)
1717

1818
The clade labels should now show selected amino acids (with the selecting being done by auspice).
1919

20-
# [P2: Zoom into clade 3C.2a1b.2a.2](http://localhost:4000/flu/seasonal/h3n2/ha/3y?d=tree&c=num_date&label=clade:3C.2a1b.2a.2&branchLabel=aa&showBranchLabels=all)
20+
# [P2: Zoom in via filtering & "zoom-to-selected"](http://localhost:4000/flu/seasonal/h3n2/ha/3y?d=tree&branchLabel=aa&f_clade_membership=3C.2a1b.2a.1,3C.2a1b.2a.2&treeZoom=selected)
2121

22-
We simultaneously zoom into clade 3C.2a1b.2a.2 (top right section of tree) and show _all_ AA mutations as branch labels.
23-
If you go to the previous slide the aa mutation labels should revert to only showing a subset.
22+
We add two clade filters: "3C.2a1b.2a.1" & "3C.2a1b.2a.2" and the URL query `treeZoom=selected` which zooms into their CA
23+
(which does not have its own branch label)
24+
25+
26+
# [P3: Zoom in further](http://localhost:4000/flu/seasonal/h3n2/ha/3y?d=tree&c=num_date&label=clade:3C.2a1b.2a.2&branchLabel=aa&showBranchLabels=all)
27+
28+
We simultaneously zoom into clade 3C.2a1b.2a.2 (top right section) and show _all_ AA mutations as branch labels.
29+
If you go to the previous slide(s) the aa mutation labels should revert to only showing a subset.
30+
31+
# [P4: Zoom out to clade 3C.2a1b.2 _and_ change color](http://localhost:4000/flu/seasonal/h3n2/ha/3y?d=tree&label=clade:3C.2a1b.2)
2432

25-
# [P3: Zoom out to clade 3C.2a1b.2 _and_ change color](http://localhost:4000/flu/seasonal/h3n2/ha/3y?d=tree&label=clade:3C.2a1b.2)
2633
Check that the coloring of the branches and tips update as we zoom out (slightly).
2734

2835
The coloring should be back to the default (clade), and so should the branch labels (clade also).
2936

30-
# [P4: Lots of simultaneous changes](http://localhost:4000/flu/seasonal/h3n2/ha/3y?c=lbi&d=tree&dmin=2017-01-01&f_region=North%20America&label=clade:3C.2a&m=div)
37+
# [P5: Lots of simultaneous changes](http://localhost:4000/flu/seasonal/h3n2/ha/3y?c=lbi&d=tree&dmin=2017-01-01&f_region=North%20America&label=clade:3C.2a&m=div)
3138
* Zoomed out to near the root (clade 3C.2a)
3239
* Changed the horizontal scale to divergence
3340
* Changed the color-by to LBI (and legend should be open)
34-
* Filtered to North American Samples (i.e. the majority of tips are not visible).
41+
* Kept the North American region filter (i.e. the majority of tips are not visible).
3542
* Time slice excludes pre-2017 samples (note that we don't have the grey overlay here since the axis is divergence)
3643

37-
# [P5: Layout change (and others)](http://localhost:4000/flu/seasonal/h3n2/ha/3y?branchLabel=none&d=tree&l=radial&m=div)
44+
# [P6: Layout change (and others)](http://localhost:4000/flu/seasonal/h3n2/ha/3y?branchLabel=none&d=tree&l=radial&m=div)
3845
Underneath, this uses a different set of d3 calls where instead of transitioning all elements (branches, tips etc), which is too slow, we do a "in-parts" update: we hide the branches, transition the tips, then redraw the branches in their new position.
3946
This slide should
4047
* switch to radial view

src/actions/recomputeReduxState.js

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { cloneDeep, isEqualWith } from 'lodash';
33
import { numericToCalendar, calendarToNumeric } from "../util/dateHelpers";
44
import { reallySmallNumber, twoColumnBreakpoint, defaultColorBy, defaultGeoResolution, defaultDateRange, nucleotide_gene, strainSymbol, genotypeSymbol } from "../util/globals";
55
import { calcBrowserDimensionsInitialState } from "../reducers/browserDimensions";
6-
import { getIdxMatchingLabel, calculateVisiblityAndBranchThickness } from "../util/treeVisibilityHelpers";
6+
import { getIdxMatchingLabel, calculateVisiblityAndBranchThickness, getFilteredAndIdxOfFilteredRoot } from "../util/treeVisibilityHelpers";
77
import { constructVisibleTipLookupBetweenTrees } from "../util/treeTangleHelpers";
88
import { getDefaultControlsState, shouldDisplayTemporalConfidence } from "../reducers/controls";
99
import { getDefaultFrequenciesState } from "../reducers/frequencies";
@@ -656,6 +656,11 @@ const checkAndCorrectErrorsInState = (state, metadata, genomeMap, query, tree, v
656656
}
657657
}
658658

659+
/* Currently the (rather unique) treeZoom query can only have the value 'selected' */
660+
if (query.treeZoom && query.treeZoom!=='selected') {
661+
delete query.treeZoom;
662+
}
663+
659664
/* temporalConfidence */
660665
if (
661666
shouldDisplayTemporalConfidence(state.temporalConfidence.exists, state.distanceMeasure, state.layout)
@@ -756,25 +761,29 @@ const checkAndCorrectErrorsInState = (state, metadata, genomeMap, query, tree, v
756761
const modifyTreeStateVisAndBranchThickness = (oldState, displayDefaults, query, controlsState, dispatch) => {
757762
/* calculate the index of the (in-view) root note, which depends on any selected zoom */
758763
let newIdxRoot = oldState.idxOfInViewRootNode;
759-
let cladeSelectedIdx = 0; // default to tree root
760764
if (query.label) {
761765
const [labelName, labelValue] = parseLabel(query.label)
762766
const idx = getIdxMatchingLabel(oldState.nodes, labelName, labelValue, dispatch);
763767
if (idx === null) {
764768
delete query.label;
765769
} else {
766-
cladeSelectedIdx = idx;
770+
newIdxRoot = applyInViewNodesToTree(idx, oldState);
767771
}
772+
} else if (query.treeZoom==="selected") {
773+
// zoom to selected requires filters to be applied to tree to calculate the appropriate root
774+
// Note that these are re-calculated by `calculateVisiblityAndBranchThickness`
775+
const {idxOfFilteredRoot} = getFilteredAndIdxOfFilteredRoot(oldState, controlsState, oldState.nodes.map(() => true));
776+
newIdxRoot = applyInViewNodesToTree(idxOfFilteredRoot, oldState);
777+
if (!idxOfFilteredRoot) delete query.treeZoom;
768778
} else if (displayDefaults.label) {
769779
const [labelName, labelValue] = parseLabel(displayDefaults.label)
770780
const idx = getIdxMatchingLabel(oldState.nodes, labelName, labelValue, dispatch);
771781
if (idx === null) {
772782
delete displayDefaults.label;
773783
} else {
774-
cladeSelectedIdx = idx;
784+
newIdxRoot = applyInViewNodesToTree(idx, oldState);
775785
}
776786
}
777-
newIdxRoot = applyInViewNodesToTree(cladeSelectedIdx, oldState);
778787

779788
/* calculate new branch thicknesses & visibility, as this depends on the root note */
780789
const visAndThicknessData = calculateVisiblityAndBranchThickness(

src/middleware/changeURL.js

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { urlQueryLabel } from "../util/treeVisibilityHelpers";
55
import { shouldDisplayTemporalConfidence } from "../reducers/controls";
66
import { genotypeSymbol, nucleotide_gene, strainSymbol } from "../util/globals";
77
import { encodeGenotypeFilters, decodeColorByGenotype, isColorByGenotype } from "../util/getGenotype";
8+
import { getFilteredAndIdxOfFilteredRoot } from "../util/treeVisibilityHelpers";
89
import { removeInvalidMeasurementsFilterQuery } from "../actions/measurements";
910

1011
export const strainSymbolUrlString = "__strain__";
@@ -197,6 +198,24 @@ export const changeURLMiddleware = (store) => (next) => (action) => {
197198
case types.UPDATE_VISIBILITY_AND_BRANCH_THICKNESS: {
198199
// NOTE: `idxOfInViewRootNode` refers to the left tree only (if 2 trees displayed)
199200
query.label = urlQueryLabel(state.tree.nodes[action.idxOfInViewRootNode], state.tree.availableBranchLabels)
201+
/* The "zoom to selected" button will zoom to the `idxOfFilteredRoot` (i.e. its behaviour depends on the selected
202+
filters). If the LHS tree is zoomed to this node, either by chance or by having clicked on the button, then set
203+
this as query. Any other zoom/filtering action will remove it */
204+
if (
205+
/* if there's a label then don't use zoom-to-selected as the label is more specific */
206+
query.label===undefined &&
207+
/* The "zoom to selected" button will zoom us to the idxOfFilteredRoot, so check we're
208+
there - either because we pressed the button or because we clicked that branch */
209+
action.idxOfFilteredRoot===action.idxOfInViewRootNode &&
210+
/* Ensure the filtered tips (across the entire tree) are _all_ downstream of the current zoomed node
211+
i.e. the CA of filtered tips when the entire tree's in view is the same as the current zoom node. See
212+
<https://github.com/nextstrain/auspice/pull/1321#issuecomment-2914923800> for more context. */
213+
action.idxOfFilteredRoot===getFilteredAndIdxOfFilteredRoot(state.tree, state.controls, state.tree.nodes.map(() => true)).idxOfFilteredRoot
214+
) {
215+
query.treeZoom="selected"; // using a string value allows for future expansion of functionality
216+
} else {
217+
query.treeZoom = undefined
218+
}
200219
break;
201220
}
202221
case types.MAP_ANIMATION_PLAY_PAUSE_BUTTON:

src/util/treeVisibilityHelpers.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -172,7 +172,7 @@ const getInView = (tree) => {
172172
* - controls.filters (redux) is a dict of trait name -> values
173173
* - filters (in this code) is a list of filters to apply
174174
* e.g. [{trait: "country", values: [...]}, ...] */
175-
const getFilteredAndIdxOfFilteredRoot = (tree, controls, inView) => {
175+
export const getFilteredAndIdxOfFilteredRoot = (tree, controls, inView) => {
176176
if (!tree.nodes) {
177177
console.error("getFiltered() ran without tree.nodes");
178178
return null;

0 commit comments

Comments
 (0)