diff --git a/src/actions/types.js b/src/actions/types.js
index 84524c336..277a9b42d 100644
--- a/src/actions/types.js
+++ b/src/actions/types.js
@@ -61,3 +61,4 @@ export const TOGGLE_MEASUREMENTS_OVERALL_MEAN = "TOGGLE_MEASUREMENTS_OVERALL_MEA
export const CHANGE_MEASUREMENTS_DISPLAY = "CHANGE_MEASUREMENTS_DISPLAY";
export const APPLY_MEASUREMENTS_FILTER = "APPLY_MEASUREMENTS_FILTER";
export const UPDATE_MEASUREMENTS_ERROR = "UPDATE_MEASUREMENTS_ERROR";
+export const TOGGLE_TEMPORAL_ZOOM_FLAG = "TOGGLE_TEMPORAL_ZOOM_FLAG";
diff --git a/src/components/controls/choose-zoom-mode.js b/src/components/controls/choose-zoom-mode.js
new file mode 100644
index 000000000..ca5f60217
--- /dev/null
+++ b/src/components/controls/choose-zoom-mode.js
@@ -0,0 +1,32 @@
+import React from "react";
+import { connect } from "react-redux";
+import { withTranslation } from "react-i18next";
+
+import Toggle from "./toggle";
+import { controlsWidth } from "../../util/globals";
+import { TOGGLE_TEMPORAL_ZOOM_FLAG } from "../../actions/types";
+
+@connect((state) => {
+ return {
+ treeZoomsTemporally: state.controls.treeZoomsTemporally
+ };
+})
+class ChooseZoomMode extends React.Component {
+ render() {
+ const { t } = this.props;
+ return (
+
+ {
+ this.props.dispatch({ type: TOGGLE_TEMPORAL_ZOOM_FLAG });
+ }}
+ label={t("sidebar:Zoom Temporally")}
+ />
+
+ );
+ }
+}
+
+export default withTranslation()(ChooseZoomMode);
diff --git a/src/components/controls/controls.js b/src/components/controls/controls.js
index 8a0d391ec..0df84d835 100644
--- a/src/components/controls/controls.js
+++ b/src/components/controls/controls.js
@@ -25,6 +25,7 @@ import {TreeOptionsInfo, MapOptionsInfo, AnimationOptionsInfo, PanelOptionsInfo,
ExplodeTreeInfo, FrequencyInfo, MeasurementsOptionsInfo} from "./miscInfoText";
import { AnnotatedHeader } from "./annotatedHeader";
import MeasurementsOptions from "./measurementsOptions";
+import ChooseZoomMode from "./choose-zoom-mode";
function Controls({mapOn, frequenciesOn, measurementsOn, mobileDisplay}) {
const { t } = useTranslation();
@@ -44,6 +45,7 @@ function Controls({mapOn, frequenciesOn, measurementsOn, mobileDisplay}) {
+
diff --git a/src/components/tree/index.js b/src/components/tree/index.js
index bb4f54e73..55d9aa3c6 100644
--- a/src/components/tree/index.js
+++ b/src/components/tree/index.js
@@ -24,7 +24,8 @@ const Tree = connect((state) => ({
canRenderBranchLabels: state.controls.canRenderBranchLabels,
tipLabelKey: state.controls.tipLabelKey,
narrativeMode: state.narrative.display,
- animationPlayPauseButton: state.controls.animationPlayPauseButton
+ animationPlayPauseButton: state.controls.animationPlayPauseButton,
+ treeZoomsTemporally: state.controls.treeZoomsTemporally
}))(UnconnectedTree);
export default Tree;
diff --git a/src/components/tree/phyloTree/change.js b/src/components/tree/phyloTree/change.js
index 88929688a..59b8dcf9c 100644
--- a/src/components/tree/phyloTree/change.js
+++ b/src/components/tree/phyloTree/change.js
@@ -179,7 +179,7 @@ export const modifySVG = function modifySVG(elemsToUpdate, svgPropsToUpdate, tra
}
/* background temporal time slice */
- if (extras.timeSliceHasPotentiallyChanged) {
+ if (extras.timeSliceHasPotentiallyChanged || (extras.newZoomMode!==undefined)) {
this.showTemporalSlice();
}
@@ -258,6 +258,7 @@ export const change = function change({
/* change these things to provided value (unless undefined) */
newDistance = undefined,
newLayout = undefined,
+ newZoomMode = undefined,
updateLayout = undefined, // todo - this seems identical to `newLayout`
newBranchLabellingKey = undefined,
newTipLabelKey = undefined,
@@ -277,6 +278,8 @@ export const change = function change({
const nodePropsToModify = {}; /* which properties (keys) on the nodes should be updated (before the SVG) */
const svgPropsToUpdate = new Set(); /* which SVG properties shall be changed. E.g. "fill", "stroke" */
const useModifySVGInStages = newLayout; /* use modifySVGInStages rather than modifySVG. Not used often. */
+ const timeSliceHasPotentiallyChanged = changeVisibility || newDistance;
+ if (newZoomMode!==undefined) this.treeZoomsTemporally = newZoomMode;
/* calculate dt */
const idealTransitionTime = 500;
@@ -312,7 +315,8 @@ export const change = function change({
svgPropsToUpdate.add("stroke-width");
nodePropsToModify["stroke-width"] = branchThickness;
}
- if (newDistance || newLayout || updateLayout || zoomIntoClade || svgHasChangedDimensions || changeNodeOrder) {
+ if (newDistance || newLayout || updateLayout || zoomIntoClade || (newZoomMode!==undefined) ||
+ svgHasChangedDimensions || changeNodeOrder || timeSliceHasPotentiallyChanged) {
elemsToUpdate.add(".tip").add(".branch.S").add(".branch.T").add(".branch");
elemsToUpdate.add(".vaccineCross").add(".vaccineDottedLine").add(".conf");
elemsToUpdate.add('.branchLabel').add('.tipLabel');
@@ -368,13 +372,15 @@ export const change = function change({
/* mapToScreen */
if (
svgPropsToUpdate.has(["stroke-width"]) ||
- newDistance ||
+ newDistance || // technically unnecessary as part of timeSliceHas...
newLayout ||
changeNodeOrder ||
updateLayout ||
zoomIntoClade ||
svgHasChangedDimensions ||
- showConfidences
+ showConfidences ||
+ (newZoomMode!==undefined) ||
+ timeSliceHasPotentiallyChanged // TODO could be expensive to run mapToScreen every time here...
) {
this.mapToScreen();
}
@@ -388,8 +394,7 @@ export const change = function change({
if (svgHasChangedDimensions) {
this.setClipMask();
}
- const extras = { removeConfidences, showConfidences, newBranchLabellingKey };
- extras.timeSliceHasPotentiallyChanged = changeVisibility || newDistance;
+ const extras = { removeConfidences, showConfidences, newBranchLabellingKey, timeSliceHasPotentiallyChanged, newZoomMode};
extras.hideTipLabels = animationInProgress;
if (useModifySVGInStages) {
this.modifySVGInStages(elemsToUpdate, svgPropsToUpdate, transitionTime, 1000, extras);
diff --git a/src/components/tree/phyloTree/grid.js b/src/components/tree/phyloTree/grid.js
index 86a0a668b..0bb9cc07f 100644
--- a/src/components/tree/phyloTree/grid.js
+++ b/src/components/tree/phyloTree/grid.js
@@ -441,7 +441,7 @@ export const temporalWindowTransition = transition('temporalWindowTransition')
* add background grey rectangles to demarcate the temporal slice
*/
export const showTemporalSlice = function showTemporalSlice() {
- if (this.layout !== "rect" || this.distance !== "num_date") {
+ if (this.layout !== "rect" || this.distance !== "num_date" || this.treeZoomsTemporally===true) {
this.hideTemporalSlice();
return;
}
diff --git a/src/components/tree/phyloTree/layouts.js b/src/components/tree/phyloTree/layouts.js
index d6e0340bb..058feaa4b 100644
--- a/src/components/tree/phyloTree/layouts.js
+++ b/src/components/tree/phyloTree/layouts.js
@@ -355,6 +355,19 @@ export const mapToScreen = function mapToScreen() {
nodesInDomain = nodesInDomain.filter((d) => !d.n.hasChildren);
}
+ /* experimental -- restrict the nodesInDomain via the time-slice */
+ if (this.treeZoomsTemporally===true) {
+ // TODO -- this works for the max time (and viz is intuitive) but for the earliest date (LHS of rect tree)
+ // we should be a bit smarter.
+ // Note that branches prior to the date cutoff have no visibility (rendered as thin lines) so it's not just
+ // a case of changing the tree domain like I'm prototyping here...
+ const early_cutoff = this.dateRange[0] - 0.1*(this.dateRange[1]-this.dateRange[0]);
+ nodesInDomain = nodesInDomain.filter((d) => {
+ const num_date = getTraitFromNode(d.n, 'num_date');
+ return (num_date > early_cutoff && num_date < this.dateRange[1]);
+ });
+ }
+
/* Compute the domains to pass to the d3 scales for the x & y axes */
let xDomain, yDomain, spanX, spanY;
if (this.layout!=="scatter" || this.scatterVariables.xContinuous) {
diff --git a/src/components/tree/phyloTree/renderers.js b/src/components/tree/phyloTree/renderers.js
index 1e5e6f9de..2afb2720f 100644
--- a/src/components/tree/phyloTree/renderers.js
+++ b/src/components/tree/phyloTree/renderers.js
@@ -21,13 +21,14 @@ 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, parameters, callbacks, branchThickness, visibility, drawConfidence, vaccines, branchStroke, tipStroke, tipFill, tipRadii, dateRange, scatterVariables, treeZoomsTemporally) {
timerStart("phyloTree render()");
this.svg = svg;
this.params = Object.assign(this.params, parameters);
this.callbacks = callbacks;
this.vaccines = vaccines ? vaccines.map((d) => d.shell) : undefined;
this.dateRange = dateRange;
+ this.treeZoomsTemporally = treeZoomsTemporally;
/* set nodes stroke / fill */
this.nodes.forEach((d, i) => {
diff --git a/src/components/tree/reactD3Interface/change.js b/src/components/tree/reactD3Interface/change.js
index 076d01254..cd9c0e152 100644
--- a/src/components/tree/reactD3Interface/change.js
+++ b/src/components/tree/reactD3Interface/change.js
@@ -97,6 +97,11 @@ export const changePhyloTreeViaPropsComparison = (mainTree, phylotree, oldProps,
}
}
+ if (oldProps.treeZoomsTemporally !== newProps.treeZoomsTemporally) {
+ args.newZoomMode = newProps.treeZoomsTemporally;
+ args.updateLayout = true; // unsure why this' needed
+ }
+
if (oldProps.width !== newProps.width || oldProps.height !== newProps.height) {
args.svgHasChangedDimensions = true;
}
diff --git a/src/components/tree/reactD3Interface/initialRender.js b/src/components/tree/reactD3Interface/initialRender.js
index 38ea89c2c..e5554f092 100644
--- a/src/components/tree/reactD3Interface/initialRender.js
+++ b/src/components/tree/reactD3Interface/initialRender.js
@@ -47,6 +47,7 @@ export const renderTree = (that, main, phylotree, props) => {
treeState.nodeColors.map(getBrighterColor),
treeState.tipRadii, /* might be null */
[props.dateMinNumeric, props.dateMaxNumeric],
- props.scatterVariables
+ props.scatterVariables,
+ props.treeZoomsTemporally
);
};
diff --git a/src/reducers/controls.js b/src/reducers/controls.js
index 3e741c0fd..502a61d3e 100644
--- a/src/reducers/controls.js
+++ b/src/reducers/controls.js
@@ -85,6 +85,7 @@ export const getDefaultControlsState = () => {
showTangle: false,
zoomMin: undefined,
zoomMax: undefined,
+ treeZoomsTemporally: true,
branchLengthsToDisplay: "divAndDate",
sidebarOpen: initialSidebarState.sidebarOpen,
treeLegendOpen: undefined,
@@ -123,6 +124,9 @@ const Controls = (state = getDefaultControlsState(), action) => {
explodeAttr: action.explodeAttr,
colorScale: Object.assign({}, state.colorScale, { visibleLegendValues: action.visibleLegendValues })
});
+ case types.TOGGLE_TEMPORAL_ZOOM_FLAG: {
+ return Object.assign({}, state, {treeZoomsTemporally: !state.treeZoomsTemporally});
+ }
case types.CHANGE_BRANCH_LABEL:
return Object.assign({}, state, { selectedBranchLabel: action.value });
case types.CHANGE_LAYOUT: