Skip to content
Draft
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
6 changes: 6 additions & 0 deletions src/actions/recomputeReduxState.js
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,12 @@ const modifyStateViaURLQuery = (state, query) => {
}
state["focus"] = query.focus === undefined ? null : query.focus;

if (query.zoom && query.zoom !== "dynamic") {
console.error(`Invalid zoom value of ${JSON.stringify(query.zoom)}; removing zoom.`);
delete query.zoom;
}
state["zoom"] = query.zoom === undefined ? null : query.zoom;

if (query.ci === undefined) {
state["temporalConfidence"]["on"] = false;
} else {
Expand Down
1 change: 1 addition & 0 deletions src/actions/types.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ export const CHANGE_LAYOUT = "CHANGE_LAYOUT";
export const CHANGE_BRANCH_LABEL = "CHANGE_BRANCH_LABEL";
export const CHANGE_DISTANCE_MEASURE = "CHANGE_DISTANCE_MEASURE";
export const SET_FOCUS = "SET_FOCUS";
export const SET_ZOOM = "SET_ZOOM";
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
2 changes: 2 additions & 0 deletions src/components/controls/controls.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import NormalizeFrequencies from "./frequency-normalization";
import AnimationOptions from "./animation-options";
import { PanelSection } from "./panelSection";
import { ToggleFocus } from "./toggle-focus";
import { ToggleZoom } from "./toggle-zoom";
import ToggleTangle from "./toggle-tangle";
import { ChooseStreamTrees } from "./choose-stream-trees";
import Language from "./language";
Expand Down Expand Up @@ -68,6 +69,7 @@ function Controls(): JSX.Element {
<ChooseLayout />
<ChooseMetric />
<ToggleFocus />
<ToggleZoom />
<ChooseStreamTrees />
<ChooseBranchLabelling />
<ChooseTipLabel />
Expand Down
59 changes: 59 additions & 0 deletions src/components/controls/toggle-zoom.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import React from "react";
import { useSelector } from "react-redux";
import { FaInfoCircle } from "react-icons/fa";
import Toggle from "./toggle";
import { SidebarIconContainer, StyledTooltip } from "./styles";
import { SET_ZOOM } from "../../actions/types";
import { RootState } from "../../store";
import { useAppDispatch } from "../../hooks";
import { useTranslation } from "react-i18next";


export function ToggleZoom(): JSX.Element {
const zoom = useSelector((state: RootState) => state.controls.zoom);
const layout = useSelector((state: RootState) => state.controls.layout);
const dispatch = useAppDispatch();
const mobileDisplay = useSelector((state: RootState) => state.general.mobileDisplay);
const { t } = useTranslation();

// Zoom functionality is only available to rectangular layout
const validLayouts = new Set(["rect"]);
const disabled = !validLayouts.has(layout);

const text = disabled ? t("sidebar:Dynamic zoom unavailable") : t("sidebar:Dynamic zoom");
const label = (
<div style={{ display: "flex", alignItems: "center" }}>
<span style={{ marginRight: "5px" }}>{text}</span>
{!mobileDisplay && (
<>
<SidebarIconContainer style={{ display: "inline-flex" }} data-tip data-for="toggle-zoom">
<FaInfoCircle />
</SidebarIconContainer>
<StyledTooltip place="bottom" type="dark" effect="solid" id="toggle-zoom">
When dynamic zoom is enabled, the tree automatically adjusts its XY bounds
to fill the page based on visible nodes. Date updates and filters will
dynamically update the zoom level. Only applicable to rectangular layout.
</StyledTooltip>
</>
)}
</div>
);

return (
<Toggle
display
on={zoom === "dynamic" && !disabled}
disabled={disabled}
callback={(): void => {
const valueAfterToggling = zoom === "dynamic" ? null : "dynamic";

dispatch({
type: SET_ZOOM,
zoom: valueAfterToggling,
});
}}
label={label}
style={{ marginBottom: 8 }}
/>
);
}
1 change: 1 addition & 0 deletions src/components/tree/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ const mapStateToProps: MapStateToProps<TreeComponentStateProps, TreeComponentOwn
distanceMeasure: state.controls.distanceMeasure,
explodeAttr: state.controls.explodeAttr,
focus: state.controls.focus,
zoom: state.controls.zoom,
colorScale: state.controls.colorScale,
colorings: state.metadata.colorings,
genomeMap: state.entropy.genomeMap,
Expand Down
14 changes: 11 additions & 3 deletions src/components/tree/phyloTree/change.ts
Original file line number Diff line number Diff line change
Expand Up @@ -308,6 +308,7 @@ export const change = function change(
animationInProgress = false,
changeNodeOrder = false,
focus = null,
zoom = undefined,
newDistance = undefined,
newLayout = undefined,
updateLayout = undefined,
Expand All @@ -329,6 +330,12 @@ export const change = function change(
): void {
// console.log("\n** phylotree.change() (time since last run:", Date.now() - this.timeLastRenderRequested, "ms) **\n\n");
timerStart("phylotree.change()");

/* Update zoom state on phyloTree instance */
if (zoom !== undefined) {
this.zoom = zoom;
}

const elemsToUpdate = new Set<TreeElement>(); /* what needs updating? E.g. ".branch", ".tip" etc */
const nodePropsToModify: PropsForPhyloNodes = {}; /* which properties (keys) on the nodes should be updated (before the SVG) */
const svgPropsToUpdate = new Set<SVGProperty>(); /* which SVG properties shall be changed. E.g. "fill", "stroke" */
Expand Down Expand Up @@ -376,7 +383,7 @@ 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 || svgHasChangedDimensions || changeNodeOrder || changeVisibility) {
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 @@ -428,7 +435,7 @@ export const change = function change(
zoomIntoClade.n.parent.shell;
applyToChildren(this.zoomNode, (d: PhyloNode) => {d.inView = true;});
}
if (svgHasChangedDimensions || changeNodeOrder) {
if (svgHasChangedDimensions || changeNodeOrder || changeVisibility) {
this.nodes.forEach((d) => {d.update = true;});
}

Expand Down Expand Up @@ -465,6 +472,7 @@ export const change = function change(
zoomIntoClade ||
svgHasChangedDimensions ||
streamDefinitionChange ||
changeVisibility ||
showConfidences
) {
this.mapToScreen();
Expand Down Expand Up @@ -501,7 +509,7 @@ export const change = function change(
this.drawStreams(); // removes streams, as appropriate
} else {
const extras: Extras = { removeConfidences, showConfidences, newBranchLabellingKey };
extras.timeSliceHasPotentiallyChanged = changeVisibility || newDistance !== undefined;
extras.timeSliceHasPotentiallyChanged = changeVisibility || newDistance !== undefined || updateLayout;
extras.hideTipLabels = animationInProgress || newTipLabelKey === 'none';
if (useModifySVGInStages) {
this.modifySVGInStages(elemsToUpdate, svgPropsToUpdate, transitionTime, 1000, extras);
Expand Down
2 changes: 1 addition & 1 deletion src/components/tree/phyloTree/grid.js
Original file line number Diff line number Diff line change
Expand Up @@ -443,7 +443,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.zoom === "dynamic") {
this.hideTemporalSlice();
return;
}
Expand Down
22 changes: 21 additions & 1 deletion src/components/tree/phyloTree/layouts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { numDate } from "../../../util/colorHelpers";
import { Layout, ScatterVariables } from "../../../reducers/controls";
import { ReduxNode, colorBySymbol } from "../../../reducers/tree/types";
import { Distance, Params, PhyloNode, PhyloTreeType, Ripple } from "./types";
import { NODE_VISIBLE } from "../../../util/globals";

/**
* assigns the attribute this.layout and calls the function that
Expand Down Expand Up @@ -359,7 +360,26 @@ export const mapToScreen = function mapToScreen(this: PhyloTreeType): void {
/* update the clip mask accordingly */
this.setClipMask();

let nodesInDomain = this.nodes.filter((d) => d.inView && d.y!==undefined && d.x!==undefined);
/* ------------------------------------------------------------------
* Conditionally limit axis domains based on dynamic zoom setting.
* When zoom is "dynamic", use visible nodes. Otherwise use inView.
* ------------------------------------------------------------------ */
const inViewNodes = this.nodes.filter((d) => d.inView && d.y!==undefined && d.x!==undefined);
let nodesInDomain: PhyloNode[];

if (this.zoom === "dynamic") {
const visibleNodes = inViewNodes.filter((d) => d.visibility === NODE_VISIBLE);

/* Fallback: if nothing is currently visible (e.g. the time-slider is
before the earliest sample), fall back to the original “inView” rule
so the scales never become undefined. */
nodesInDomain = visibleNodes.length > 0
? visibleNodes
: inViewNodes;
} else {
nodesInDomain = inViewNodes;
}

// scatterplots further restrict nodes used for domain calcs - if not rendering branches,
// then we don't consider internal nodes for the domain calc
if (this.layout==="scatter" && this.scatterVariables.showBranches===false) {
Expand Down
1 change: 1 addition & 0 deletions src/components/tree/phyloTree/phyloTree.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ const PhyloTree = function PhyloTree(
return phyloNode;
});
this.zoomNode = this.nodes[idxOfInViewRootNode];
this.zoom = null; /* initialize zoom state */
this.strainToNode = {};
this.nodes.forEach((phylonode) => {this.strainToNode[phylonode.n.name] = phylonode;});
/* debounced functions (AFAIK you can't define these as normal prototypes as they need "this") */
Expand Down
9 changes: 7 additions & 2 deletions src/components/tree/phyloTree/renderers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { getEmphasizedColor } from "../../../util/colorHelpers";
import { Callbacks, Distance, Params, PhyloNode, PhyloTreeType, Ripple } from "./types";
import { select, Selection } from "d3-selection";
import { area } from "d3-shape";
import { Focus, Layout, ScatterVariables } from "../../../reducers/controls";
import { Focus, Zoom, Layout, ScatterVariables } from "../../../reducers/controls";
import { ReduxNode, Visibility, StreamSummary, TreeState } from "../../../reducers/tree/types";

export const render = function render(
Expand All @@ -16,6 +16,7 @@ export const render = function render(
layout,
distance,
focus,
zoom,
parameters,
callbacks,
branchThickness,
Expand Down Expand Up @@ -43,6 +44,9 @@ export const render = function render(
/** how to focus on nodes */
focus: Focus

/** how to zoom the tree */
zoom: Zoom

/** an object that contains options that will be added to this.params */
parameters: Partial<Params>

Expand Down Expand Up @@ -93,6 +97,7 @@ export const render = function render(
this.measurementsColorGrouping = measurementsColorGrouping;
this.dateRange = dateRange;
this.streams = streams;
this.zoom = zoom;

/* set nodes stroke / fill */
this.nodes.forEach((d, i) => {
Expand Down Expand Up @@ -811,4 +816,4 @@ export function highlightStreamtreeRipples(this: PhyloTreeType, attr: TreeState[
select(this).style('opacity', nonHoveredRippleOpacity)
}
})
}
}
4 changes: 3 additions & 1 deletion src/components/tree/phyloTree/types.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Selection } from "d3-selection";
import { Focus, Layout, PerformanceFlags, ScatterVariables } from "../../../reducers/controls";
import { Focus, Zoom, Layout, PerformanceFlags, ScatterVariables } from "../../../reducers/controls";
import { ReduxNode, Visibility, Streams, TreeState } from "../../../reducers/tree/types";
import { change, modifySVG, modifySVGInStages } from "./change";
import { TreeComponent } from "../tree";
Expand Down Expand Up @@ -221,6 +221,7 @@ export interface ChangeParams {
animationInProgress?: boolean
changeNodeOrder?: boolean
focus?: Focus
zoom?: Zoom

/**
* Streams are either toggled on/off or the partitioning (branch label) has changed
Expand Down Expand Up @@ -326,6 +327,7 @@ export interface PhyloTreeType {
updateTipLabels: typeof labels.updateTipLabels
vaccines?: PhyloNode[]
visibility: Visibility[]
zoom: Zoom

showStreamTrees: boolean;

Expand Down
12 changes: 12 additions & 0 deletions src/components/tree/reactD3Interface/change.ts
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,18 @@ export const changePhyloTreeViaPropsComparison = (
args.updateLayout = true;
}

/* enable/disable dynamic zoom */
if (oldProps.zoom !== newProps.zoom) {
args.zoom = newProps.zoom;
args.updateLayout = true;
}
/* re-apply dynamic zoom on changes */
else if (newProps.zoom === "dynamic") {
if (dateRangeChange || filterChange) {
args.zoom = "dynamic";
}
}

/* change in key used to define branch labels, tip labels */
if (oldProps.canRenderBranchLabels===true && newProps.canRenderBranchLabels===false) {
args.newBranchLabellingKey = "none";
Expand Down
1 change: 1 addition & 0 deletions src/components/tree/reactD3Interface/initialRender.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ export const renderTree = (
layout: props.layout,
distance: props.distanceMeasure,
focus: props.focus,
zoom: props.zoom,
parameters: { /* modifies PhyloTree's defaults */
grid: true,
confidence: props.temporalConfidence.display,
Expand Down
3 changes: 2 additions & 1 deletion src/components/tree/types.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { WithTranslation } from "react-i18next"
import { ColorScale, Focus, Layout, PerformanceFlags, ScatterVariables, SelectedNode, TemporalConfidence } from "../../reducers/controls";
import { ColorScale, Focus, Zoom, Layout, PerformanceFlags, ScatterVariables, SelectedNode, TemporalConfidence } from "../../reducers/controls";
import { TreeState, TreeTooState } from "../../reducers/tree/types";
import { AppDispatch } from "../../store";
import { Distance, PhyloNode, PhyloTreeType } from "./phyloTree/types";
Expand Down Expand Up @@ -27,6 +27,7 @@ export interface TreeComponentStateProps {
explodeAttr: string
filters: Record<string, Array<{ value: string, active: boolean }>>
focus: Focus
zoom: Zoom
genomeMap: unknown
layout: Layout
narrativeMode: boolean
Expand Down
8 changes: 8 additions & 0 deletions src/middleware/changeURL.js
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,14 @@ export const changeURLMiddleware = (store) => (next) => (action) => {
query.focus = action.focus;
}
break;
case types.SET_ZOOM:
if (action.zoom === null) {
delete query.zoom;
}
else {
query.zoom = action.zoom;
}
break;
case types.TOGGLE_TEMPORAL_CONF:
if ("ci" in query) {
query.ci = undefined;
Expand Down
10 changes: 10 additions & 0 deletions src/reducers/controls.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { defaultGeoResolution,
defaultDistanceMeasure,
defaultLayout,
defaultFocus,
defaultZoom,
controlsHiddenWidth,
strainSymbol,
twoColumnBreakpoint } from "../util/globals";
Expand Down Expand Up @@ -40,6 +41,8 @@ export type Layout = "rect" | "radial" | "unrooted" | "clock" | "scatter"

export type Focus = "selected" | null

export type Zoom = "dynamic" | null

export type LegendBounds = {
[key: string | number]: [number, number]
}
Expand Down Expand Up @@ -95,6 +98,7 @@ interface Defaults {
distanceMeasure: Distance
layout: Layout
focus: Focus
zoom: Zoom
geoResolution: string
filters: Record<string, unknown>
filtersInFooter: string[]
Expand Down Expand Up @@ -135,6 +139,7 @@ export interface BasicControlsState {
filters: Record<string | symbol, Array<{ value: string, active: boolean }>>
filtersInFooter: string[]
focus: Focus
zoom: Zoom
geoResolution: string
layout: Layout
mapAnimationCumulative: boolean
Expand Down Expand Up @@ -188,6 +193,7 @@ export const getDefaultControlsState = (): ControlsState => {
distanceMeasure: defaultDistanceMeasure,
layout: defaultLayout,
focus: defaultFocus,
zoom: defaultZoom,
geoResolution: defaultGeoResolution,
filters: {},
filtersInFooter: [],
Expand Down Expand Up @@ -216,6 +222,7 @@ export const getDefaultControlsState = (): ControlsState => {
scatterVariables: {},
distanceMeasure: defaults.distanceMeasure,
focus: defaults.focus,
zoom: defaults.zoom,
dateMin,
dateMinNumeric,
dateMax,
Expand Down Expand Up @@ -345,6 +352,9 @@ const Controls = (state: ControlsState = getDefaultControlsState(), action): Con
case types.SET_FOCUS: {
return {...state, focus: action.focus}
}
case types.SET_ZOOM: {
return {...state, zoom: action.zoom}
}
case types.CHANGE_DATES_VISIBILITY_THICKNESS: {
const newDates: Partial<ControlsState> = { quickdraw: action.quickdraw };
if (action.dateMin) {
Expand Down
Loading
Loading