From 474f301cc0fbfea635ae2a0777931fb08baa8582 Mon Sep 17 00:00:00 2001 From: James Hadfield Date: Tue, 30 Mar 2021 19:00:15 +1300 Subject: [PATCH] Zoom to selected is stored as a URL query When clicking "Zoom to selected" we now add a URL query `treeZoom=selected`, which is cleared upon the next zoom action. This allows us to load a page to a zoomed in view of the tree. This behavior is similar to when zooming to a branch which a clade label, which results in a query such as `label=clade:A1`. In the situation where both are applicable, the clade label takes priority. Note that due to the current implementation of zooming in Auspice, "zoom to selected" will zoom to different subtrees. As an example, load each of the following datasets and click "zoom to selected": `/flu/seasonal/h3n2/ha/3y?f_clade_membership=A1b/137F,A1b/197R`, `/flu/seasonal/h3n2/ha/3y?f_clade_membership=A1b/137F,A1b/197R&label=clade:A1b/131K`. The resulting subtrees will be different for each case, however both result in a URL query `treeZoom=selected`. This is a shortcoming of the implementation used in this PR, however one that is not easily solved. --- narratives/test_simultaneous-tree-updates.md | 4 ++++ src/actions/recomputeReduxState.js | 24 +++++++++++++------- src/actions/tree.js | 9 ++++++++ src/middleware/changeURL.js | 12 +++++++++- src/util/treeVisibilityHelpers.js | 2 +- 5 files changed, 41 insertions(+), 10 deletions(-) diff --git a/narratives/test_simultaneous-tree-updates.md b/narratives/test_simultaneous-tree-updates.md index 5380f08a5..0ff00a892 100644 --- a/narratives/test_simultaneous-tree-updates.md +++ b/narratives/test_simultaneous-tree-updates.md @@ -19,6 +19,10 @@ Check that both the branches and tips update. # [P3: Zoom into clade A1b _and_ change color](http://localhost:4000/flu/seasonal/h3n2/ha/3y?d=tree&label=clade:A1b) Check that the coloring of the branches and tips update as we zoom in. +# [P3b: Zoom in slightly more via "zoom to selected"](http://localhost:4000/flu/seasonal/h3n2/ha/3y?d=tree&f_clade_membership=A1b/186D,A1b/197R,A1b/94N&treeZoom=selected) + +We're now recreating the functionality of filtering to clades A1b/186D, A1b/197R and A1b/94N then clicking "zoom to selected" + # [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:3c2.A&m=div) * Zoomed out to near the root (clade 3c2.A) * Changed the horizontal scale to divergence diff --git a/src/actions/recomputeReduxState.js b/src/actions/recomputeReduxState.js index 734646a1e..96f7c54d0 100644 --- a/src/actions/recomputeReduxState.js +++ b/src/actions/recomputeReduxState.js @@ -3,7 +3,7 @@ import { cloneDeep } from 'lodash'; import { numericToCalendar, calendarToNumeric } from "../util/dateHelpers"; import { reallySmallNumber, twoColumnBreakpoint, defaultColorBy, defaultGeoResolution, defaultDateRange, nucleotide_gene, strainSymbol, genotypeSymbol } from "../util/globals"; import { calcBrowserDimensionsInitialState } from "../reducers/browserDimensions"; -import { getIdxMatchingLabel, calculateVisiblityAndBranchThickness } from "../util/treeVisibilityHelpers"; +import { getIdxMatchingLabel, calculateVisiblityAndBranchThickness, getFilteredAndIdxOfFilteredRoot } from "../util/treeVisibilityHelpers"; import { constructVisibleTipLookupBetweenTrees } from "../util/treeTangleHelpers"; import { getDefaultControlsState, shouldDisplayTemporalConfidence } from "../reducers/controls"; import { countTraitsAcrossTree, calcTotalTipsInTree } from "../util/treeCountingHelpers"; @@ -587,15 +587,23 @@ const checkAndCorrectErrorsInState = (state, metadata, query, tree, viewingNarra return state; }; -const modifyTreeStateVisAndBranchThickness = (oldState, zoomSelected, controlsState, dispatch) => { +const modifyTreeStateVisAndBranchThickness = (oldState, query, controlsState, dispatch) => { /* calculate new branch thicknesses & visibility */ let newIdxRoot = oldState.idxOfInViewRootNode; - if (zoomSelected) { + + if (typeof query.label==="string") { // Check and fix old format 'clade=B' - in this case selectionClade is just 'B' - const [labelName, labelValue] = zoomSelected.split(":"); + const [labelName, labelValue] = query.label.split(":"); const cladeSelectedIdx = getIdxMatchingLabel(oldState.nodes, labelName, labelValue, dispatch); - oldState.selectedClade = zoomSelected; + oldState.selectedClade = query.label; newIdxRoot = applyInViewNodesToTree(cladeSelectedIdx, oldState); + delete query.treeZoom; + } else if (query.treeZoom==="selected") { + // zoom to selected requires filters to be applied to tree to calculate the appropriate root + // Note that these are re-calculated by `calculateVisiblityAndBranchThickness` + const {idxOfFilteredRoot} = getFilteredAndIdxOfFilteredRoot(oldState, controlsState, oldState.nodes.map(() => true)); + newIdxRoot = applyInViewNodesToTree(idxOfFilteredRoot, oldState); + if (!idxOfFilteredRoot) delete query.treeZoom; } else { oldState.selectedClade = undefined; newIdxRoot = applyInViewNodesToTree(0, oldState); @@ -831,12 +839,12 @@ export const createStateFromQueryOrJSONs = ({ } /* if query.label is undefined then we intend to zoom to the root */ - tree = modifyTreeStateVisAndBranchThickness(tree, query.label, controls, dispatch); + tree = modifyTreeStateVisAndBranchThickness(tree, query, controls, dispatch); if (treeToo && treeToo.loaded) { treeToo.nodeColorsVersion = tree.nodeColorsVersion; treeToo.nodeColors = calcNodeColor(treeToo, controls.colorScale); - treeToo = modifyTreeStateVisAndBranchThickness(treeToo, undefined, controls, dispatch); + treeToo = modifyTreeStateVisAndBranchThickness(treeToo, {}, controls, dispatch); controls = modifyControlsViaTreeToo(controls, treeToo.name); treeToo.tangleTipLookup = constructVisibleTipLookupBetweenTrees(tree.nodes, treeToo.nodes, tree.visibility, treeToo.visibility); } @@ -912,7 +920,7 @@ export const createTreeTooState = ({ treeToo.debug = "RIGHT"; controls = modifyControlsStateViaTree(controls, tree, treeToo, oldState.metadata.colorings); controls = modifyControlsViaTreeToo(controls, secondTreeUrl); - treeToo = modifyTreeStateVisAndBranchThickness(treeToo, undefined, controls, dispatch); + treeToo = modifyTreeStateVisAndBranchThickness(treeToo, {}, controls, dispatch); /* calculate colours if loading from JSONs or if the query demands change */ const colorScale = calcColorScale(controls.colorBy, controls, tree, treeToo, oldState.metadata); diff --git a/src/actions/tree.js b/src/actions/tree.js index 6518652de..32d0c0c0f 100644 --- a/src/actions/tree.js +++ b/src/actions/tree.js @@ -111,6 +111,15 @@ export const updateVisibleTipsAndBranchThicknesses = ( visibilityToo: dispatchObj.visibilityToo }); + /* We set a flag for the URL middleware here */ + if (root[0]!== undefined) { + if (tree.idxOfFilteredRoot===root[0]) { + dispatchObj.zoomToSelectedQuery = true; + } else { + dispatchObj.zoomToSelectedQuery = false; + } + } + /* D I S P A T C H */ dispatch(dispatchObj); updateEntropyVisibility(dispatch, getState); diff --git a/src/middleware/changeURL.js b/src/middleware/changeURL.js index 53799a603..2d50d4770 100644 --- a/src/middleware/changeURL.js +++ b/src/middleware/changeURL.js @@ -157,7 +157,17 @@ export const changeURLMiddleware = (store) => (next) => (action) => { } case types.UPDATE_VISIBILITY_AND_BRANCH_THICKNESS: { // query.s = action.selectedStrain ? action.selectedStrain : undefined; - query.label = action.cladeName ? action.cladeName : undefined; + if (action.cladeName) { + query.label = action.cladeName; + query.treeZoom = undefined; // we preferentially restore state from clade (branch) label + } else { + query.label = undefined; + if (action.zoomToSelectedQuery===true) { + query.treeZoom="selected"; // setting a string variable allows for future expansion + } else if (action.zoomToSelectedQuery===false) { + query.treeZoom=undefined; + } + } break; } case types.MAP_ANIMATION_PLAY_PAUSE_BUTTON: diff --git a/src/util/treeVisibilityHelpers.js b/src/util/treeVisibilityHelpers.js index d6e0d5570..49f1ec242 100644 --- a/src/util/treeVisibilityHelpers.js +++ b/src/util/treeVisibilityHelpers.js @@ -143,7 +143,7 @@ const getInView = (tree) => { * - controls.filters (redux) is a dict of trait name -> values * - filters (in this code) is a list of filters to apply * e.g. [{trait: "country", values: [...]}, ...] */ -const getFilteredAndIdxOfFilteredRoot = (tree, controls, inView) => { +export const getFilteredAndIdxOfFilteredRoot = (tree, controls, inView) => { if (!tree.nodes) { console.error("getFiltered() ran without tree.nodes"); return null;