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: