From c4bcae994e7b920a069f6bfdb856c3500488fe29 Mon Sep 17 00:00:00 2001 From: Victor Lin <13424970+victorlin@users.noreply.github.com> Date: Fri, 25 Oct 2024 15:42:36 -0700 Subject: [PATCH 01/31] Compile for ES2015 ES2015 allows iteration of sets which is used in this codebase. --- tsconfig.json | 1 + 1 file changed, 1 insertion(+) diff --git a/tsconfig.json b/tsconfig.json index 4b8fa23c5..71ded34a2 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -13,6 +13,7 @@ Visit https://aka.ms/tsconfig.json for a detailed list of options. "compilerOptions": { /* Language and Environment */ "jsx": "react", /* Specify what JSX code is generated. */ + "target": "es2015", /* Modules */ "moduleResolution": "node", /* Specify how TypeScript looks up a file from a given module specifier. */ From d06d095497d4b729d439c16c476b73bb9e3b01fc Mon Sep 17 00:00:00 2001 From: Victor Lin <13424970+victorlin@users.noreply.github.com> Date: Mon, 28 Oct 2024 15:15:00 -0700 Subject: [PATCH 02/31] Fix imports from d3-scale Use named imports from the package instead of direct imports from source. This allows proper typing by the external types/d3 package. --- src/components/tree/phyloTree/layouts.js | 3 +-- src/util/colorScale.js | 3 +-- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/src/components/tree/phyloTree/layouts.js b/src/components/tree/phyloTree/layouts.js index 226ae2352..0c142d2cf 100644 --- a/src/components/tree/phyloTree/layouts.js +++ b/src/components/tree/phyloTree/layouts.js @@ -1,8 +1,7 @@ /* eslint-disable no-multi-spaces */ /* eslint-disable space-infix-ops */ import { min, max } from "d3-array"; -import scaleLinear from "d3-scale/src/linear"; -import {point as scalePoint} from "d3-scale/src/band"; +import { scaleLinear, scalePoint } from "d3-scale"; import { timerStart, timerEnd } from "../../../util/perf"; import { getTraitFromNode, getDivFromNode } from "../../../util/treeMiscHelpers"; import { stemParent, nodeOrdering } from "./helpers"; diff --git a/src/util/colorScale.js b/src/util/colorScale.js index f92e56b0e..47a9f932a 100644 --- a/src/util/colorScale.js +++ b/src/util/colorScale.js @@ -1,5 +1,4 @@ -import scaleOrdinal from "d3-scale/src/ordinal"; -import scaleLinear from "d3-scale/src/linear"; +import { scaleLinear, scaleOrdinal } from "d3-scale"; import { min, max, range as d3Range } from "d3-array"; import { rgb } from "d3-color"; import { interpolateHcl } from "d3-interpolate"; From 1b4c441c70f03ebac1169a28acf30a73d1f27e8c Mon Sep 17 00:00:00 2001 From: Victor Lin <13424970+victorlin@users.noreply.github.com> Date: Thu, 24 Oct 2024 18:12:28 -0700 Subject: [PATCH 03/31] Remove non-existent parameter from docstring --- src/actions/tree.js | 1 - 1 file changed, 1 deletion(-) diff --git a/src/actions/tree.js b/src/actions/tree.js index a5f7de910..fb891e642 100644 --- a/src/actions/tree.js +++ b/src/actions/tree.js @@ -63,7 +63,6 @@ export const applyInViewNodesToTree = (idx, tree) => { * for arg destructuring see https://simonsmith.io/destructuring-objects-as-function-parameters-in-es6/ * @param {array|undefined} root Change the in-view part of the tree. [root idx tree1, root idx tree2]. * [0, 0]: reset. [undefined, undefined]: do nothing - * @param {object | undefined} tipSelected * @param {string | undefined} cladeSelected * @return {function} a function to be handled by redux (thunk) */ From cb7e657a4af372c28b42b652b939ba5decb011c0 Mon Sep 17 00:00:00 2001 From: Victor Lin <13424970+victorlin@users.noreply.github.com> Date: Tue, 29 Oct 2024 17:45:01 -0700 Subject: [PATCH 04/31] render(): Convert from positional to named parameters For improved readability. --- src/components/tree/phyloTree/renderers.js | 2 +- .../tree/reactD3Interface/initialRender.js | 36 +++++++++---------- 2 files changed, 19 insertions(+), 19 deletions(-) diff --git a/src/components/tree/phyloTree/renderers.js b/src/components/tree/phyloTree/renderers.js index b2cd90298..099c20f2d 100644 --- a/src/components/tree/phyloTree/renderers.js +++ b/src/components/tree/phyloTree/renderers.js @@ -22,7 +22,7 @@ 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, focus, parameters, callbacks, branchThickness, visibility, drawConfidence, vaccines, branchStroke, tipStroke, tipFill, tipRadii, dateRange, scatterVariables) { +export const render = function render({svg, layout, distance, focus, parameters, callbacks, branchThickness, visibility, drawConfidence, vaccines, branchStroke, tipStroke, tipFill, tipRadii, dateRange, scatterVariables}) { timerStart("phyloTree render()"); this.svg = svg; this.params = Object.assign(this.params, parameters); diff --git a/src/components/tree/reactD3Interface/initialRender.js b/src/components/tree/reactD3Interface/initialRender.js index 9b44fa0b6..b836a4e2a 100644 --- a/src/components/tree/reactD3Interface/initialRender.js +++ b/src/components/tree/reactD3Interface/initialRender.js @@ -18,12 +18,12 @@ export const renderTree = (that, main, phylotree, props) => { } const tipStrokeColors = calculateStrokeColors(treeState, false, props.colorByConfidence, props.colorBy); /* simply the call to phylotree.render */ - phylotree.render( - select(ref), - props.layout, - props.distanceMeasure, - props.focus, - { /* parameters (modifies PhyloTree's defaults) */ + phylotree.render({ + svg: select(ref), + layout: props.layout, + distance: props.distanceMeasure, + focus: props.focus, + parameters: { /* modifies PhyloTree's defaults */ grid: true, confidence: props.temporalConfidence.display, branchLabelKey: renderBranchLabels && props.selectedBranchLabel, @@ -32,7 +32,7 @@ export const renderTree = (that, main, phylotree, props) => { tipLabels: true, showTipLabels: true }, - { /* callbacks */ + callbacks: { onTipHover: callbacks.onTipHover.bind(that), onTipClick: callbacks.onTipClick.bind(that), onBranchHover: callbacks.onBranchHover.bind(that), @@ -41,15 +41,15 @@ export const renderTree = (that, main, phylotree, props) => { onTipLeave: callbacks.onTipLeave.bind(that), tipLabel: makeTipLabelFunc(props.tipLabelKey) }, - treeState.branchThickness, /* guaranteed to be in redux by now */ - treeState.visibility, - props.temporalConfidence.on, /* drawConfidence? */ - treeState.vaccines, - calculateStrokeColors(treeState, true, props.colorByConfidence, props.colorBy), - tipStrokeColors, - tipStrokeColors.map(getBrighterColor), // tip fill colors - treeState.tipRadii, /* might be null */ - [props.dateMinNumeric, props.dateMaxNumeric], - props.scatterVariables - ); + branchThickness: treeState.branchThickness, /* guaranteed to be in redux by now */ + visibility: treeState.visibility, + drawConfidence: props.temporalConfidence.on, + vaccines: treeState.vaccines, + branchStroke: calculateStrokeColors(treeState, true, props.colorByConfidence, props.colorBy), + tipStroke: tipStrokeColors, + tipFill: tipStrokeColors.map(getBrighterColor), + tipRadii: treeState.tipRadii, + dateRange: [props.dateMinNumeric, props.dateMaxNumeric], + scatterVariables: props.scatterVariables, + }); }; From fd26d53fb0c48605558255f36639fbcf216b8dfc Mon Sep 17 00:00:00 2001 From: Victor Lin <13424970+victorlin@users.noreply.github.com> Date: Tue, 5 Nov 2024 15:28:20 -0800 Subject: [PATCH 05/31] Use context-specific dispatch type The AppDispatch type is already used elsewhere. --- src/components/controls/toggle-focus.tsx | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/components/controls/toggle-focus.tsx b/src/components/controls/toggle-focus.tsx index 2f82f87ae..06da849c5 100644 --- a/src/components/controls/toggle-focus.tsx +++ b/src/components/controls/toggle-focus.tsx @@ -1,18 +1,17 @@ import React from "react"; import { connect } from "react-redux"; import { FaInfoCircle } from "react-icons/fa"; -import { Dispatch } from "@reduxjs/toolkit"; import Toggle from "./toggle"; import { SidebarIconContainer, StyledTooltip } from "./styles"; import { TOGGLE_FOCUS } from "../../actions/types"; -import { RootState } from "../../store"; +import { AppDispatch, RootState } from "../../store"; function ToggleFocus({ tooltip, focus, layout, dispatch, mobileDisplay }: { tooltip: React.ReactElement; focus: boolean; layout: "rect" | "radial" | "unrooted" | "clock" | "scatter"; - dispatch: Dispatch; + dispatch: AppDispatch; mobileDisplay: boolean; }) { // Focus functionality is only available to layouts that have the concept of a unitless y-axis From 7b3a0e1344fc57a9380817306b9068ff43fa4cf9 Mon Sep 17 00:00:00 2001 From: Victor Lin <13424970+victorlin@users.noreply.github.com> Date: Tue, 5 Nov 2024 15:27:05 -0800 Subject: [PATCH 06/31] Add tree-related types Converted most but not all tree-related files. There are many type errors exposed by conversion. They will be addressed in subsequent commits. I removed a few comments that are now self-explanatory with the added types. I also moved all JSDoc types and descriptions into the function signature. Note that there are many optional properties in the newly defined interfaces. This is not ideal and doesn't capture the fact that many properties are conditional on things such as layout. But it's a starting point to be improved over time. --- package-lock.json | 538 ++++++++++++++++++ package.json | 1 + src/actions/{tree.js => tree.ts} | 222 +++++--- src/components/tree/index.ts | 11 +- .../tree/phyloTree/{change.js => change.ts} | 199 ++++--- .../{defaultParams.js => defaultParams.ts} | 3 +- .../tree/phyloTree/{helpers.js => helpers.ts} | 87 +-- .../tree/phyloTree/{layouts.js => layouts.ts} | 68 ++- .../phyloTree/{phyloTree.js => phyloTree.ts} | 15 +- .../{regression.js => regression.ts} | 13 +- .../phyloTree/{renderers.js => renderers.ts} | 144 +++-- src/components/tree/phyloTree/types.ts | 297 ++++++++++ .../{callbacks.js => callbacks.ts} | 25 +- .../reactD3Interface/{change.js => change.ts} | 25 +- .../{initialRender.js => initialRender.ts} | 12 +- src/components/tree/{tree.js => tree.tsx} | 65 ++- src/components/tree/types.ts | 57 ++ src/{globalStyles.js => globalStyles.ts} | 37 +- src/metadata.ts | 36 ++ src/reducers/controls.ts | 130 ++++- src/reducers/index.ts | 7 +- src/reducers/{tree.js => tree/index.ts} | 46 +- src/reducers/{treeToo.js => tree/treeToo.ts} | 42 +- src/reducers/tree/types.ts | 86 +++ src/util/{colorScale.js => colorScale.ts} | 230 +++++--- src/util/tipRadiusHelpers.ts | 69 ++- ...ntingHelpers.js => treeCountingHelpers.ts} | 57 +- ...sonProcessing.js => treeJsonProcessing.ts} | 87 +-- 28 files changed, 2081 insertions(+), 528 deletions(-) rename src/actions/{tree.js => tree.ts} (72%) rename src/components/tree/phyloTree/{change.js => change.ts} (72%) rename src/components/tree/phyloTree/{defaultParams.js => defaultParams.ts} (92%) rename src/components/tree/phyloTree/{helpers.js => helpers.ts} (84%) rename src/components/tree/phyloTree/{layouts.js => layouts.ts} (92%) rename src/components/tree/phyloTree/{phyloTree.js => phyloTree.ts} (92%) rename src/components/tree/phyloTree/{regression.js => regression.ts} (85%) rename src/components/tree/phyloTree/{renderers.js => renderers.ts} (82%) create mode 100644 src/components/tree/phyloTree/types.ts rename src/components/tree/reactD3Interface/{callbacks.js => callbacks.ts} (84%) rename src/components/tree/reactD3Interface/{change.js => change.ts} (89%) rename src/components/tree/reactD3Interface/{initialRender.js => initialRender.ts} (89%) rename src/components/tree/{tree.js => tree.tsx} (84%) create mode 100644 src/components/tree/types.ts rename src/{globalStyles.js => globalStyles.ts} (84%) create mode 100644 src/metadata.ts rename src/reducers/{tree.js => tree/index.ts} (73%) rename src/reducers/{treeToo.js => tree/treeToo.ts} (72%) create mode 100644 src/reducers/tree/types.ts rename src/util/{colorScale.js => colorScale.ts} (77%) rename src/util/{treeCountingHelpers.js => treeCountingHelpers.ts} (68%) rename src/util/{treeJsonProcessing.js => treeJsonProcessing.ts} (75%) diff --git a/package-lock.json b/package-lock.json index 141c34bdb..878e94e28 100644 --- a/package-lock.json +++ b/package-lock.json @@ -96,6 +96,7 @@ }, "devDependencies": { "@playwright/test": "^1.48.2", + "@types/d3": "^7.4.3", "@types/leaflet": "^1.9.3", "@types/node": "^18.15.11", "@types/webpack-env": "^1.18.2", @@ -3051,6 +3052,290 @@ "integrity": "sha512-t73xJJrvdTjXrn4jLS9VSGRbz0nUY3cl2DMGDU48lKl+HR9dbbjW2A9r3g40VA++mQpy6uuHg33gy7du2BKpog==", "dev": true }, + "node_modules/@types/d3": { + "version": "7.4.3", + "resolved": "https://registry.npmjs.org/@types/d3/-/d3-7.4.3.tgz", + "integrity": "sha512-lZXZ9ckh5R8uiFVt8ogUNf+pIrK4EsWrx2Np75WvF/eTpJ0FMHNhjXk8CKEx/+gpHbNQyJWehbFaTvqmHWB3ww==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/d3-array": "*", + "@types/d3-axis": "*", + "@types/d3-brush": "*", + "@types/d3-chord": "*", + "@types/d3-color": "*", + "@types/d3-contour": "*", + "@types/d3-delaunay": "*", + "@types/d3-dispatch": "*", + "@types/d3-drag": "*", + "@types/d3-dsv": "*", + "@types/d3-ease": "*", + "@types/d3-fetch": "*", + "@types/d3-force": "*", + "@types/d3-format": "*", + "@types/d3-geo": "*", + "@types/d3-hierarchy": "*", + "@types/d3-interpolate": "*", + "@types/d3-path": "*", + "@types/d3-polygon": "*", + "@types/d3-quadtree": "*", + "@types/d3-random": "*", + "@types/d3-scale": "*", + "@types/d3-scale-chromatic": "*", + "@types/d3-selection": "*", + "@types/d3-shape": "*", + "@types/d3-time": "*", + "@types/d3-time-format": "*", + "@types/d3-timer": "*", + "@types/d3-transition": "*", + "@types/d3-zoom": "*" + } + }, + "node_modules/@types/d3-array": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-3.2.1.tgz", + "integrity": "sha512-Y2Jn2idRrLzUfAKV2LyRImR+y4oa2AntrgID95SHJxuMUrkNXmanDSed71sRNZysveJVt1hLLemQZIady0FpEg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/d3-axis": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@types/d3-axis/-/d3-axis-3.0.6.tgz", + "integrity": "sha512-pYeijfZuBd87T0hGn0FO1vQ/cgLk6E1ALJjfkC0oJ8cbwkZl3TpgS8bVBLZN+2jjGgg38epgxb2zmoGtSfvgMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/d3-selection": "*" + } + }, + "node_modules/@types/d3-brush": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@types/d3-brush/-/d3-brush-3.0.6.tgz", + "integrity": "sha512-nH60IZNNxEcrh6L1ZSMNA28rj27ut/2ZmI3r96Zd+1jrZD++zD3LsMIjWlvg4AYrHn/Pqz4CF3veCxGjtbqt7A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/d3-selection": "*" + } + }, + "node_modules/@types/d3-chord": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@types/d3-chord/-/d3-chord-3.0.6.tgz", + "integrity": "sha512-LFYWWd8nwfwEmTZG9PfQxd17HbNPksHBiJHaKuY1XeqscXacsS2tyoo6OdRsjf+NQYeB6XrNL3a25E3gH69lcg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/d3-color": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/@types/d3-color/-/d3-color-3.1.3.tgz", + "integrity": "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/d3-contour": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@types/d3-contour/-/d3-contour-3.0.6.tgz", + "integrity": "sha512-BjzLgXGnCWjUSYGfH1cpdo41/hgdWETu4YxpezoztawmqsvCeep+8QGfiY6YbDvfgHz/DkjeIkkZVJavB4a3rg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/d3-array": "*", + "@types/geojson": "*" + } + }, + "node_modules/@types/d3-delaunay": { + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-delaunay/-/d3-delaunay-6.0.4.tgz", + "integrity": "sha512-ZMaSKu4THYCU6sV64Lhg6qjf1orxBthaC161plr5KuPHo3CNm8DTHiLw/5Eq2b6TsNP0W0iJrUOFscY6Q450Hw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/d3-dispatch": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@types/d3-dispatch/-/d3-dispatch-3.0.6.tgz", + "integrity": "sha512-4fvZhzMeeuBJYZXRXrRIQnvUYfyXwYmLsdiN7XXmVNQKKw1cM8a5WdID0g1hVFZDqT9ZqZEY5pD44p24VS7iZQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/d3-drag": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@types/d3-drag/-/d3-drag-3.0.7.tgz", + "integrity": "sha512-HE3jVKlzU9AaMazNufooRJ5ZpWmLIoc90A37WU2JMmeq28w1FQqCZswHZ3xR+SuxYftzHq6WU6KJHvqxKzTxxQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/d3-selection": "*" + } + }, + "node_modules/@types/d3-dsv": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@types/d3-dsv/-/d3-dsv-3.0.7.tgz", + "integrity": "sha512-n6QBF9/+XASqcKK6waudgL0pf/S5XHPPI8APyMLLUHd8NqouBGLsU8MgtO7NINGtPBtk9Kko/W4ea0oAspwh9g==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/d3-ease": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-ease/-/d3-ease-3.0.2.tgz", + "integrity": "sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/d3-fetch": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@types/d3-fetch/-/d3-fetch-3.0.7.tgz", + "integrity": "sha512-fTAfNmxSb9SOWNB9IoG5c8Hg6R+AzUHDRlsXsDZsNp6sxAEOP0tkP3gKkNSO/qmHPoBFTxNrjDprVHDQDvo5aA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/d3-dsv": "*" + } + }, + "node_modules/@types/d3-force": { + "version": "3.0.10", + "resolved": "https://registry.npmjs.org/@types/d3-force/-/d3-force-3.0.10.tgz", + "integrity": "sha512-ZYeSaCF3p73RdOKcjj+swRlZfnYpK1EbaDiYICEEp5Q6sUiqFaFQ9qgoshp5CzIyyb/yD09kD9o2zEltCexlgw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/d3-format": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-format/-/d3-format-3.0.4.tgz", + "integrity": "sha512-fALi2aI6shfg7vM5KiR1wNJnZ7r6UuggVqtDA+xiEdPZQwy/trcQaHnwShLuLdta2rTymCNpxYTiMZX/e09F4g==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/d3-geo": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@types/d3-geo/-/d3-geo-3.1.0.tgz", + "integrity": "sha512-856sckF0oP/diXtS4jNsiQw/UuK5fQG8l/a9VVLeSouf1/PPbBE1i1W852zVwKwYCBkFJJB7nCFTbk6UMEXBOQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/geojson": "*" + } + }, + "node_modules/@types/d3-hierarchy": { + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/@types/d3-hierarchy/-/d3-hierarchy-3.1.7.tgz", + "integrity": "sha512-tJFtNoYBtRtkNysX1Xq4sxtjK8YgoWUNpIiUee0/jHGRwqvzYxkq0hGVbbOGSz+JgFxxRu4K8nb3YpG3CMARtg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/d3-interpolate": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-interpolate/-/d3-interpolate-3.0.4.tgz", + "integrity": "sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/d3-color": "*" + } + }, + "node_modules/@types/d3-path": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@types/d3-path/-/d3-path-3.1.0.tgz", + "integrity": "sha512-P2dlU/q51fkOc/Gfl3Ul9kicV7l+ra934qBFXCFhrZMOL6du1TM0pm1ThYvENukyOn5h9v+yMJ9Fn5JK4QozrQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/d3-polygon": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-polygon/-/d3-polygon-3.0.2.tgz", + "integrity": "sha512-ZuWOtMaHCkN9xoeEMr1ubW2nGWsp4nIql+OPQRstu4ypeZ+zk3YKqQT0CXVe/PYqrKpZAi+J9mTs05TKwjXSRA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/d3-quadtree": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@types/d3-quadtree/-/d3-quadtree-3.0.6.tgz", + "integrity": "sha512-oUzyO1/Zm6rsxKRHA1vH0NEDG58HrT5icx/azi9MF1TWdtttWl0UIUsjEQBBh+SIkrpd21ZjEv7ptxWys1ncsg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/d3-random": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/d3-random/-/d3-random-3.0.3.tgz", + "integrity": "sha512-Imagg1vJ3y76Y2ea0871wpabqp613+8/r0mCLEBfdtqC7xMSfj9idOnmBYyMoULfHePJyxMAw3nWhJxzc+LFwQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/d3-scale": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/@types/d3-scale/-/d3-scale-4.0.8.tgz", + "integrity": "sha512-gkK1VVTr5iNiYJ7vWDI+yUFFlszhNMtVeneJ6lUTKPjprsvLLI9/tgEGiXJOnlINJA8FyA88gfnQsHbybVZrYQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/d3-time": "*" + } + }, + "node_modules/@types/d3-scale-chromatic": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/d3-scale-chromatic/-/d3-scale-chromatic-3.0.3.tgz", + "integrity": "sha512-laXM4+1o5ImZv3RpFAsTRn3TEkzqkytiOY0Dz0sq5cnd1dtNlk6sHLon4OvqaiJb28T0S/TdsBI3Sjsy+keJrw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/d3-selection": { + "version": "3.0.11", + "resolved": "https://registry.npmjs.org/@types/d3-selection/-/d3-selection-3.0.11.tgz", + "integrity": "sha512-bhAXu23DJWsrI45xafYpkQ4NtcKMwWnAC/vKrd2l+nxMFuvOT3XMYTIj2opv8vq8AO5Yh7Qac/nSeP/3zjTK0w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/d3-shape": { + "version": "3.1.6", + "resolved": "https://registry.npmjs.org/@types/d3-shape/-/d3-shape-3.1.6.tgz", + "integrity": "sha512-5KKk5aKGu2I+O6SONMYSNflgiP0WfZIQvVUMan50wHsLG1G94JlxEVnCpQARfTtzytuY0p/9PXXZb3I7giofIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/d3-path": "*" + } + }, + "node_modules/@types/d3-time": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/d3-time/-/d3-time-3.0.3.tgz", + "integrity": "sha512-2p6olUZ4w3s+07q3Tm2dbiMZy5pCDfYwtLXXHUnVzXgQlZ/OyPtUz6OL382BkOuGlLXqfT+wqv8Fw2v8/0geBw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/d3-time-format": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/@types/d3-time-format/-/d3-time-format-4.0.3.tgz", + "integrity": "sha512-5xg9rC+wWL8kdDj153qZcsJ0FWiFt0J5RB6LYUNZjwSnesfblqrI/bJ1wBdJ8OQfncgbJG5+2F+qfqnqyzYxyg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/d3-timer": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-timer/-/d3-timer-3.0.2.tgz", + "integrity": "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/d3-transition": { + "version": "3.0.9", + "resolved": "https://registry.npmjs.org/@types/d3-transition/-/d3-transition-3.0.9.tgz", + "integrity": "sha512-uZS5shfxzO3rGlu0cC3bjmMFKsXv+SmZZcgp0KD22ts4uGXp5EVYGzu/0YdwZeKmddhcAccYtREJKkPfXkZuCg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/d3-selection": "*" + } + }, + "node_modules/@types/d3-zoom": { + "version": "3.0.8", + "resolved": "https://registry.npmjs.org/@types/d3-zoom/-/d3-zoom-3.0.8.tgz", + "integrity": "sha512-iqMC4/YlFCSlO8+2Ii1GGGliCAY4XdeG748w5vQUbevlbDu0zSjH/+jojorQVBK/se0j6DUFNPBGSqD3YWYnDw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/d3-interpolate": "*", + "@types/d3-selection": "*" + } + }, "node_modules/@types/eslint": { "version": "8.4.6", "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-8.4.6.tgz", @@ -15290,6 +15575,259 @@ "integrity": "sha512-t73xJJrvdTjXrn4jLS9VSGRbz0nUY3cl2DMGDU48lKl+HR9dbbjW2A9r3g40VA++mQpy6uuHg33gy7du2BKpog==", "dev": true }, + "@types/d3": { + "version": "7.4.3", + "resolved": "https://registry.npmjs.org/@types/d3/-/d3-7.4.3.tgz", + "integrity": "sha512-lZXZ9ckh5R8uiFVt8ogUNf+pIrK4EsWrx2Np75WvF/eTpJ0FMHNhjXk8CKEx/+gpHbNQyJWehbFaTvqmHWB3ww==", + "dev": true, + "requires": { + "@types/d3-array": "*", + "@types/d3-axis": "*", + "@types/d3-brush": "*", + "@types/d3-chord": "*", + "@types/d3-color": "*", + "@types/d3-contour": "*", + "@types/d3-delaunay": "*", + "@types/d3-dispatch": "*", + "@types/d3-drag": "*", + "@types/d3-dsv": "*", + "@types/d3-ease": "*", + "@types/d3-fetch": "*", + "@types/d3-force": "*", + "@types/d3-format": "*", + "@types/d3-geo": "*", + "@types/d3-hierarchy": "*", + "@types/d3-interpolate": "*", + "@types/d3-path": "*", + "@types/d3-polygon": "*", + "@types/d3-quadtree": "*", + "@types/d3-random": "*", + "@types/d3-scale": "*", + "@types/d3-scale-chromatic": "*", + "@types/d3-selection": "*", + "@types/d3-shape": "*", + "@types/d3-time": "*", + "@types/d3-time-format": "*", + "@types/d3-timer": "*", + "@types/d3-transition": "*", + "@types/d3-zoom": "*" + } + }, + "@types/d3-array": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-3.2.1.tgz", + "integrity": "sha512-Y2Jn2idRrLzUfAKV2LyRImR+y4oa2AntrgID95SHJxuMUrkNXmanDSed71sRNZysveJVt1hLLemQZIady0FpEg==", + "dev": true + }, + "@types/d3-axis": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@types/d3-axis/-/d3-axis-3.0.6.tgz", + "integrity": "sha512-pYeijfZuBd87T0hGn0FO1vQ/cgLk6E1ALJjfkC0oJ8cbwkZl3TpgS8bVBLZN+2jjGgg38epgxb2zmoGtSfvgMw==", + "dev": true, + "requires": { + "@types/d3-selection": "*" + } + }, + "@types/d3-brush": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@types/d3-brush/-/d3-brush-3.0.6.tgz", + "integrity": "sha512-nH60IZNNxEcrh6L1ZSMNA28rj27ut/2ZmI3r96Zd+1jrZD++zD3LsMIjWlvg4AYrHn/Pqz4CF3veCxGjtbqt7A==", + "dev": true, + "requires": { + "@types/d3-selection": "*" + } + }, + "@types/d3-chord": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@types/d3-chord/-/d3-chord-3.0.6.tgz", + "integrity": "sha512-LFYWWd8nwfwEmTZG9PfQxd17HbNPksHBiJHaKuY1XeqscXacsS2tyoo6OdRsjf+NQYeB6XrNL3a25E3gH69lcg==", + "dev": true + }, + "@types/d3-color": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/@types/d3-color/-/d3-color-3.1.3.tgz", + "integrity": "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==", + "dev": true + }, + "@types/d3-contour": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@types/d3-contour/-/d3-contour-3.0.6.tgz", + "integrity": "sha512-BjzLgXGnCWjUSYGfH1cpdo41/hgdWETu4YxpezoztawmqsvCeep+8QGfiY6YbDvfgHz/DkjeIkkZVJavB4a3rg==", + "dev": true, + "requires": { + "@types/d3-array": "*", + "@types/geojson": "*" + } + }, + "@types/d3-delaunay": { + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-delaunay/-/d3-delaunay-6.0.4.tgz", + "integrity": "sha512-ZMaSKu4THYCU6sV64Lhg6qjf1orxBthaC161plr5KuPHo3CNm8DTHiLw/5Eq2b6TsNP0W0iJrUOFscY6Q450Hw==", + "dev": true + }, + "@types/d3-dispatch": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@types/d3-dispatch/-/d3-dispatch-3.0.6.tgz", + "integrity": "sha512-4fvZhzMeeuBJYZXRXrRIQnvUYfyXwYmLsdiN7XXmVNQKKw1cM8a5WdID0g1hVFZDqT9ZqZEY5pD44p24VS7iZQ==", + "dev": true + }, + "@types/d3-drag": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@types/d3-drag/-/d3-drag-3.0.7.tgz", + "integrity": "sha512-HE3jVKlzU9AaMazNufooRJ5ZpWmLIoc90A37WU2JMmeq28w1FQqCZswHZ3xR+SuxYftzHq6WU6KJHvqxKzTxxQ==", + "dev": true, + "requires": { + "@types/d3-selection": "*" + } + }, + "@types/d3-dsv": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@types/d3-dsv/-/d3-dsv-3.0.7.tgz", + "integrity": "sha512-n6QBF9/+XASqcKK6waudgL0pf/S5XHPPI8APyMLLUHd8NqouBGLsU8MgtO7NINGtPBtk9Kko/W4ea0oAspwh9g==", + "dev": true + }, + "@types/d3-ease": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-ease/-/d3-ease-3.0.2.tgz", + "integrity": "sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA==", + "dev": true + }, + "@types/d3-fetch": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@types/d3-fetch/-/d3-fetch-3.0.7.tgz", + "integrity": "sha512-fTAfNmxSb9SOWNB9IoG5c8Hg6R+AzUHDRlsXsDZsNp6sxAEOP0tkP3gKkNSO/qmHPoBFTxNrjDprVHDQDvo5aA==", + "dev": true, + "requires": { + "@types/d3-dsv": "*" + } + }, + "@types/d3-force": { + "version": "3.0.10", + "resolved": "https://registry.npmjs.org/@types/d3-force/-/d3-force-3.0.10.tgz", + "integrity": "sha512-ZYeSaCF3p73RdOKcjj+swRlZfnYpK1EbaDiYICEEp5Q6sUiqFaFQ9qgoshp5CzIyyb/yD09kD9o2zEltCexlgw==", + "dev": true + }, + "@types/d3-format": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-format/-/d3-format-3.0.4.tgz", + "integrity": "sha512-fALi2aI6shfg7vM5KiR1wNJnZ7r6UuggVqtDA+xiEdPZQwy/trcQaHnwShLuLdta2rTymCNpxYTiMZX/e09F4g==", + "dev": true + }, + "@types/d3-geo": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@types/d3-geo/-/d3-geo-3.1.0.tgz", + "integrity": "sha512-856sckF0oP/diXtS4jNsiQw/UuK5fQG8l/a9VVLeSouf1/PPbBE1i1W852zVwKwYCBkFJJB7nCFTbk6UMEXBOQ==", + "dev": true, + "requires": { + "@types/geojson": "*" + } + }, + "@types/d3-hierarchy": { + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/@types/d3-hierarchy/-/d3-hierarchy-3.1.7.tgz", + "integrity": "sha512-tJFtNoYBtRtkNysX1Xq4sxtjK8YgoWUNpIiUee0/jHGRwqvzYxkq0hGVbbOGSz+JgFxxRu4K8nb3YpG3CMARtg==", + "dev": true + }, + "@types/d3-interpolate": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-interpolate/-/d3-interpolate-3.0.4.tgz", + "integrity": "sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==", + "dev": true, + "requires": { + "@types/d3-color": "*" + } + }, + "@types/d3-path": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@types/d3-path/-/d3-path-3.1.0.tgz", + "integrity": "sha512-P2dlU/q51fkOc/Gfl3Ul9kicV7l+ra934qBFXCFhrZMOL6du1TM0pm1ThYvENukyOn5h9v+yMJ9Fn5JK4QozrQ==", + "dev": true + }, + "@types/d3-polygon": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-polygon/-/d3-polygon-3.0.2.tgz", + "integrity": "sha512-ZuWOtMaHCkN9xoeEMr1ubW2nGWsp4nIql+OPQRstu4ypeZ+zk3YKqQT0CXVe/PYqrKpZAi+J9mTs05TKwjXSRA==", + "dev": true + }, + "@types/d3-quadtree": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@types/d3-quadtree/-/d3-quadtree-3.0.6.tgz", + "integrity": "sha512-oUzyO1/Zm6rsxKRHA1vH0NEDG58HrT5icx/azi9MF1TWdtttWl0UIUsjEQBBh+SIkrpd21ZjEv7ptxWys1ncsg==", + "dev": true + }, + "@types/d3-random": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/d3-random/-/d3-random-3.0.3.tgz", + "integrity": "sha512-Imagg1vJ3y76Y2ea0871wpabqp613+8/r0mCLEBfdtqC7xMSfj9idOnmBYyMoULfHePJyxMAw3nWhJxzc+LFwQ==", + "dev": true + }, + "@types/d3-scale": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/@types/d3-scale/-/d3-scale-4.0.8.tgz", + "integrity": "sha512-gkK1VVTr5iNiYJ7vWDI+yUFFlszhNMtVeneJ6lUTKPjprsvLLI9/tgEGiXJOnlINJA8FyA88gfnQsHbybVZrYQ==", + "dev": true, + "requires": { + "@types/d3-time": "*" + } + }, + "@types/d3-scale-chromatic": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/d3-scale-chromatic/-/d3-scale-chromatic-3.0.3.tgz", + "integrity": "sha512-laXM4+1o5ImZv3RpFAsTRn3TEkzqkytiOY0Dz0sq5cnd1dtNlk6sHLon4OvqaiJb28T0S/TdsBI3Sjsy+keJrw==", + "dev": true + }, + "@types/d3-selection": { + "version": "3.0.11", + "resolved": "https://registry.npmjs.org/@types/d3-selection/-/d3-selection-3.0.11.tgz", + "integrity": "sha512-bhAXu23DJWsrI45xafYpkQ4NtcKMwWnAC/vKrd2l+nxMFuvOT3XMYTIj2opv8vq8AO5Yh7Qac/nSeP/3zjTK0w==", + "dev": true + }, + "@types/d3-shape": { + "version": "3.1.6", + "resolved": "https://registry.npmjs.org/@types/d3-shape/-/d3-shape-3.1.6.tgz", + "integrity": "sha512-5KKk5aKGu2I+O6SONMYSNflgiP0WfZIQvVUMan50wHsLG1G94JlxEVnCpQARfTtzytuY0p/9PXXZb3I7giofIA==", + "dev": true, + "requires": { + "@types/d3-path": "*" + } + }, + "@types/d3-time": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/d3-time/-/d3-time-3.0.3.tgz", + "integrity": "sha512-2p6olUZ4w3s+07q3Tm2dbiMZy5pCDfYwtLXXHUnVzXgQlZ/OyPtUz6OL382BkOuGlLXqfT+wqv8Fw2v8/0geBw==", + "dev": true + }, + "@types/d3-time-format": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/@types/d3-time-format/-/d3-time-format-4.0.3.tgz", + "integrity": "sha512-5xg9rC+wWL8kdDj153qZcsJ0FWiFt0J5RB6LYUNZjwSnesfblqrI/bJ1wBdJ8OQfncgbJG5+2F+qfqnqyzYxyg==", + "dev": true + }, + "@types/d3-timer": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-timer/-/d3-timer-3.0.2.tgz", + "integrity": "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==", + "dev": true + }, + "@types/d3-transition": { + "version": "3.0.9", + "resolved": "https://registry.npmjs.org/@types/d3-transition/-/d3-transition-3.0.9.tgz", + "integrity": "sha512-uZS5shfxzO3rGlu0cC3bjmMFKsXv+SmZZcgp0KD22ts4uGXp5EVYGzu/0YdwZeKmddhcAccYtREJKkPfXkZuCg==", + "dev": true, + "requires": { + "@types/d3-selection": "*" + } + }, + "@types/d3-zoom": { + "version": "3.0.8", + "resolved": "https://registry.npmjs.org/@types/d3-zoom/-/d3-zoom-3.0.8.tgz", + "integrity": "sha512-iqMC4/YlFCSlO8+2Ii1GGGliCAY4XdeG748w5vQUbevlbDu0zSjH/+jojorQVBK/se0j6DUFNPBGSqD3YWYnDw==", + "dev": true, + "requires": { + "@types/d3-interpolate": "*", + "@types/d3-selection": "*" + } + }, "@types/eslint": { "version": "8.4.6", "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-8.4.6.tgz", diff --git a/package.json b/package.json index 31c79dd1e..8bbbccbc6 100644 --- a/package.json +++ b/package.json @@ -117,6 +117,7 @@ }, "devDependencies": { "@playwright/test": "^1.48.2", + "@types/d3": "^7.4.3", "@types/leaflet": "^1.9.3", "@types/node": "^18.15.11", "@types/webpack-env": "^1.18.2", diff --git a/src/actions/tree.js b/src/actions/tree.ts similarity index 72% rename from src/actions/tree.js rename to src/actions/tree.ts index fb891e642..321c68b61 100644 --- a/src/actions/tree.js +++ b/src/actions/tree.ts @@ -1,3 +1,4 @@ +import { AnyAction } from "@reduxjs/toolkit"; import { calcTipRadii } from "../util/tipRadiusHelpers"; import { strainNameToIdx, calculateVisiblityAndBranchThickness } from "../util/treeVisibilityHelpers"; import * as types from "./types"; @@ -10,7 +11,18 @@ import { createVisibleLegendValues, getLegendOrder } from "../util/colorScale"; import { getTraitFromNode } from "../util/treeMiscHelpers"; import { warningNotification } from "./notifications"; import { calcFullTipCounts, calcTipCounts } from "../util/treeCountingHelpers"; +import { PhyloNode } from "../components/tree/phyloTree/types"; +import { Metadata } from "../metadata"; +import { AppDispatch, RootState } from "../store"; +import { ReduxNode, TreeState } from "../reducers/tree/types"; +type RootIndex = number | undefined + +/** [root idx tree1, root idx tree2] */ +export type Root = [RootIndex, RootIndex] + +/** A function to be handled by redux (thunk) */ +type ThunkFunction = (dispatch: AppDispatch, getState: () => RootState) => void /** * Updates the `inView` property of nodes which depends on the currently selected @@ -18,10 +30,13 @@ import { calcFullTipCounts, calcTipCounts } from "../util/treeCountingHelpers"; * Note that this property is historically the remit of PhyloTree, however this function * may be called before those objects are created; in this case we store the property on * the tree node itself. - * @param {Int} idx - index of displayed root node - * @param {ReduxTreeState} tree */ -export const applyInViewNodesToTree = (idx, tree) => { +export const applyInViewNodesToTree = ( + /** index of displayed root node */ + idx: RootIndex, + + tree: TreeState, +): number => { const validIdxRoot = idx !== undefined ? idx : tree.idxOfInViewRootNode; if (tree.nodes[0].shell) { tree.nodes.forEach((d) => { @@ -29,12 +44,12 @@ export const applyInViewNodesToTree = (idx, tree) => { d.shell.update = true; }); if (tree.nodes[validIdxRoot].hasChildren) { - applyToChildren(tree.nodes[validIdxRoot].shell, (d) => {d.inView = true;}); + applyToChildren(tree.nodes[validIdxRoot].shell, (d: PhyloNode) => {d.inView = true;}); } else if (tree.nodes[validIdxRoot].parent.arrayIdx===0) { // subtree with n=1 tips => don't make the parent in-view as this will cover the entire tree! tree.nodes[validIdxRoot].shell.inView = true; } else { - applyToChildren(tree.nodes[validIdxRoot].parent.shell, (d) => {d.inView = true;}); + applyToChildren(tree.nodes[validIdxRoot].parent.shell, (d: PhyloNode) => {d.inView = true;}); } } else { /* FYI applyInViewNodesToTree is now setting inView on the redux nodes */ @@ -42,7 +57,7 @@ export const applyInViewNodesToTree = (idx, tree) => { d.inView = false; }); /* note that we cannot use `applyToChildren` as that operates on PhyloNodes */ - const _markChildrenInView = (node) => { + const _markChildrenInView = (node: ReduxNode) => { node.inView = true; if (node.children) { for (const child of node.children) _markChildrenInView(child); @@ -61,14 +76,21 @@ export const applyInViewNodesToTree = (idx, tree) => { * this fn relies on the "inView" attr of nodes * note that this function checks to see if the tree has been defined (different to if it's ready / loaded!) * for arg destructuring see https://simonsmith.io/destructuring-objects-as-function-parameters-in-es6/ - * @param {array|undefined} root Change the in-view part of the tree. [root idx tree1, root idx tree2]. - * [0, 0]: reset. [undefined, undefined]: do nothing - * @param {string | undefined} cladeSelected - * @return {function} a function to be handled by redux (thunk) */ -export const updateVisibleTipsAndBranchThicknesses = ( - {root = [undefined, undefined], cladeSelected = undefined} = {} -) => { +export const updateVisibleTipsAndBranchThicknesses = ({ + root = [undefined, undefined], + cladeSelected = undefined, +}: { + /** + * Change the in-view part of the tree. + * + * [0, 0]: reset. [undefined, undefined]: do nothing + */ + root?: Root + + cladeSelected?: string +} = {} +): ThunkFunction => { return (dispatch, getState) => { const { tree, treeToo, controls, frequencies } = getState(); if (root[0] === undefined && !cladeSelected && tree.selectedClade) { @@ -86,7 +108,7 @@ export const updateVisibleTipsAndBranchThicknesses = ( controls, {dateMinNumeric: controls.dateMinNumeric, dateMaxNumeric: controls.dateMaxNumeric} ); - const dispatchObj = { + const dispatchObj: AnyAction = { type: types.UPDATE_VISIBILITY_AND_BRANCH_THICKNESS, visibility: data.visibility, visibilityVersion: data.visibilityVersion, @@ -141,11 +163,17 @@ export const updateVisibleTipsAndBranchThicknesses = ( * date changes need to update tip visibility & branch thicknesses * this can be done in a single action * NB calling this without specifying newMin OR newMax is a no-op - * @param {string|false} newMin optional - * @param {string|false} newMax optional - * @return {null} side-effects: a single action + * side-effects: a single action */ -export const changeDateFilter = ({newMin = false, newMax = false, quickdraw = false}) => { +export const changeDateFilter = ({ + newMin = false, + newMax = false, + quickdraw = false, +}: { + newMin?: string | false + newMax?: string | false + quickdraw?: boolean +}): ThunkFunction => { return (dispatch, getState) => { const { tree, treeToo, controls, frequencies } = getState(); if (!tree.nodes) {return;} @@ -154,7 +182,7 @@ export const changeDateFilter = ({newMin = false, newMax = false, quickdraw = fa dateMaxNumeric: newMax ? calendarToNumeric(newMax) : controls.dateMaxNumeric }; const data = calculateVisiblityAndBranchThickness(tree, controls, dates); - const dispatchObj = { + const dispatchObj: AnyAction = { type: types.CHANGE_DATES_VISIBILITY_THICKNESS, quickdraw, dateMin: newMin ? newMin : controls.dateMin, @@ -199,18 +227,31 @@ export const changeDateFilter = ({newMin = false, newMax = false, quickdraw = fa /** * NB all params are optional - supplying none resets the tip radii to defaults - * @param {string|number} selectedLegendItem value of the attr. if scale is continuous a bound will be used. - * @param {int} tipSelectedIdx the strain to highlight (always tree 1) - * @param {array} geoFilter a filter to apply to the strains. Empty array or array of len 2. [0]: geoResolution, [1]: value to filter to - * @return {null} side-effects: a single action + * side-effects: a single action */ export const updateTipRadii = ( - {tipSelectedIdx = false, selectedLegendItem = false, geoFilter = [], searchNodes = false} = {} -) => { + { + tipSelectedIdx = false, + selectedLegendItem = false, + geoFilter = [], + searchNodes = false + }: { + /** the strain to highlight (always tree 1) */ + tipSelectedIdx?: number | false, + + /** value of the attr. if scale is continuous a bound will be used. */ + selectedLegendItem?: string | number | false, + + /** a filter to apply to the strains. Empty array or array of len 2. [0]: geoResolution, [1]: value to filter to */ + geoFilter?: [string, string] | [], + + searchNodes?: PhyloNode[] | false + } = {} +): ThunkFunction => { return (dispatch, getState) => { const { controls, tree, treeToo } = getState(); const colorScale = controls.colorScale; - const d = { + const d: AnyAction = { type: types.UPDATE_TIP_RADII, version: tree.tipRadiiVersion + 1 }; const tt = controls.showTreeToo; @@ -230,16 +271,22 @@ export const updateTipRadii = ( /** * Apply a filter to the current selection (i.e. filtered / "on" values associated with this trait) - * Explanation of the modes: - * "add" -> add the values to the current selection (if any exists). - * "inactivate" -> inactivate values (i.e. change active prop to false). To activate just use "add". - * "remove" -> remove the values from the current selection - * "set" -> set the values of the filter to be those provided. All disabled filters will be removed. XXX TODO. - * @param {string} mode allowed values: "set", "add", "remove" - * @param {string} trait the trait name of the filter ("authors", "country" etcetera) - * @param {Array of strings} values the values (see above) */ -export const applyFilter = (mode, trait, values) => { +export const applyFilter = ( + /** Explanation of the modes: + * - "add" -> add the values to the current selection (if any exists). + * - "inactivate" -> inactivate values (i.e. change active prop to false). To activate just use "add". + * - "remove" -> remove the values from the current selection + * - "set" -> set the values of the filter to be those provided. All disabled filters will be removed. XXX TODO. + */ + mode: "add" | "inactivate" | "remove" | "set", + + /** the trait name of the filter ("authors", "country" etcetera) */ + trait: string | symbol, + + /** the values (see above) */ + values: string[], +): ThunkFunction => { return (dispatch, getState) => { const { controls } = getState(); const currentlyFilteredTraits = Reflect.ownKeys(controls.filters); @@ -296,16 +343,15 @@ export const applyFilter = (mode, trait, values) => { }; }; -export const toggleTemporalConfidence = () => ({ +export const toggleTemporalConfidence = (): AnyAction => ({ type: types.TOGGLE_TEMPORAL_CONF }); /** * restore original state by iterating over all nodes and restoring children to unexplodedChildren (as necessary) - * @param {Array} nodes */ -const _resetExpodedTree = (nodes) => { +const _resetExpodedTree = (nodes: ReduxNode[]): void => { nodes.forEach((n) => { if (Object.prototype.hasOwnProperty.call(n, 'unexplodedChildren')) { n.children = n.unexplodedChildren; @@ -323,11 +369,17 @@ const _resetExpodedTree = (nodes) => { * create subtrees where branches have different attrs. * Note: because the children of a node may change, we store the previous (unexploded) children * as `unexplodedChildren` so we can return to the original tree. - * @param {Node} root - root node of entire tree - * @param {Node} node - current node being traversed - * @param {String} attr - trait name to determine if a child should become subtree */ -const _traverseAndCreateSubtrees = (root, node, attr) => { +const _traverseAndCreateSubtrees = ( + /** root node of entire tree */ + root: ReduxNode, + + /** current node being traversed */ + node: ReduxNode, + + /** trait name to determine if a child should become subtree */ + attr: string, +): void => { // store original children so we traverse the entire tree const originalChildren = node.hasChildren ? [...node.children] : []; @@ -363,7 +415,11 @@ const _traverseAndCreateSubtrees = (root, node, attr) => { /** * sort the subtrees by the order the trait would appear in the legend */ -const _orderSubtrees = (metadata, nodes, attr) => { +const _orderSubtrees = ( + metadata: Metadata, + nodes: ReduxNode[], + attr: string, +): void => { const attrValueOrder = getLegendOrder(attr, metadata.colorings[attr], nodes, undefined); nodes[0].children.sort((childA, childB) => { const [attrA, attrB] = [getTraitFromNode(childA, attr), getTraitFromNode(childB, attr)]; @@ -380,43 +436,47 @@ const _orderSubtrees = (metadata, nodes, attr) => { }); }; -export const explodeTree = (attr) => (dispatch, getState) => { - const {tree, metadata, controls} = getState(); - _resetExpodedTree(tree.nodes); // ensure we start with an unexploded tree - if (attr) { - const root = tree.nodes[0]; - _traverseAndCreateSubtrees(root, root, attr); - if (root.unexplodedChildren.length === root.children.length) { - dispatch(warningNotification({message: "Cannot explode tree on this trait - is it defined on internal nodes?"})); - return; +export const explodeTree = ( + attr: string | undefined, +): ThunkFunction => { + return (dispatch, getState) => { + const {tree, metadata, controls} = getState(); + _resetExpodedTree(tree.nodes); // ensure we start with an unexploded tree + if (attr) { + const root = tree.nodes[0]; + _traverseAndCreateSubtrees(root, root, attr); + if (root.unexplodedChildren.length === root.children.length) { + dispatch(warningNotification({message: "Cannot explode tree on this trait - is it defined on internal nodes?"})); + return; + } + _orderSubtrees(metadata, tree.nodes, attr); } - _orderSubtrees(metadata, tree.nodes, attr); - } - /* tree splitting necessitates recalculation of tip counts */ - calcFullTipCounts(tree.nodes[0]); - calcTipCounts(tree.nodes[0], tree.visibility); - /* we default to zooming out completely whenever we explode the tree. There are nicer behaviours here, - such as re-calculating the MRCA of visible nodes, but this comes at the cost of increased complexity. - Note that the functions called here involve a lot of code duplication and are good targets for refactoring */ - applyInViewNodesToTree(0, tree); - const visData = calculateVisiblityAndBranchThickness( - tree, - controls, - {dateMinNumeric: controls.dateMinNumeric, dateMaxNumeric: controls.dateMaxNumeric} - ); - visData.idxOfInViewRootNode = 0; - /* Changes in visibility require a recomputation of which legend items we wish to display */ - visData.visibleLegendValues = createVisibleLegendValues({ - colorBy: controls.colorBy, - genotype: controls.colorScale.genotype, - scaleType: controls.colorScale.scaleType, - legendValues: controls.colorScale.legendValues, - treeNodes: tree.nodes, - visibility: visData.visibility - }); - dispatch({ - type: types.CHANGE_EXPLODE_ATTR, - explodeAttr: attr, - ...visData - }); + /* tree splitting necessitates recalculation of tip counts */ + calcFullTipCounts(tree.nodes[0]); + calcTipCounts(tree.nodes[0], tree.visibility); + /* we default to zooming out completely whenever we explode the tree. There are nicer behaviours here, + such as re-calculating the MRCA of visible nodes, but this comes at the cost of increased complexity. + Note that the functions called here involve a lot of code duplication and are good targets for refactoring */ + applyInViewNodesToTree(0, tree); + const visData = calculateVisiblityAndBranchThickness( + tree, + controls, + {dateMinNumeric: controls.dateMinNumeric, dateMaxNumeric: controls.dateMaxNumeric} + ); + visData.idxOfInViewRootNode = 0; + /* Changes in visibility require a recomputation of which legend items we wish to display */ + visData.visibleLegendValues = createVisibleLegendValues({ + colorBy: controls.colorBy, + genotype: controls.colorScale.genotype, + scaleType: controls.colorScale.scaleType, + legendValues: controls.colorScale.legendValues, + treeNodes: tree.nodes, + visibility: visData.visibility + }); + dispatch({ + type: types.CHANGE_EXPLODE_ATTR, + explodeAttr: attr, + ...visData + }); + }; }; diff --git a/src/components/tree/index.ts b/src/components/tree/index.ts index 9f0cc1d2c..10de934bc 100644 --- a/src/components/tree/index.ts +++ b/src/components/tree/index.ts @@ -1,8 +1,11 @@ -import { connect } from "react-redux"; +import { connect, MapStateToProps } from "react-redux"; import UnconnectedTree from "./tree"; import { RootState } from "../../store"; +import { TreeComponentOwnProps, TreeComponentStateProps } from "./types"; -const Tree = connect((state: RootState) => ({ +const mapStateToProps: MapStateToProps = ( + state: RootState, +): TreeComponentStateProps => ({ tree: state.tree, treeToo: state.treeToo, selectedNode: state.controls.selectedNode, @@ -32,6 +35,8 @@ const Tree = connect((state: RootState) => ({ animationPlayPauseButton: state.controls.animationPlayPauseButton, showOnlyPanels: state.controls.showOnlyPanels, performanceFlags: state.controls.performanceFlags, -}))(UnconnectedTree); +}); + +const Tree = connect(mapStateToProps)(UnconnectedTree); export default Tree; diff --git a/src/components/tree/phyloTree/change.js b/src/components/tree/phyloTree/change.ts similarity index 72% rename from src/components/tree/phyloTree/change.js rename to src/components/tree/phyloTree/change.ts index 1a0b6bedc..e3bbbfad4 100644 --- a/src/components/tree/phyloTree/change.js +++ b/src/components/tree/phyloTree/change.ts @@ -1,3 +1,4 @@ +import { Selection, Transition } from "d3"; import { timerFlush } from "d3-timer"; import { calcConfidenceWidth } from "./confidence"; import { applyToChildren, setDisplayOrder } from "./helpers"; @@ -6,11 +7,15 @@ import { NODE_VISIBLE } from "../../../util/globals"; import { getBranchVisibility, strokeForBranch } from "./renderers"; import { shouldDisplayTemporalConfidence } from "../../../reducers/controls"; import { makeTipLabelFunc } from "./labels"; +import { ChangeParams, PhyloNode, PhyloTreeType, PropsForPhyloNodes, SVGProperty, TreeElement } from "./types"; /* loop through the nodes and update each provided prop with the new value * additionally, set d.update -> whether or not the node props changed */ -const updateNodesWithNewData = (nodes, newNodeProps) => { +const updateNodesWithNewData = ( + nodes: PhyloNode[], + newNodeProps: PropsForPhyloNodes, +): void => { // console.log("update nodes with data for these keys:", Object.keys(newNodeProps)); // let tmp = 0; nodes.forEach((d, i) => { @@ -35,72 +40,88 @@ const updateNodesWithNewData = (nodes, newNodeProps) => { const svgSetters = { attrs: { ".tip": { - r: (d) => d.r, - cx: (d) => d.xTip, - cy: (d) => d.yTip + r: (d: PhyloNode) => d.r, + cx: (d: PhyloNode) => d.xTip, + cy: (d: PhyloNode) => d.yTip }, ".branch": { }, ".vaccineCross": { - d: (d) => d.vaccineCross + d: (d: PhyloNode) => d.vaccineCross }, ".conf": { - d: (d) => d.confLine + d: (d: PhyloNode) => d.confLine } }, styles: { ".tip": { - fill: (d) => d.fill, - stroke: (d) => d.tipStroke, - visibility: (d) => d.visibility === NODE_VISIBLE ? "visible" : "hidden" + fill: (d: PhyloNode) => d.fill, + stroke: (d: PhyloNode) => d.tipStroke, + visibility: (d: PhyloNode) => d.visibility === NODE_VISIBLE ? "visible" : "hidden" }, ".conf": { - stroke: (d) => d.branchStroke, + stroke: (d: PhyloNode) => d.branchStroke, "stroke-width": calcConfidenceWidth }, // only allow stroke to be set on individual branches ".branch": { - "stroke-width": (d) => d["stroke-width"] + "px", // style - as per drawBranches() - stroke: (d) => strokeForBranch(d), // TODO: revisit if we bring back SVG gradients - cursor: (d) => d.visibility === NODE_VISIBLE ? "pointer" : "default", + "stroke-width": (d: PhyloNode) => d["stroke-width"] + "px", // style - as per drawBranches() + stroke: (d: PhyloNode) => strokeForBranch(d), // TODO: revisit if we bring back SVG gradients + cursor: (d: PhyloNode) => d.visibility === NODE_VISIBLE ? "pointer" : "default", visibility: getBranchVisibility } } }; +type SelectionOrTransition = + Selection | + Transition + +type UpdateCall = (selectionOrTransition: SelectionOrTransition) => void; + + /** createUpdateCall * returns a function which can be called as part of a D3 chain in order to modify * the SVG elements. * svgSetters (see above) are used to actually modify the property on the element, * so the given property must also be present there! - * @param {string} treeElem (e.g. ".tip" or ".branch") - * @param {list} properties (e.g. ["visibiliy", "stroke-width"]) - * @return {function} used in a d3 selection, i.e. d3.selection().methods().call(X) */ -const createUpdateCall = (treeElem, properties) => (selection) => { - // First: the properties to update via d3Selection.attr call - if (svgSetters.attrs[treeElem]) { - [...properties].filter((x) => svgSetters.attrs[treeElem][x]) - .forEach((attrName) => { - // console.log(`applying attr ${attrName} to ${treeElem}`) - selection.attr(attrName, svgSetters.attrs[treeElem][attrName]); - }); - } - // Second: the properties to update via d3Selection.style call - if (svgSetters.styles[treeElem]) { - [...properties].filter((x) => svgSetters.styles[treeElem][x]) - .forEach((styleName) => { - // console.log(`applying style ${styleName} to ${treeElem}`) - selection.style(styleName, svgSetters.styles[treeElem][styleName]); - }); - } -}; +function createUpdateCall( + treeElem: TreeElement, -const genericSelectAndModify = (svg, treeElem, updateCall, transitionTime) => { + /** e.g. ["visibility", "stroke-width"] */ + properties: Set, +): UpdateCall { + return (selection) => { + // First: the properties to update via d3Selection.attr call + if (svgSetters.attrs[treeElem]) { + [...properties].filter((x) => svgSetters.attrs[treeElem][x]) + .forEach((attrName) => { + // console.log(`applying attr ${attrName} to ${treeElem}`) + selection.attr(attrName, svgSetters.attrs[treeElem][attrName]); + }); + } + // Second: the properties to update via d3Selection.style call + if (svgSetters.styles[treeElem]) { + [...properties].filter((x) => svgSetters.styles[treeElem][x]) + .forEach((styleName) => { + // console.log(`applying style ${styleName} to ${treeElem}`) + selection.style(styleName, svgSetters.styles[treeElem][styleName]); + }); + } + }; +} + +const genericSelectAndModify = ( + svg: Selection, + treeElem: TreeElement, + updateCall: UpdateCall, + transitionTime: number, +): void => { // console.log("general svg update for", treeElem); - let selection = svg.selectAll(treeElem) - .filter((d) => d.update); + let selection: SelectionOrTransition = svg.selectAll(treeElem) + .filter((d: PhyloNode) => d.update); if (transitionTime) { selection = selection.transition().duration(transitionTime); } @@ -113,14 +134,20 @@ const genericSelectAndModify = (svg, treeElem, updateCall, transitionTime) => { * @transitionTime {INT} - in ms. if 0 then no transition (timerFlush is used) * @extras {dict} - extra keywords to tell this function to call certain phyloTree update methods. In flux. */ -export const modifySVG = function modifySVG(elemsToUpdate, svgPropsToUpdate, transitionTime, extras) { - let updateCall; - const classesToPotentiallyUpdate = [".tip", ".vaccineDottedLine", ".vaccineCross", ".branch"]; /* order is respected */ +export const modifySVG = function modifySVG( + this: PhyloTreeType, + elemsToUpdate: Set, + svgPropsToUpdate: Set, + transitionTime: number, + extras: Extras, +): void { + let updateCall: UpdateCall; + const classesToPotentiallyUpdate: TreeElement[] = [".tip", ".vaccineDottedLine", ".vaccineCross", ".branch"]; /* order is respected */ /* treat stem / branch specially, but use these to replace a normal .branch call if that's also to be applied */ if (elemsToUpdate.has(".branch.S") || elemsToUpdate.has(".branch.T")) { const applyBranchPropsAlso = elemsToUpdate.has(".branch"); if (applyBranchPropsAlso) classesToPotentiallyUpdate.splice(classesToPotentiallyUpdate.indexOf(".branch"), 1); - const ST = [".S", ".T"]; + const ST: Array<".S" | ".T"> = [".S", ".T"]; ST.forEach((x, STidx) => { if (elemsToUpdate.has(`.branch${x}`)) { if (applyBranchPropsAlso) { @@ -196,7 +223,14 @@ export const modifySVG = function modifySVG(elemsToUpdate, svgPropsToUpdate, tra * step 2: when step 1 has finished, move tips across the screen. * step 3: when step 2 has finished, redraw everything. No transition here. */ -export const modifySVGInStages = function modifySVGInStages(elemsToUpdate, svgPropsToUpdate, transitionTimeFadeOut, transitionTimeMoveTips, extras) { +export const modifySVGInStages = function modifySVGInStages( + this: PhyloTreeType, + elemsToUpdate: Set, + svgPropsToUpdate: Set, + transitionTimeFadeOut: number, + transitionTimeMoveTips: number, + extras: Extras, +): void { elemsToUpdate.delete(".tip"); this.hideGrid(); let inProgress = 0; /* counter of transitions currently in progress */ @@ -236,46 +270,55 @@ export const modifySVGInStages = function modifySVGInStages(elemsToUpdate, svgPr }; +interface Extras { + removeConfidences: boolean + showConfidences: boolean + newBranchLabellingKey?: string + + timeSliceHasPotentiallyChanged?: boolean + hideTipLabels?: boolean +} + + /* the main interface to changing a currently rendered tree. * simply call change and tell it what should be changed. * try to do a single change() call with as many things as possible in it */ -export const change = function change({ - /* booleans for what should be changed */ - changeColorBy = false, - changeVisibility = false, - changeTipRadii = false, - changeBranchThickness = false, - showConfidences = false, - removeConfidences = false, - zoomIntoClade = false, - svgHasChangedDimensions = false, - animationInProgress = false, - changeNodeOrder = false, - /* change these things to provided value (unless undefined) */ - newDistance = undefined, - newLayout = undefined, - updateLayout = undefined, // todo - this seems identical to `newLayout` - newBranchLabellingKey = undefined, - showAllBranchLabels = undefined, - newTipLabelKey = undefined, - /* arrays of data (the same length as nodes) */ - branchStroke = undefined, - tipStroke = undefined, - fill = undefined, - visibility = undefined, - tipRadii = undefined, - branchThickness = undefined, - /* other data */ - focus = undefined, - scatterVariables = undefined, - performanceFlags = {}, -}) { +export const change = function change( + this: PhyloTreeType, + { + changeColorBy = false, + changeVisibility = false, + changeTipRadii = false, + changeBranchThickness = false, + showConfidences = false, + removeConfidences = false, + zoomIntoClade = false, + svgHasChangedDimensions = false, + animationInProgress = false, + changeNodeOrder = false, + newDistance = undefined, + newLayout = undefined, + updateLayout = undefined, + newBranchLabellingKey = undefined, + showAllBranchLabels = undefined, + newTipLabelKey = undefined, + branchStroke = undefined, + tipStroke = undefined, + fill = undefined, + visibility = undefined, + tipRadii = undefined, + branchThickness = undefined, + focus = undefined, + scatterVariables = undefined, + performanceFlags = {}, + }: ChangeParams +): void { // console.log("\n** phylotree.change() (time since last run:", Date.now() - this.timeLastRenderRequested, "ms) **\n\n"); timerStart("phylotree.change()"); - const elemsToUpdate = new Set(); /* what needs updating? E.g. ".branch", ".tip" etc */ - 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 elemsToUpdate = new Set(); /* 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(); /* which SVG properties shall be changed. E.g. "fill", "stroke" */ const useModifySVGInStages = newLayout; /* use modifySVGInStages rather than modifySVG. Not used often. */ @@ -350,7 +393,7 @@ export const change = function change({ this.zoomNode = zoomIntoClade.n.hasChildren ? zoomIntoClade : zoomIntoClade.n.parent.shell; - applyToChildren(this.zoomNode, (d) => {d.inView = true;}); + applyToChildren(this.zoomNode, (d: PhyloNode) => {d.inView = true;}); } if (svgHasChangedDimensions || changeNodeOrder) { this.nodes.forEach((d) => {d.update = true;}); @@ -392,7 +435,7 @@ export const change = function change({ elemsToUpdate.add('.tipLabel'); /* will trigger d3 commands as required */ } - const extras = { removeConfidences, showConfidences, newBranchLabellingKey }; + const extras: Extras = { removeConfidences, showConfidences, newBranchLabellingKey }; extras.timeSliceHasPotentiallyChanged = changeVisibility || newDistance; extras.hideTipLabels = animationInProgress || newTipLabelKey === 'none'; if (useModifySVGInStages) { diff --git a/src/components/tree/phyloTree/defaultParams.js b/src/components/tree/phyloTree/defaultParams.ts similarity index 92% rename from src/components/tree/phyloTree/defaultParams.js rename to src/components/tree/phyloTree/defaultParams.ts index b758e4cb0..beba4a81a 100644 --- a/src/components/tree/phyloTree/defaultParams.js +++ b/src/components/tree/phyloTree/defaultParams.ts @@ -1,6 +1,7 @@ import { dataFont, darkGrey } from "../../../globalStyles"; +import { Params } from "./types"; -export const createDefaultParams = () => ({ +export const createDefaultParams = (): Params => ({ regressionStroke: darkGrey, regressionWidth: 6, majorGridStroke: "#DDD", diff --git a/src/components/tree/phyloTree/helpers.js b/src/components/tree/phyloTree/helpers.ts similarity index 84% rename from src/components/tree/phyloTree/helpers.js rename to src/components/tree/phyloTree/helpers.ts index 82a13684e..32c59cc57 100644 --- a/src/components/tree/phyloTree/helpers.js +++ b/src/components/tree/phyloTree/helpers.ts @@ -3,11 +3,16 @@ import { max } from "d3-array"; import {getTraitFromNode, getDivFromNode, getBranchMutations} from "../../../util/treeMiscHelpers"; import { NODE_VISIBLE } from "../../../util/globals"; import { timerStart, timerEnd } from "../../../util/perf"; +import { ReduxNode } from "../../../reducers/tree/types"; +import { Distance, PhyloNode } from "./types"; /** get a string to be used as the DOM element ID * Note that this cannot have any "special" characters */ -export const getDomId = (type, strain) => { +export const getDomId = ( + type: string, + strain: string, +): string => { // Replace non-alphanumeric characters with dashes (probably unnecessary) const name = `${type}_${strain}`.replace(/(\W+)/g, '-'); return CSS.escape(name); @@ -16,10 +21,13 @@ export const getDomId = (type, strain) => { /** * this function takes a call back and applies it recursively * to all child nodes, including internal nodes - * @param {PhyloNode} node - * @param {Function} func - function to apply to each children. Is passed a single argument, the of the children. */ -export const applyToChildren = (phyloNode, func) => { +export const applyToChildren = ( + phyloNode: PhyloNode, + + /** function to apply to each child. Is passed a single argument, the of the children. */ + func: (node: PhyloNode) => void, +): void => { func(phyloNode); const node = phyloNode.n; if ((!node.hasChildren) || (node.children === undefined)) { // in case clade set by URL, terminal hasn't been set yet! @@ -34,13 +42,14 @@ export const applyToChildren = (phyloNode, func) => { * Calculates the display order of all nodes, which corresponds to the vertical position * of nodes in a rectangular tree. * If `yCounter` is undefined then we wish to hide the node and all descendants of it - * @param {PhyloNode} node - * @param {function} incrementer - * @param {int|undefined} yCounter * @sideeffect modifies node.displayOrder and node.displayOrderRange - * @returns {int|undefined} current yCounter after assignment to the tree originating from `node` + * Returns the current yCounter after assignment to the tree originating from `node` */ -export const setDisplayOrderRecursively = (node, incrementer, yCounter) => { +export const setDisplayOrderRecursively = ( + node: PhyloNode, + incrementer: (node: PhyloNode) => number, + yCounter?: number, +): number | undefined => { const children = node.n.children; // (redux) tree node if (children && children.length) { for (let i = children.length - 1; i >= 0; i--) { @@ -65,7 +74,10 @@ export const setDisplayOrderRecursively = (node, incrementer, yCounter) => { * the returned value is to be interpreted as a count of the number of tips that would * otherwise fit in the gap */ -function _getSpaceBetweenSubtrees(numSubtrees, numTips) { +function _getSpaceBetweenSubtrees( + numSubtrees: number, + numTips: number, +): number { if (numSubtrees===1 || numTips<10) { return 0; } @@ -83,12 +95,14 @@ function _getSpaceBetweenSubtrees(numSubtrees, numTips) { * PhyloTree can subsequently use this information. Accessed by prototypes * rectangularLayout, radialLayout, createChildrenAndParents * side effects: .displayOrder (i.e. in the redux node) and .displayOrderRange - * @param {Object} props - * @param {Array} props.nodes - * @param {boolean} props.focus - * @returns {undefined} */ -export const setDisplayOrder = ({nodes, focus}) => { +export const setDisplayOrder = ({ + nodes, + focus, +}: { + nodes: PhyloNode[] + focus: boolean +}): void => { timerStart("setDisplayOrder"); const numSubtrees = nodes[0].n.children.filter((n) => n.fullTipCount!==0).length; @@ -143,7 +157,7 @@ export const setDisplayOrder = ({nodes, focus}) => { }; -export const formatDivergence = (divergence) => { +export const formatDivergence = (divergence: number): string | number => { return divergence > 1 ? Math.round((divergence + Number.EPSILON) * 1000) / 1000 : divergence > 0.01 ? @@ -156,7 +170,7 @@ export const formatDivergence = (divergence) => { * This differs depending on which tree is in view so it's helpful to access it * by reaching into phyotree to get it */ -export const getIdxOfInViewRootNode = (node) => { +export const getIdxOfInViewRootNode = (node: ReduxNode): number => { return node.shell.that.zoomNode.n.arrayIdx; }; @@ -164,7 +178,11 @@ export const getIdxOfInViewRootNode = (node) => { * Are the provided nodes within some divergence / time of each other? * NOTE: `otherNode` is always closer to the root in the tree than `node` */ -function isWithinBranchTolerance(node, otherNode, distanceMeasure) { +function isWithinBranchTolerance( + node: ReduxNode, + otherNode: ReduxNode, + distanceMeasure: Distance, +): boolean { if (distanceMeasure === "num_date") { /* We calculate the threshold by reaching into phylotree to extract the date range of the dataset and then split the data into ~50 slices. This could be refactored to not reach into phylotree. */ @@ -183,7 +201,7 @@ function isWithinBranchTolerance(node, otherNode, distanceMeasure) { * Walk up the tree from node until we find either a node which has a nucleotide mutation or we * reach the root of the (sub)tree. Gaps, deletions and undeletions do not count as mutations here. */ -function findFirstBranchWithAMutation(node) { +function findFirstBranchWithAMutation(node: ReduxNode): ReduxNode { if (node.parent === node) { return node; } @@ -200,13 +218,12 @@ function findFirstBranchWithAMutation(node) { * branch length threshold (either divergence or time). This is useful for finding the node * beyond a polytomy, or polytomy-like structure. If nucleotide mutations are defined on * the tree (and distanceMeasure=div) then we find the first branch with a mutation. - * @param {object} node - tree node - * @param {string} distanceMeasure -- 'num_date' or 'div' - * @param {object} observedMutations - * @returns {object} the closest node up the tree (towards the root) which is beyond - * some threshold */ -export const getParentBeyondPolytomy = (node, distanceMeasure, observedMutations) => { +export const getParentBeyondPolytomy = ( + node: ReduxNode, + distanceMeasure: Distance, + observedMutations: Record, +): ReduxNode => { let potentialNode = node.parent; if (distanceMeasure==="div" && areNucleotideMutationsPresent(observedMutations)) { return findFirstBranchWithAMutation(node); @@ -235,7 +252,9 @@ function areNucleotideMutationsPresent(observedMutations) { * we will "guess" this here. A future augur update will export this in a JSON key, * removing the need to guess. */ -export function guessAreMutationsPerSite(scale) { +export function guessAreMutationsPerSite( + scale: d3.ScaleContinuousNumeric, +): boolean { const maxDivergence = max(scale.domain()); return maxDivergence <= 5; } @@ -243,19 +262,15 @@ export function guessAreMutationsPerSite(scale) { /** * Is the node a subtree root node? (implies that we have either exploded trees or * the dataset has multiple subtrees to display) - * @param {ReduxTreeNode} n - * @returns {bool} */ -const isSubtreeRoot = (n) => (n.parent.name === "__ROOT" && n.parentInfo.original); +const isSubtreeRoot = (n: ReduxNode): boolean => (n.parent.name === "__ROOT" && n.parentInfo.original); /** * Gets the parent node to be used for stem / branch calculation. * Most of the time this is the same as `d.n.parent` however it is not in the * case of the root nodes for subtrees (e.g. exploded trees). - * @param {Node} n - * @returns {Node} */ -export const stemParent = (n) => { +export const stemParent = (n: ReduxNode): ReduxNode => { return isSubtreeRoot(n) ? n.parentInfo.original : n.parent; }; @@ -265,13 +280,13 @@ export const stemParent = (n) => { * This is not strictly the same as the `displayOrder` the scatterplot axis * renders increasing values going upwards (i.e. from the bottom to top of screen) * whereas the rectangular tree renders zero at the top and goes downwards - * @param {Array} nodes - * @returns {function} which takes a single argument of type */ -export const nodeOrdering = (nodes) => { +export const nodeOrdering = ( + nodes: PhyloNode[], +): ((d: PhyloNode) => [number, number]) => { const maxVal = nodes.map((d) => d.displayOrder) .reduce((acc, val) => ((val ?? 0) > acc ? val : acc), 0); - return (d) => ([ + return (d: PhyloNode) => ([ maxVal - d.displayOrder, isSubtreeRoot(d.n) ? undefined : (maxVal - d.n.parent.shell.displayOrder) ]); diff --git a/src/components/tree/phyloTree/layouts.js b/src/components/tree/phyloTree/layouts.ts similarity index 92% rename from src/components/tree/phyloTree/layouts.js rename to src/components/tree/phyloTree/layouts.ts index 0c142d2cf..ac499a217 100644 --- a/src/components/tree/phyloTree/layouts.js +++ b/src/components/tree/phyloTree/layouts.ts @@ -6,14 +6,19 @@ import { timerStart, timerEnd } from "../../../util/perf"; import { getTraitFromNode, getDivFromNode } from "../../../util/treeMiscHelpers"; import { stemParent, nodeOrdering } from "./helpers"; import { numDate } from "../../../util/colorHelpers"; +import { Layout, ScatterVariables } from "../../../reducers/controls"; +import { ReduxNode } from "../../../reducers/tree/types"; +import { Distance, Params, PhyloNode, PhyloTreeType } from "./types"; /** * assigns the attribute this.layout and calls the function that * calculates the x,y coordinates for the respective layouts - * @param layout -- the layout to be used, has to be one of - * ["rect", "radial", "unrooted", "clock", "scatter"] */ -export const setLayout = function setLayout(layout, scatterVariables) { +export const setLayout = function setLayout( + this: PhyloTreeType, + layout?: Layout, + scatterVariables?: ScatterVariables, +): void { // console.log("set layout"); timerStart("setLayout"); if (typeof layout === "undefined" || layout !== this.layout) { @@ -51,9 +56,8 @@ export const setLayout = function setLayout(layout, scatterVariables) { /** * assignes x,y coordinates for a rectangular layout - * @return {null} */ -export const rectangularLayout = function rectangularLayout() { +export const rectangularLayout = function rectangularLayout(this: PhyloTreeType): void { this.nodes.forEach((d) => { d.y = d.displayOrder; // precomputed y-values d.x = d.depth; // depth according to current distance @@ -73,7 +77,7 @@ export const rectangularLayout = function rectangularLayout() { * assign x,y coordinates for nodes based upon user-selected variables * TODO: timeVsRootToTip is a specific instance of this */ -export const scatterplotLayout = function scatterplotLayout() { +export const scatterplotLayout = function scatterplotLayout(this: PhyloTreeType): void { if (!this.scatterVariables) { console.error("Scatterplot called without variables"); return; @@ -83,7 +87,7 @@ export const scatterplotLayout = function scatterplotLayout() { nodeOrdering(this.nodes) : undefined; - this.nodes.forEach((d) => { + for (const d of this.nodes) { // set x and parent X values if (this.scatterVariables.x==="div") { d.x = getDivFromNode(d.n); @@ -116,7 +120,7 @@ export const scatterplotLayout = function scatterplotLayout() { [d.y, d.py] = [numDate(d.y, true), numDate(d.py, true)] } } - }); + } if (this.vaccines) { /* overlay vaccine cross on tip */ this.vaccines.forEach((d) => { @@ -134,17 +138,18 @@ export const scatterplotLayout = function scatterplotLayout() { /** * Utility function for the unrooted tree layout. See `unrootedLayout` for details. - * @param {PhyloNode} node - * @param {number} totalLeafWeight */ -const unrootedPlaceSubtree = (node, totalLeafWeight) => { +const unrootedPlaceSubtree = ( + node: PhyloNode, + totalLeafWeight: number, +): void => { const branchLength = node.depth - node.pDepth; node.x = node.px + branchLength * Math.cos(node.tau + node.w * 0.5); node.y = node.py + branchLength * Math.sin(node.tau + node.w * 0.5); let eta = node.tau; // eta is the cumulative angle for the wedges in the layout if (node.n.hasChildren) { for (let i = 0; i < node.n.children.length; i++) { - const ch = node.n.children[i].shell; // ch is a + const ch = node.n.children[i].shell; ch.w = 2 * Math.PI * leafWeight(ch.n) / totalLeafWeight; ch.tau = eta; eta += ch.w; @@ -163,9 +168,8 @@ const unrootedPlaceSubtree = (node, totalLeafWeight) => { /** * calculates x,y coordinates for the unrooted layout. this is * done recursively via a the function unrootedPlaceSubtree - * @return {null} */ -export const unrootedLayout = function unrootedLayout() { +export const unrootedLayout = function unrootedLayout(this: PhyloTreeType): void { /* the angle of a branch (i.e. the line leading to the node) is `tau + 0.5*w` `tau` stores the previous angle which has been used `w` is a measurement of the angle occupied by the clade defined by this node @@ -213,9 +217,8 @@ export const unrootedLayout = function unrootedLayout() { * calculates and assigns x,y coordinates for the radial layout. * in addition to x,y, this calculates the end-points of the radial * arcs and whether that arc is more than pi or not - * @return {null} */ -export const radialLayout = function radialLayout() { +export const radialLayout = function radialLayout(this: PhyloTreeType): void { const maxDisplayOrder = Math.max(...this.nodes.map((d) => d.displayOrder).filter((val) => val)); const offset = this.nodes[0].depth; this.nodes.forEach((d) => { @@ -251,7 +254,10 @@ export const radialLayout = function radialLayout() { * calculate coordinates. Parent depth is assigned as well. * @sideEffect sets this.distance -> "div" or "num_date" */ -export const setDistance = function setDistance(distanceAttribute) { +export const setDistance = function setDistance( + this: PhyloTreeType, + distanceAttribute?: Distance, +): void { timerStart("setDistance"); this.nodes.forEach((d) => {d.update = true;}); if (distanceAttribute) { @@ -291,9 +297,8 @@ export const setDistance = function setDistance(distanceAttribute) { /** * Initializes and sets the range of the scales (this.xScale, this.yScale) * which are used to map the x,y coordinates to the screen - * @param {margins} -- object with "right, left, top, bottom" margins */ -export const setScales = function setScales() { +export const setScales = function setScales(this: PhyloTreeType): void { if (this.layout==="scatter" && !this.scatterVariables.xContinuous) { this.xScale = scalePoint().round(false).align(0.5).padding(0.5); @@ -336,9 +341,8 @@ export const setScales = function setScales() { /** * this function sets the xScale, yScale domains and maps precalculated x,y * coordinates to their places on the screen -* @return {null} */ -export const mapToScreen = function mapToScreen() { +export const mapToScreen = function mapToScreen(this: PhyloTreeType): void { timerStart("mapToScreen"); const inViewTerminalNodes = this.nodes.filter((d) => !d.n.hasChildren).filter((d) => d.inView); @@ -518,7 +522,10 @@ export const mapToScreen = function mapToScreen() { const JITTER_MIN_STEP_SIZE = 50; // pixels -function padCategoricalScales(domain, scale) { +function padCategoricalScales( + domain: string[], + scale: d3.ScalePoint, +): d3.ScalePoint { if (scale.step() > JITTER_MIN_STEP_SIZE) return scale.padding(0.5); // balanced padding when we can jitter if (domain.length<=4) return scale.padding(0.4); if (domain.length<=6) return scale.padding(0.3); @@ -529,16 +536,20 @@ function padCategoricalScales(domain, scale) { /** * Add jitter to the already-computed node positions. */ -function jitter(axis, scale, nodes) { +function jitter( + axis: "x" | "y", + scale: d3.ScalePoint, + nodes: PhyloNode[], +): void { const step = scale.step(); if (scale.step() <= JITTER_MIN_STEP_SIZE) return; - const rand = []; // pre-compute a small set of pseudo random numbers for speed + const rand: number[] = []; // pre-compute a small set of pseudo random numbers for speed for (let i=1e2; i--;) { rand.push((Math.random()-0.5)*step*0.5); // occupy 50% } const [base, tip, randLen] = [`${axis}Base`, `${axis}Tip`, rand.length]; let j = 0; - function recurse(phyloNode) { + function recurse(phyloNode: PhyloNode): void { phyloNode[base] = stemParent(phyloNode.n).shell[tip]; phyloNode[tip] += rand[j++]; if (j>=randLen) j=0; @@ -549,7 +560,10 @@ function jitter(axis, scale, nodes) { } -function getTipLabelPadding(params, inViewTerminalNodes) { +function getTipLabelPadding( + params: Params, + inViewTerminalNodes: PhyloNode[], +): number { let padBy = 0; if (inViewTerminalNodes.length < params.tipLabelBreakL1) { @@ -570,6 +584,6 @@ function getTipLabelPadding(params, inViewTerminalNodes) { return padBy; } -function leafWeight(node) { +function leafWeight(node: ReduxNode): number { return node.tipCount + 0.15*(node.fullTipCount-node.tipCount); } diff --git a/src/components/tree/phyloTree/phyloTree.js b/src/components/tree/phyloTree/phyloTree.ts similarity index 92% rename from src/components/tree/phyloTree/phyloTree.js rename to src/components/tree/phyloTree/phyloTree.ts index 9c1129c02..79c2a0f19 100644 --- a/src/components/tree/phyloTree/phyloTree.js +++ b/src/components/tree/phyloTree/phyloTree.ts @@ -1,5 +1,7 @@ +import { ReduxNode } from "../../../reducers/tree/types"; import { createDefaultParams } from "./defaultParams"; import { change, modifySVG, modifySVGInStages } from "./change"; +import { PhyloNode, PhyloTreeType } from "./types"; /* PROTOTYPES */ import * as renderers from "./renderers"; @@ -10,7 +12,12 @@ import * as labels from "./labels"; import * as regression from "./regression"; /* phylogenetic tree drawing function - the actual tree is rendered by the render prototype */ -const PhyloTree = function PhyloTree(reduxNodes, id, idxOfInViewRootNode) { +const PhyloTree = function PhyloTree( + this: PhyloTreeType, + reduxNodes: ReduxNode[], + id: string, + idxOfInViewRootNode: number, +): void { this.grid = false; this.attributes = ['r', 'cx', 'cy', 'id', 'class', 'd']; this.params = createDefaultParams(); @@ -24,14 +31,14 @@ const PhyloTree = function PhyloTree(reduxNodes, id, idxOfInViewRootNode) { -- this.nodes[i].n = reduxNodes[i] -- reduxNodes[i].shell = this.nodes[i] */ this.nodes = reduxNodes.map((d) => { - const phyloNode = { + const phyloNode: PhyloNode = { that: this, - n: d, /* a back link to the redux node */ + n: d, x: 0, y: 0, inView: d.inView !== undefined ? d.inView : true /* each node is visible, unless set earlier! */ }; - d.shell = phyloNode; /* set the link from the redux node to the phylotree node */ + d.shell = phyloNode; return phyloNode; }); this.zoomNode = this.nodes[idxOfInViewRootNode]; diff --git a/src/components/tree/phyloTree/regression.js b/src/components/tree/phyloTree/regression.ts similarity index 85% rename from src/components/tree/phyloTree/regression.js rename to src/components/tree/phyloTree/regression.ts index effcc84d1..aff531295 100644 --- a/src/components/tree/phyloTree/regression.js +++ b/src/components/tree/phyloTree/regression.ts @@ -1,6 +1,7 @@ import { sum } from "d3-array"; import { formatDivergence, guessAreMutationsPerSite} from "./helpers"; import { NODE_VISIBLE } from "../../../util/globals"; +import { PhyloNode, PhyloTreeType, Regression } from "./types"; /** @@ -8,7 +9,7 @@ import { NODE_VISIBLE } from "../../../util/globals"; * the x and y values of terminal nodes which are also visible. * The regression is forced to pass through nodes[0]. */ -function calculateRegressionThroughRoot(nodes) { +function calculateRegressionThroughRoot(nodes: PhyloNode[]): Regression { const terminalNodes = nodes.filter((d) => !d.n.hasChildren && d.visibility === NODE_VISIBLE); const nTips = terminalNodes.length; if (nTips===0) { @@ -31,7 +32,7 @@ function calculateRegressionThroughRoot(nodes) { * Calculate regression through visible terminal nodes which have both x & y values * set. These values must be numeric. */ -function calculateRegressionWithFreeIntercept(nodes) { +function calculateRegressionWithFreeIntercept(nodes: PhyloNode[]): Regression { const terminalNodesWithXY = nodes.filter( (d) => (!d.n.hasChildren) && d.x!==undefined && d.y!==undefined && d.visibility === NODE_VISIBLE ); @@ -50,7 +51,7 @@ function calculateRegressionWithFreeIntercept(nodes) { } /** sets this.regression */ -export function calculateRegression() { +export function calculateRegression(this: PhyloTreeType) { if (this.layout==="clock") { this.regression = calculateRegressionThroughRoot(this.nodes); } else { @@ -58,7 +59,11 @@ export function calculateRegression() { } } -export function makeRegressionText(regression, layout, yScale) { +export function makeRegressionText( + regression: Regression, + layout: string, + yScale: d3.ScaleContinuousNumeric, +): string { if (layout==="clock") { if (guessAreMutationsPerSite(yScale)) { return `rate estimate: ${regression.slope.toExponential(2)} subs per site per year`; diff --git a/src/components/tree/phyloTree/renderers.js b/src/components/tree/phyloTree/renderers.ts similarity index 82% rename from src/components/tree/phyloTree/renderers.js rename to src/components/tree/phyloTree/renderers.ts index 099c20f2d..d3c648d5d 100644 --- a/src/components/tree/phyloTree/renderers.js +++ b/src/components/tree/phyloTree/renderers.ts @@ -3,29 +3,84 @@ import { NODE_VISIBLE } from "../../../util/globals"; import { getDomId, setDisplayOrder } from "./helpers"; import { makeRegressionText } from "./regression"; import { getEmphasizedColor } from "../../../util/colorHelpers"; -/** - * @param {d3 selection} svg -- the svg into which the tree is drawn - * @param {string} layout -- the layout to be used, e.g. "rect" - * @param {string} distance -- the property used as branch length, e.g. div or num_date - * @param {string} focus -- whether to focus on filtered nodes - * @param {object} parameters -- an object that contains options that will be added to this.params - * @param {object} callbacks -- an object with call back function defining mouse behavior - * @param {array} branchThickness -- array of branch thicknesses (same ordering as tree nodes) - * @param {array} visibility -- array of visibility of nodes(same ordering as tree nodes) - * @param {bool} drawConfidence -- should confidence intervals be drawn? - * @param {bool} vaccines -- should vaccine crosses (and dotted lines if applicable) be drawn? - * @param {array} branchStroke -- branch stroke colour for each node (set onto each node) - * @param {array} tipStroke -- tip stroke colour for each node (set onto each node) - * @param {array} tipFill -- tip fill colour for each node (set onto each node) - * @param {array|null} tipRadii -- array of tip radius' - * @param {array} dateRange - * @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, focus, parameters, callbacks, branchThickness, visibility, drawConfidence, vaccines, branchStroke, tipStroke, tipFill, tipRadii, dateRange, scatterVariables}) { +import { Callbacks, Distance, Params, PhyloNode, PhyloTreeType } from "./types"; +import { Selection } from "d3"; +import { Layout, ScatterVariables } from "../../../reducers/controls"; +import { ReduxNode, Visibility } from "../../../reducers/tree/types"; + +export const render = function render( + this: PhyloTreeType, +{ + svg, + layout, + distance, + focus, + parameters, + callbacks, + branchThickness, + visibility, + drawConfidence, + vaccines, + branchStroke, + tipStroke, + tipFill, + tipRadii, + dateRange, + scatterVariables +}: { + /** the svg into which the tree is drawn */ + svg: Selection + + /** the layout to be used, e.g. "rect" */ + layout: Layout + + /** the property used as branch length, e.g. div or num_date */ + distance: Distance + + /** whether to focus on filtered nodes */ + focus: boolean + + /** an object that contains options that will be added to this.params */ + parameters: Partial + + /** an object with call back function defining mouse behavior */ + callbacks: Callbacks + + /** array of branch thicknesses (same ordering as tree nodes) */ + branchThickness: number[] + + /** array of visibility of nodes(same ordering as tree nodes) */ + visibility: Visibility[] + + /** should confidence intervals be drawn? */ + drawConfidence: boolean + + /** should vaccine crosses (and dotted lines if applicable) be drawn? */ + vaccines: ReduxNode[] | false + + /** branch stroke colour for each node (set onto each node) */ + branchStroke: string[] + + /** tip stroke colour for each node (set onto each node) */ + tipStroke: string[] + + /** tip fill colour for each node (set onto each node) */ + tipFill: string[] + + /** array of tip radius' */ + tipRadii: number[] | null + + dateRange: [number, number] + + /** {x, y} properties to map nodes => scatterplot (only used if layout="scatter") */ + scatterVariables: ScatterVariables +}) { timerStart("phyloTree render()"); this.svg = svg; - this.params = Object.assign(this.params, parameters); + this.params = { + ...this.params, + ...parameters + }; this.callbacks = callbacks; this.vaccines = vaccines ? vaccines.map((d) => d.shell) : undefined; this.dateRange = dateRange; @@ -67,9 +122,8 @@ export const render = function render({svg, layout, distance, focus, parameters, /** * adds crosses to the vaccines - * @return {null} */ -export const drawVaccines = function drawVaccines() { +export const drawVaccines = function drawVaccines(this: PhyloTreeType): void { if (!this.vaccines || !this.vaccines.length) return; if (!("vaccines" in this.groups)) { @@ -95,9 +149,8 @@ export const drawVaccines = function drawVaccines() { /** * adds all the tip circles to the svg, they have class tip - * @return {null} */ -export const drawTips = function drawTips() { +export const drawTips = function drawTips(this: PhyloTreeType): void { timerStart("drawTips"); const params = this.params; if (!("tips" in this.groups)) { @@ -130,9 +183,8 @@ export const drawTips = function drawTips() { * given a tree node, decide whether the branch should be rendered * This enforces the "hidden" property set on `node.node_attrs.hidden` * in the dataset JSON - * @return {string} */ -export const getBranchVisibility = (d) => { +export const getBranchVisibility = (d: PhyloNode): "visible" | "hidden" => { const hiddenSetting = d.n.node_attrs && d.n.node_attrs.hidden; if (hiddenSetting && ( @@ -148,10 +200,13 @@ export const getBranchVisibility = (d) => { /** Calculate the stroke for a given branch. May return a hex or a `url` referring to * a SVG gradient definition - * @param {obj} d node - * @param {string} b branch type -- either "T" (tee) or "S" (stem) */ -export const strokeForBranch = (d, _b) => { +export const strokeForBranch = ( + d: PhyloNode, + + /** branch type -- either "T" (tee) or "S" (stem) */ + _b?: "T" | "S", +): string => { /* Due to errors rendering gradients on SVG branches on some browsers/OSs which would cause the branches to not appear, we're falling back to the previous solution which doesn't use gradients. The commented code remains & hopefully a solution can be @@ -166,9 +221,8 @@ export const strokeForBranch = (d, _b) => { /** * adds all branches to the svg, these are paths with class branch, which comprise two groups - * @return {null} */ -export const drawBranches = function drawBranches() { +export const drawBranches = function drawBranches(this: PhyloTreeType): void { timerStart("drawBranches"); const params = this.params; @@ -239,9 +293,8 @@ export const drawBranches = function drawBranches() { /** * draws the regression line in the svg and adds a text with the rate estimate - * @return {null} */ -export const drawRegression = function drawRegression() { +export const drawRegression = function drawRegression(this: PhyloTreeType): void { /* check we have computed a sensible regression before attempting to draw */ if (this.regression.slope===undefined) { return; @@ -280,7 +333,7 @@ export const drawRegression = function drawRegression() { .style("font-family", this.params.fontFamily); }; -export const removeRegression = function removeRegression() { +export const removeRegression = function removeRegression(this: PhyloTreeType): void { if ("regression" in this.groups) { this.groups.regression.selectAll("*").remove(); } @@ -289,7 +342,7 @@ export const removeRegression = function removeRegression() { /* * add and remove elements from tree, initial render */ -export const clearSVG = function clearSVG() { +export const clearSVG = function clearSVG(this: PhyloTreeType): void { this.svg.selectAll("*").remove(); }; @@ -338,11 +391,16 @@ export const updateColorBy = function updateColorBy() {}; /** given a node `d` which is being hovered, update it's colour to emphasize * that it's being hovered. This updates the SVG element stroke style in-place * _or_ updates the SVG gradient def in place. - * @param {PhyloNode} d node - * @param {string} c1 colour of the parent (start of the branch) - * @param {string} c2 colour of the node (end of the branch) */ -const handleBranchHoverColor = (d, c1, c2) => { +const handleBranchHoverColor = ( + d: PhyloNode, + + /** colour of the parent (start of the branch) */ + c1: string, + + /** colour of the node (end of the branch) */ + c2: string, +): void => { if (!d) { return; } /* We want to emphasize the colour of the branch. How we do this depends on how the branch was rendered in the first place! */ @@ -359,12 +417,12 @@ const handleBranchHoverColor = (d, c1, c2) => { } }; -export const branchStrokeForLeave = function branchStrokeForLeave(d) { +export const branchStrokeForLeave = function branchStrokeForLeave(d: PhyloNode) { if (!d) { return; } handleBranchHoverColor(d, d.n.parent.shell.branchStroke, d.branchStroke); }; -export const branchStrokeForHover = function branchStrokeForHover(d) { +export const branchStrokeForHover = function branchStrokeForHover(d: PhyloNode) { if (!d) { return; } handleBranchHoverColor(d, getEmphasizedColor(d.n.parent.shell.branchStroke), getEmphasizedColor(d.branchStroke)); }; @@ -374,7 +432,7 @@ export const branchStrokeForHover = function branchStrokeForHover(d) { * and regression lines. In theory, we can clip to exactly the {xy}Scale range, however * in practice, elements (or portions of elements) render outside this. */ -export const setClipMask = function setClipMask() { +export const setClipMask = function setClipMask(this: PhyloTreeType): void { const [yMin, yMax] = this.yScale.range(); // for the RHS tree (if there is one) ensure that xMin < xMax, else width<0 which some // browsers don't like. See diff --git a/src/components/tree/phyloTree/types.ts b/src/components/tree/phyloTree/types.ts new file mode 100644 index 000000000..da7bb5684 --- /dev/null +++ b/src/components/tree/phyloTree/types.ts @@ -0,0 +1,297 @@ +import { Selection } from "d3"; +import { Layout, PerformanceFlags, ScatterVariables } from "../../../reducers/controls"; +import { ReduxNode, Visibility } from "../../../reducers/tree/types"; +import { change, modifySVG, modifySVGInStages } from "./change"; + +import * as confidence from "./confidence"; +import * as grid from "./grid"; +import * as labels from "./labels"; +import * as layouts from "./layouts"; +import * as regression from "./regression"; +import * as renderers from "./renderers"; + +// ---------- Basics ---------- // + +export type Distance = "num_date" | "div" + +export type TreeElement = + ".branch.S" | + ".branch.T" | + ".branch" | + ".branchLabel" | + ".conf" | + ".grid" | + ".regression" | + ".tip" | + ".tipLabel" | + ".vaccineCross" | + ".vaccineDottedLine" + +export interface Regression { + intercept?: number + r2?: number + slope?: number +} + +// ---------- Callbacks ---------- // + +type NodeCallback = (d: PhyloNode) => void + +export interface Callbacks { + onBranchClick: NodeCallback + onBranchHover: NodeCallback + onBranchLeave: NodeCallback + onTipClick: NodeCallback + onTipHover: NodeCallback + onTipLeave: NodeCallback + tipLabel: NodeCallback +} + +// ---------- PhyloNode ---------- // + +/** + * This is a subset of CSSStyleDeclaration. Reasons for not using that directly: + * 1. CSSStyleDeclaration has generic string types for all properties. We allow a number type on many. + * 2. We do not use most CSSStyleDeclaration properties. + * 3. CSSStyleDeclaration uses strokeWidth instead of stroke-width. + * + */ +// TODO: consider extending existing interfaces such as SVGCircleElement for cx/cy/r +interface SVG { + cursor?: unknown + cx?: number + cy?: number + d?: unknown + fill?: unknown + opacity?: unknown + r?: number + stroke?: unknown + "stroke-width"?: number + visibility?: Visibility + + // TODO: This should be `string | number`, conditional on layout + x?: any + + // TODO: This should be `string | number`, conditional on layout + y?: any +} + +export type SVGProperty = keyof SVG + +export interface PhyloNode extends SVG { + angle?: number + branch?: [string, string] + branchStroke?: string + conf?: [number, number] + + /** SVG path */ + confLine?: string + + crossDepth?: number + depth?: number + displayOrder?: number + displayOrderRange?: [number, number] + fill?: string + inView: boolean + n: ReduxNode + pDepth?: number + + // TODO: This should be `string | number`, conditional on layout + px?: any + + // TODO: This should be `string | number`, conditional on layout + py?: any + + rot?: number + smallBigArc?: boolean + tau?: number + that: PhyloTreeType + tipStroke?: string + update?: boolean + + /** SVG path */ + vaccineCross?: string + + w?: number + xBase?: number + xCBarEnd?: number + xCBarStart?: number + xCross?: number + xTip?: number + yBase?: number + yCBarEnd?: number + yCBarStart?: number + yCross?: number + yTip?: number +} + +/** + * Properties can be any property on PhyloNode but as an array for multiple nodes. + * These are the ones that are used in the code. + */ +export interface PropsForPhyloNodes { + branchStroke?: string[] + fill?: string[] + r?: number[] + tipStroke?: (string | undefined)[] + visibility?: Visibility[] +} + +// ---------- PhyloTree ---------- // + +export interface Params { + branchLabelFill: string + branchLabelFont: string + branchLabelFontWeight: number + branchLabelKey: string | false + branchLabelPadX: number + branchLabelPadY: number + branchStroke: string + branchStrokeWidth: number + confidence?: boolean + fillSelected: string + fontFamily: string + grid?: boolean + majorGridStroke: string + majorGridWidth: number + mapToScreenDebounceTime: number + minorGridStroke: string + minorGridWidth: number + minorTicks: number + orientation: [number, number] + radiusSelected: number + regressionStroke: string + regressionWidth: number + showAllBranchLabels?: boolean + showGrid: boolean + showTipLabels?: boolean + tickLabelFill: string + tickLabelSize: number + tipFill: string + tipLabelBreakL1: number + tipLabelBreakL2: number + tipLabelBreakL3: number + tipLabelFill: string + tipLabelFont: string + tipLabelFontSizeL1: number + tipLabelFontSizeL2: number + tipLabelFontSizeL3: number + tipLabelPadX: number + tipLabelPadY: number + tipLabels: boolean + tipRadius: number + tipStroke: string + tipStrokeWidth: number +} + +export interface ChangeParams { + // booleans for what should be changed // + changeColorBy?: boolean + changeVisibility?: boolean + changeTipRadii?: boolean + changeBranchThickness?: boolean + showConfidences?: boolean + removeConfidences?: boolean + zoomIntoClade?: false | PhyloNode + svgHasChangedDimensions?: boolean + animationInProgress?: boolean + changeNodeOrder?: boolean + + // change these things to provided value (unless undefined) // + newDistance?: Distance + newLayout?: Layout + updateLayout?: boolean // todo - this seems identical to `newLayout` + newBranchLabellingKey?: string + showAllBranchLabels?: boolean + newTipLabelKey?: string | symbol + + // arrays of data (the same length as nodes) // + branchStroke?: string[] + tipStroke?: (string | undefined)[] + fill?: string[] + visibility?: Visibility[] + tipRadii?: number[] + branchThickness?: number[] + + // other data // + focus?: boolean + scatterVariables?: ScatterVariables + performanceFlags?: PerformanceFlags +} + +export interface PhyloTreeType { + addGrid: typeof grid.addGrid + attributes: string[] + calculateRegression: typeof regression.calculateRegression + callbacks: Callbacks + change: typeof change + clearSVG: typeof renderers.clearSVG + confidencesInSVG: boolean + dateRange: [number, number] + distance: Distance + drawBranchLabels: typeof labels.drawBranchLabels + drawBranches: typeof renderers.drawBranches + drawConfidence: typeof confidence.drawConfidence + drawRegression: typeof renderers.drawRegression + drawSingleCI: typeof confidence.drawSingleCI + drawTips: typeof renderers.drawTips + drawVaccines: typeof renderers.drawVaccines + grid: boolean + groups: { + branchGradientDefs?: Selection + branchStem?: Selection + branchTee?: Selection + clipPath?: Selection + confidenceIntervals?: Selection + regression?: Selection + tips?: Selection + vaccines?: Selection + } + hideGrid: typeof grid.hideGrid + hideTemporalSlice: typeof grid.hideTemporalSlice + id: string + layout: Layout + mapToScreen: typeof layouts.mapToScreen + margins: { + bottom: number + left: number + right: number + top: number + } + modifySVG: typeof modifySVG + modifySVGInStages: typeof modifySVGInStages + nodes: PhyloNode[] + params: Params + radialLayout: typeof layouts.radialLayout + rectangularLayout: typeof layouts.rectangularLayout + regression?: Regression + removeBranchLabels: typeof labels.removeBranchLabels + removeConfidence: typeof confidence.removeConfidence + removeRegression: typeof renderers.removeRegression + removeTipLabels: typeof labels.removeTipLabels + render: typeof renderers.render + scatterVariables?: ScatterVariables + scatterplotLayout: typeof layouts.scatterplotLayout + setClipMask: typeof renderers.setClipMask + setDistance: typeof layouts.setDistance + setLayout: typeof layouts.setLayout + setScales: typeof layouts.setScales + showTemporalSlice: typeof grid.showTemporalSlice + strainToNode: Record + svg: Selection + timeLastRenderRequested?: number + unrootedLayout: typeof layouts.unrootedLayout + updateBranchLabels: typeof labels.updateBranchLabels + updateColorBy: typeof renderers.updateColorBy + updateTipLabels: typeof labels.updateTipLabels + vaccines?: PhyloNode[] + visibility: Visibility[] + + // TODO: This should be `d3.ScalePoint | d3.ScaleContinuousNumeric`, conditional on layout + xScale: any + + // TODO: This should be `d3.ScalePoint | d3.ScaleContinuousNumeric`, conditional on layout + yScale: any + + zoomNode: PhyloNode +} diff --git a/src/components/tree/reactD3Interface/callbacks.js b/src/components/tree/reactD3Interface/callbacks.ts similarity index 84% rename from src/components/tree/reactD3Interface/callbacks.js rename to src/components/tree/reactD3Interface/callbacks.ts index 0ecc3fc85..b833f95c6 100644 --- a/src/components/tree/reactD3Interface/callbacks.js +++ b/src/components/tree/reactD3Interface/callbacks.ts @@ -1,12 +1,15 @@ -import { updateVisibleTipsAndBranchThicknesses, applyFilter } from "../../../actions/tree"; +import { updateVisibleTipsAndBranchThicknesses, applyFilter, Root } from "../../../actions/tree"; import { NODE_VISIBLE, strainSymbol } from "../../../util/globals"; import { getDomId, getParentBeyondPolytomy, getIdxOfInViewRootNode } from "../phyloTree/helpers"; import { branchStrokeForHover, branchStrokeForLeave } from "../phyloTree/renderers"; +import { PhyloNode } from "../phyloTree/types"; import { SELECT_NODE, DESELECT_NODE } from "../../../actions/types"; +import { SelectedNode } from "../../../reducers/controls"; +import { TreeComponent } from "../tree"; /* Callbacks used by the tips / branches when hovered / selected */ -export const onTipHover = function onTipHover(d) { +export const onTipHover = function onTipHover(this: TreeComponent, d: PhyloNode): void { if (d.visibility !== NODE_VISIBLE) return; const phylotree = d.that.params.orientation[0] === 1 ? this.state.tree : @@ -18,7 +21,7 @@ export const onTipHover = function onTipHover(d) { }); }; -export const onTipClick = function onTipClick(d) { +export const onTipClick = function onTipClick(this: TreeComponent, d: PhyloNode): void { if (d.visibility !== NODE_VISIBLE) return; if (this.props.narrativeMode) return; /* The order of these two dispatches is important: the reducer handling @@ -29,7 +32,7 @@ export const onTipClick = function onTipClick(d) { }; -export const onBranchHover = function onBranchHover(d) { +export const onBranchHover = function onBranchHover(this: TreeComponent, d: PhyloNode): void { if (d.visibility !== NODE_VISIBLE) return; branchStrokeForHover(d); @@ -53,7 +56,7 @@ export const onBranchHover = function onBranchHover(d) { }); }; -export const onBranchClick = function onBranchClick(d) { +export const onBranchClick = function onBranchClick(this: TreeComponent, d: PhyloNode): void { if (d.visibility !== NODE_VISIBLE) return; if (this.props.narrativeMode) return; @@ -64,12 +67,12 @@ export const onBranchClick = function onBranchClick(d) { return; } - const root = [undefined, undefined]; - let cladeSelected; + const root: Root = [undefined, undefined]; + let cladeSelected: string | undefined; // Branches with multiple labels will be used in the order specified by this.props.tree.availableBranchLabels // (The order of the drop-down on the menu) // Can't use AA mut lists as zoom labels currently - URL is bad, but also, means every node has a label, and many conflict... - let legalBranchLabels; + let legalBranchLabels: string[] | undefined; // Check has some branch labels, and remove 'aa' ones. if (d.n.branch_attrs && d.n.branch_attrs.labels !== undefined) { @@ -98,7 +101,7 @@ export const onBranchClick = function onBranchClick(d) { }; /* onBranchLeave called when mouse-off, i.e. anti-hover */ -export const onBranchLeave = function onBranchLeave(d) { +export const onBranchLeave = function onBranchLeave(this: TreeComponent, d: PhyloNode): void { /* Reset the stroke back to what it was before */ branchStrokeForLeave(d); @@ -112,7 +115,7 @@ export const onBranchLeave = function onBranchLeave(d) { this.setState({hoveredNode: null}); }; -export const onTipLeave = function onTipLeave(d) { +export const onTipLeave = function onTipLeave(this: TreeComponent, d: PhyloNode): void { const phylotree = d.that.params.orientation[0] === 1 ? this.state.tree : this.state.treeToo; @@ -124,7 +127,7 @@ export const onTipLeave = function onTipLeave(d) { }; /* clearSelectedNode when clicking to remove the node-selected modal */ -export const clearSelectedNode = function clearSelectedNode(selectedNode) { +export const clearSelectedNode = function clearSelectedNode(this: TreeComponent, selectedNode: SelectedNode): void { if (!selectedNode.isBranch) { /* perform the filtering action (if necessary) that will restore the filtering state of the node prior to the selection */ diff --git a/src/components/tree/reactD3Interface/change.js b/src/components/tree/reactD3Interface/change.ts similarity index 89% rename from src/components/tree/reactD3Interface/change.js rename to src/components/tree/reactD3Interface/change.ts index c657bd7a2..90151a824 100644 --- a/src/components/tree/reactD3Interface/change.js +++ b/src/components/tree/reactD3Interface/change.ts @@ -1,8 +1,18 @@ import { calculateStrokeColors, getBrighterColor } from "../../../util/colorHelpers"; - -export const changePhyloTreeViaPropsComparison = (mainTree, phylotree, oldProps, newProps) => { - const args = {}; - const newState = {}; +import { ChangeParams, PhyloTreeType } from "../phyloTree/types"; +import { TreeComponentProps, TreeComponentState } from "../types"; + +export const changePhyloTreeViaPropsComparison = ( + mainTree: boolean, + phylotree: PhyloTreeType, + oldProps: TreeComponentProps, + newProps: TreeComponentProps, +): { + newState: Partial | false + change: boolean +} => { + const args: ChangeParams = {}; + const newState: Partial = {}; /* do not use oldProps.tree or newTreeRedux */ const oldTreeRedux = mainTree ? oldProps.tree : oldProps.treeToo; const newTreeRedux = mainTree ? newProps.tree : newProps.treeToo; @@ -124,12 +134,15 @@ export const changePhyloTreeViaPropsComparison = (mainTree, phylotree, oldProps, args.svgHasChangedDimensions = true; } - const change = Object.keys(args).length; + const change = Object.keys(args).length > 0; if (change) { args.animationInProgress = newProps.animationPlayPauseButton === "Pause"; args.performanceFlags = newProps.performanceFlags; // console.log('\n\n** ', phylotree.id, 'changePhyloTreeViaPropsComparison **', args); phylotree.change(args); } - return [Object.keys(newState).length ? newState : false, change]; + return { + newState: Object.keys(newState).length ? newState : false, + change, + }; }; diff --git a/src/components/tree/reactD3Interface/initialRender.js b/src/components/tree/reactD3Interface/initialRender.ts similarity index 89% rename from src/components/tree/reactD3Interface/initialRender.js rename to src/components/tree/reactD3Interface/initialRender.ts index b836a4e2a..22cec9e4f 100644 --- a/src/components/tree/reactD3Interface/initialRender.js +++ b/src/components/tree/reactD3Interface/initialRender.ts @@ -3,8 +3,16 @@ import 'd3-transition'; import { calculateStrokeColors, getBrighterColor } from "../../../util/colorHelpers"; import * as callbacks from "./callbacks"; import { makeTipLabelFunc } from "../phyloTree/labels"; +import { PhyloTreeType } from "../phyloTree/types"; +import { TreeComponent } from "../tree"; +import { TreeComponentProps } from "../types"; -export const renderTree = (that, main, phylotree, props) => { +export const renderTree = ( + that: TreeComponent, + main: boolean, + phylotree: PhyloTreeType, + props: TreeComponentProps, +): void => { const ref = main ? that.domRefs.mainTree : that.domRefs.secondTree; const treeState = main ? props.tree : props.treeToo; if (!treeState.loaded) { @@ -17,7 +25,7 @@ export const renderTree = (that, main, phylotree, props) => { renderBranchLabels=false; } const tipStrokeColors = calculateStrokeColors(treeState, false, props.colorByConfidence, props.colorBy); - /* simply the call to phylotree.render */ + phylotree.render({ svg: select(ref), layout: props.layout, diff --git a/src/components/tree/tree.js b/src/components/tree/tree.tsx similarity index 84% rename from src/components/tree/tree.js rename to src/components/tree/tree.tsx index adee6cf43..04c89b717 100644 --- a/src/components/tree/tree.js +++ b/src/components/tree/tree.tsx @@ -1,7 +1,8 @@ import React from "react"; import { withTranslation } from "react-i18next"; import { FaSearchMinus } from "react-icons/fa"; -import { updateVisibleTipsAndBranchThicknesses } from "../../actions/tree"; +import { Root, updateVisibleTipsAndBranchThicknesses } from "../../actions/tree"; +import { SelectedNode } from "../../reducers/controls"; import Card from "../framework/card"; import Legend from "./legend/legend"; import PhyloTree from "./phyloTree/phyloTree"; @@ -17,13 +18,22 @@ import { attemptUntangle } from "../../util/globals"; import ErrorBoundary from "../../util/errorBoundary"; import { untangleTreeToo } from "./tangle/untangling"; import { sortByGeneOrder } from "../../util/treeMiscHelpers"; +import { TreeComponentProps, TreeComponentState } from "./types"; export const spaceBetweenTrees = 100; export const lhsTreeId = "LEFT"; const rhsTreeId = "RIGHT"; -class Tree extends React.Component { - constructor(props) { +export class TreeComponent extends React.Component { + + domRefs: { + mainTree: SVGSVGElement | null; + secondTree: SVGSVGElement | null; + }; + tangleRef?: Tangle; + clearSelectedNode: (node: SelectedNode) => void; + + constructor(props: TreeComponentProps) { super(props); this.domRefs = { mainTree: undefined, @@ -47,13 +57,13 @@ class Tree extends React.Component { } /* pressing the escape key should dismiss an info modal (if one exists) */ - handlekeydownEvent = (event) => { + handlekeydownEvent = (event: KeyboardEvent) => { if (event.key==="Escape" && this.props.selectedNode) { this.clearSelectedNode(this.props.selectedNode); } } - setUpAndRenderTreeToo(props, newState) { + setUpAndRenderTreeToo(props: TreeComponentProps, newState: Partial) { /* this.setState(newState) will be run sometime after this returns */ /* modifies newState in place */ newState.treeToo = new PhyloTree(props.treeToo.nodes, rhsTreeId, props.treeToo.idxOfInViewRootNode); @@ -66,7 +76,7 @@ class Tree extends React.Component { componentDidMount() { document.addEventListener('keyup', this.handlekeydownEvent); if (this.props.tree.loaded) { - const newState = {}; + const newState: Partial = {}; newState.tree = new PhyloTree(this.props.tree.nodes, lhsTreeId, this.props.tree.idxOfInViewRootNode); renderTree(this, true, newState.tree, this.props); if (this.props.showTreeToo) { @@ -77,12 +87,15 @@ class Tree extends React.Component { } } - componentDidUpdate(prevProps) { - let newState = {}; + componentDidUpdate(prevProps: TreeComponentProps) { + let newState: Partial = {}; let rightTreeUpdated = false; /* potentially change the (main / left hand) tree */ - const [potentialNewState, leftTreeUpdated] = changePhyloTreeViaPropsComparison(true, this.state.tree, prevProps, this.props); + const { + newState: potentialNewState, + change: leftTreeUpdated, + } = changePhyloTreeViaPropsComparison(true, this.state.tree, prevProps, this.props); if (potentialNewState) newState = potentialNewState; /* has the 2nd (right hand) tree just been turned on, off or swapped? */ @@ -98,9 +111,10 @@ class Tree extends React.Component { if (this.tangleRef) this.tangleRef.drawLines(); } } else if (this.state.treeToo) { /* the tree hasn't just been swapped, but it does exist and may need updating */ - let _unusedNewState; - [_unusedNewState, rightTreeUpdated] = changePhyloTreeViaPropsComparison(false, this.state.treeToo, prevProps, this.props); - /* note, we don't incorporate _unusedNewState into the state? why not? */ + ({ + change: rightTreeUpdated, + } = changePhyloTreeViaPropsComparison(false, this.state.treeToo, prevProps, this.props)); + /* note, we don't incorporate newState into the state? why not? */ } /* we may need to (imperatively) tell the tangle to redraw */ @@ -114,7 +128,12 @@ class Tree extends React.Component { document.removeEventListener('keyup', this.handlekeydownEvent); } - getStyles = () => { + getStyles = (): { + treeButtonsDiv: React.CSSProperties + resetTreeButton: React.CSSProperties + zoomToSelectedButton: React.CSSProperties + zoomOutButton: React.CSSProperties + } => { const filteredTree = !!this.props.tree.idxOfFilteredRoot && this.props.tree.idxOfInViewRootNode !== this.props.tree.idxOfFilteredRoot; const filteredTreeToo = !!this.props.treeToo.idxOfFilteredRoot && @@ -156,7 +175,15 @@ class Tree extends React.Component { }; }; - renderTreeDiv({width, height, mainTree}) { + renderTreeDiv({ + width, + height, + mainTree, + }: { + width: number + height: number + mainTree: boolean + }) { return ( { - let newRoot, newRootToo; + const root: Root = [undefined, undefined]; // Zoom out of main tree if index of root node is not 0 if (this.props.tree.idxOfInViewRootNode !== 0) { const rootNode = this.props.tree.nodes[this.props.tree.idxOfInViewRootNode]; - newRoot = getParentBeyondPolytomy(rootNode, this.props.distanceMeasure, this.props.tree.observedMutations).arrayIdx; + root[0] = getParentBeyondPolytomy(rootNode, this.props.distanceMeasure, this.props.tree.observedMutations).arrayIdx; } // Also zoom out of second tree if index of root node is not 0 if (this.props.treeToo.idxOfInViewRootNode !== 0) { const rootNodeToo = this.props.treeToo.nodes[this.props.treeToo.idxOfInViewRootNode]; - newRootToo = getParentBeyondPolytomy(rootNodeToo, this.props.distanceMeasure, this.props.treeToo.observedMutations).arrayIdx; + root[1] = getParentBeyondPolytomy(rootNodeToo, this.props.distanceMeasure, this.props.treeToo.observedMutations).arrayIdx; } - const root = [newRoot, newRootToo]; this.props.dispatch(updateVisibleTipsAndBranchThicknesses({root})); } @@ -272,5 +298,4 @@ class Tree extends React.Component { } } -const WithTranslation = withTranslation()(Tree); -export default WithTranslation; +export default withTranslation()(TreeComponent); diff --git a/src/components/tree/types.ts b/src/components/tree/types.ts new file mode 100644 index 000000000..c3773949b --- /dev/null +++ b/src/components/tree/types.ts @@ -0,0 +1,57 @@ +import { WithTranslation } from "react-i18next" +import { ColorScale, 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"; + +export interface TreeComponentOwnProps { + dispatch: AppDispatch + height: number + width: number +} + +export interface TreeComponentProps extends WithTranslation, TreeComponentStateProps, TreeComponentOwnProps {} + +// This is duplicated from RootState, but good to be explicit about what's +// expected here. +export interface TreeComponentStateProps { + animationPlayPauseButton: "Play" | "Pause" + canRenderBranchLabels: boolean + colorBy: string + colorByConfidence: boolean + colorings: unknown + colorScale: ColorScale + dateMaxNumeric: number + dateMinNumeric: number + distanceMeasure: Distance + explodeAttr: string + filters: Record> + focus: boolean + genomeMap: unknown + layout: Layout + narrativeMode: boolean + panelsToDisplay: string[] + performanceFlags: PerformanceFlags + quickdraw: boolean + scatterVariables: ScatterVariables + selectedBranchLabel: string + selectedNode: SelectedNode | null + showAllBranchLabels: boolean + showOnlyPanels: boolean + showTangle: boolean + showTreeToo: boolean + temporalConfidence: TemporalConfidence + tipLabelKey: string | symbol + tree: TreeState + treeToo: TreeTooState +} + +export interface TreeComponentState { + hoveredNode: { + node: PhyloNode + isBranch: boolean + } | null + tree: PhyloTreeType | null + treeToo: PhyloTreeType | null + geneSortFn?: (a: number, b: number) => number | (() => 0) +} diff --git a/src/globalStyles.js b/src/globalStyles.ts similarity index 84% rename from src/globalStyles.js rename to src/globalStyles.ts index 2f4490241..c57533fd5 100644 --- a/src/globalStyles.js +++ b/src/globalStyles.ts @@ -15,7 +15,7 @@ export const goColor = "#89B77F"; // green export const pauseColor = "#E39B39"; // orange // http://stackoverflow.com/questions/1895476/how-to-style-a-select-dropdown-with-css-only-without-javascript -export const sidebarField = { +export const sidebarField: React.CSSProperties = { backgroundColor: "#FFF", fontFamily: dataFont, width: controlsWidth - 13, @@ -31,7 +31,7 @@ export const sidebarField = { marginBottom: "3px" }; -export const materialButton = { +export const materialButton: React.CSSProperties = { border: "0px", backgroundColor: "inherit", marginLeft: 0, @@ -50,7 +50,7 @@ export const materialButton = { outline: 0 }; -export const materialButtonSelected = { +export const materialButtonSelected: React.CSSProperties = { border: "0px", backgroundColor: "inherit", marginLeft: 0, @@ -69,7 +69,7 @@ export const materialButtonSelected = { outline: 0 }; -export const materialButtonOutline = { +export const materialButtonOutline: React.CSSProperties = { border: "1px solid #CCC", backgroundColor: "inherit", borderRadius: 3, @@ -86,7 +86,7 @@ export const materialButtonOutline = { verticalAlign: "top" }; -export const tabSingle = { +export const tabSingle: React.CSSProperties = { borderTop: "1px solid #BBB", borderLeft: "1px solid #CCC", borderRight: "1px solid #CCC", @@ -104,7 +104,7 @@ export const tabSingle = { textTransform: "uppercase" }; -export const tabGroup = { +export const tabGroup: React.CSSProperties = { borderTop: "1px solid #BBB", borderLeft: "1px solid #CCC", borderRight: "1px solid #CCC", @@ -117,7 +117,7 @@ export const tabGroup = { backgroundColor: "#fff" }; -export const tabGroupMember = { +export const tabGroupMember: React.CSSProperties = { border: "none", backgroundColor: "inherit", padding: 0, @@ -130,7 +130,7 @@ export const tabGroupMember = { fontSize: 12 }; -export const tabGroupMemberSelected = { +export const tabGroupMemberSelected: React.CSSProperties = { border: "none", backgroundColor: "inherit", padding: 0, @@ -144,7 +144,10 @@ export const tabGroupMemberSelected = { }; -export const titleStyles = { +export const titleStyles: { + big: React.CSSProperties + small: React.CSSProperties +} = { big: { fontFamily: titleFont, fontSize: 76, @@ -166,7 +169,21 @@ export const titleStyles = { } }; -export const infoPanelStyles = { +export const infoPanelStyles: { + branchInfoHeading: React.CSSProperties + buttonLink: React.CSSProperties + tooltip: React.CSSProperties + modalContainer: React.CSSProperties + panel: React.CSSProperties + modalHeading: React.CSSProperties + modalSubheading: React.CSSProperties + tooltipHeading: React.CSSProperties + comment: React.CSSProperties + topRightMessage: React.CSSProperties + list: React.CSSProperties + item: React.CSSProperties + break: React.CSSProperties +} = { branchInfoHeading: { fontSize: 15, fontWeight: 400, diff --git a/src/metadata.ts b/src/metadata.ts new file mode 100644 index 000000000..73b60ea2a --- /dev/null +++ b/src/metadata.ts @@ -0,0 +1,36 @@ +import { ScaleType } from "./reducers/controls" + +export type Metadata = { + rootSequence?: unknown + rootSequenceSecondTree?: unknown + identicalGenomeMapAcrossBothTrees?: boolean + colorings: Colorings +} + +export type Colorings = { + [key: string]: ColoringInfo +} + +export type ColoringInfo = { + title: string + type: ScaleType + + /** scale set via JSON */ + scale: [string, string][] + + legend?: Legend +} + +export type Legend = { + /** + * Used to compute the legend swatch colour. The type of this depends on the scaleType. + * Continuous scales demand numeric values, however few restrictions are placed on other scales. + */ + value: unknown + + /** Displayed in the legend. Falls back to `value` if missing. */ + display?: string | number + + /** Custom legendBounds. Only considered for continuous scales. */ + bounds?: [number, number] +}[] diff --git a/src/reducers/controls.ts b/src/reducers/controls.ts index 5290c2440..13f95f98f 100644 --- a/src/reducers/controls.ts +++ b/src/reducers/controls.ts @@ -12,35 +12,141 @@ import * as types from "../actions/types"; import { calcBrowserDimensionsInitialState } from "./browserDimensions"; import { doesColorByHaveConfidence } from "../actions/recomputeReduxState"; import { hasMultipleGridPanels } from "../actions/panelDisplay"; +import { Distance } from "../components/tree/phyloTree/types"; -type Layout = "rect" | "radial" | "unrooted" | "clock" | "scatter" + +export interface ColorScale { + colorBy: string + continuous: boolean + domain?: unknown[] + genotype: Genotype | null + legendBounds?: LegendBounds + legendLabels?: LegendLabels + legendValues: LegendValues + scale: (value: any) => string + scaleType: ScaleType | null + version: number + visibleLegendValues: LegendValues +} + +export interface Genotype { + gene: string + positions: number[] + aa: boolean +} + +export type Layout = "rect" | "radial" | "unrooted" | "clock" | "scatter" + +export type LegendBounds = { + [key: string | number]: [number, number] +} + +/** A map of legendValues to a value for display in the legend. */ +export type LegendLabels = Map + +/** An array of values to display in the legend. */ +// TODO: I think this should be number[] | string[] but that requires adding type guards +export type LegendValues = any[] + +export type PerformanceFlags = Map + +export interface SelectedNode { + existingFilterState: "active" | "inactive" | null + idx: number + isBranch: boolean + name: string + treeId: string +} + +export type ScaleType = "ordinal" | "categorical" | "continuous" | "temporal" | "boolean" + +export interface ScatterVariables { + showBranches?: boolean + showRegression?: boolean + x?: string + xContinuous?: boolean + xDomain?: number[] + xTemporal?: boolean + y?: string + yContinuous?: boolean + yDomain?: number[] + yTemporal?: boolean +} + +export interface TemporalConfidence { + exists: boolean + display: boolean + on: boolean +} interface Defaults { - distanceMeasure: string + distanceMeasure: Distance layout: Layout focus: boolean geoResolution: string - filters: Record + filters: Record filtersInFooter: string[] colorBy: string selectedBranchLabel: string - tipLabelKey: typeof strainSymbol + tipLabelKey: string | symbol showTransmissionLines: boolean sidebarOpen?: boolean } export interface BasicControlsState { defaults: Defaults + + absoluteDateMax: string + absoluteDateMaxNumeric: number + absoluteDateMin: string + absoluteDateMinNumeric: number + analysisSlider: boolean + animationPlayPauseButton: "Play" | "Pause" + available?: boolean + branchLengthsToDisplay: string + canRenderBranchLabels: boolean + canTogglePanelLayout: boolean + colorBy: string + colorByConfidence: boolean + coloringsPresentOnTree?: Set + colorScale?: ColorScale + dateMax: string + dateMaxNumeric: number + dateMin: string + dateMinNumeric: number + distanceMeasure: Distance + explodeAttr?: string + filters: Record> + filtersInFooter: string[] + focus: boolean + geoResolution: string layout: Layout + mapAnimationCumulative: boolean + mapAnimationDurationInMilliseconds: number + mapAnimationShouldLoop: boolean + mapAnimationStartDate: unknown + mapLegendOpen?: boolean + modal: 'download' | 'linkOut' | null + normalizeFrequencies: boolean + panelLayout: string panelsAvailable: string[] panelsToDisplay: string[] + performanceFlags: PerformanceFlags + quickdraw: boolean + scatterVariables: ScatterVariables + selectedBranchLabel: string + selectedNode: SelectedNode | null + showAllBranchLabels: boolean + showOnlyPanels: boolean + showTangle: boolean + showTransmissionLines: boolean showTreeToo: boolean - canTogglePanelLayout: boolean - focus: boolean - - // This allows arbitrary prop names while TypeScript adoption is incomplete. - // TODO: add all other props explicitly and remove this. - [propName: string]: any; + sidebarOpen: boolean + temporalConfidence: TemporalConfidence + tipLabelKey: string | symbol + treeLegendOpen?: boolean + zoomMax?: number + zoomMin?: number } export interface MeasurementsControlState { @@ -58,7 +164,7 @@ export interface ControlsState extends BasicControlsState, MeasurementsControlSt /* defaultState is a fn so that we can re-create it at any time, e.g. if we want to revert things (e.g. on dataset change) */ -export const getDefaultControlsState = () => { +export const getDefaultControlsState = (): ControlsState => { const defaults: Defaults = { distanceMeasure: defaultDistanceMeasure, layout: defaultLayout, @@ -297,7 +403,7 @@ const Controls = (state: ControlsState = getDefaultControlsState(), action): Con const existingFilterInfo = (state.filters?.[strainSymbol]||[]).find((info) => info.value===action.name); const existingFilterState = existingFilterInfo === undefined ? null : existingFilterInfo.active ? 'active' : 'inactive'; - const selectedNode = {name: action.name, idx: action.idx, existingFilterState, isBranch: action.isBranch, treeId: action.treeId}; + const selectedNode: SelectedNode = {name: action.name, idx: action.idx, existingFilterState, isBranch: action.isBranch, treeId: action.treeId}; return {...state, selectedNode}; } case types.DESELECT_NODE: { diff --git a/src/reducers/index.ts b/src/reducers/index.ts index c6fcf7c7b..f0cef104c 100644 --- a/src/reducers/index.ts +++ b/src/reducers/index.ts @@ -1,27 +1,28 @@ import { combineReducers } from "redux"; import metadata from "./metadata"; import tree from "./tree"; +import { TreeState, TreeTooState } from "./tree/types"; import frequencies from "./frequencies"; import entropy from "./entropy"; import controls, { ControlsState } from "./controls"; import browserDimensions from "./browserDimensions"; import notifications from "./notifications"; import narrative, { NarrativeState } from "./narrative"; -import treeToo from "./treeToo"; +import treeToo from "./tree/treeToo"; import general from "./general"; import jsonCache from "./jsonCache"; import measurements from "./measurements"; interface RootState { metadata: ReturnType - tree: ReturnType + tree: TreeState frequencies: ReturnType controls: ControlsState entropy: ReturnType browserDimensions: ReturnType notifications: ReturnType narrative: NarrativeState - treeToo: ReturnType + treeToo: TreeTooState general: ReturnType jsonCache: ReturnType measurements: ReturnType diff --git a/src/reducers/tree.js b/src/reducers/tree/index.ts similarity index 73% rename from src/reducers/tree.js rename to src/reducers/tree/index.ts index 6581364f3..b7d08f2e0 100644 --- a/src/reducers/tree.js +++ b/src/reducers/tree/index.ts @@ -1,11 +1,10 @@ -import { countTraitsAcrossTree } from "../util/treeCountingHelpers"; -import { addNodeAttrs } from "../util/treeMiscHelpers"; -import * as types from "../actions/types"; +import { AnyAction } from "@reduxjs/toolkit"; +import { countTraitsAcrossTree } from "../../util/treeCountingHelpers"; +import { addNodeAttrs } from "../../util/treeMiscHelpers"; +import * as types from "../../actions/types"; +import { TreeState, TreeTooState } from "./types"; -/* A version increase (i.e. props.version !== nextProps.version) necessarily implies -that the tree is loaded as they are set on the same action */ - -export const getDefaultTreeState = () => { +export const getDefaultTreeState = (): TreeState | TreeTooState => { return { loaded: false, nodes: null, @@ -29,19 +28,23 @@ export const getDefaultTreeState = () => { }; -const Tree = (state = getDefaultTreeState(), action) => { +const Tree = ( + state: TreeState = getDefaultTreeState(), + action: AnyAction, +): TreeState => { switch (action.type) { case types.URL_QUERY_CHANGE_WITH_COMPUTED_STATE: /* fallthrough */ case types.CLEAN_START: return action.tree; case types.DATA_INVALID: - return Object.assign({}, state, { - loaded: false - }); + return { + ...state, + loaded: false, + }; case types.CHANGE_EXPLODE_ATTR: /* fallthrough */ case types.CHANGE_DATES_VISIBILITY_THICKNESS: /* fallthrough */ case types.UPDATE_VISIBILITY_AND_BRANCH_THICKNESS: { - const newStates = { + const newStates: Partial = { visibility: action.visibility, visibilityVersion: action.visibilityVersion, branchThickness: action.branchThickness, @@ -51,18 +54,23 @@ const Tree = (state = getDefaultTreeState(), action) => { cladeName: action.cladeName, selectedClade: action.cladeName, }; - return Object.assign({}, state, newStates); + return { + ...state, + ...newStates, + }; } case types.UPDATE_TIP_RADII: - return Object.assign({}, state, { + return { + ...state, tipRadii: action.data, - tipRadiiVersion: action.version - }); + tipRadiiVersion: action.version, + }; case types.NEW_COLORS: - return Object.assign({}, state, { + return { + ...state, nodeColors: action.nodeColors, - nodeColorsVersion: action.version - }); + nodeColorsVersion: action.version, + }; case types.TREE_TOO_DATA: return action.tree; case types.ADD_EXTRA_METADATA: { diff --git a/src/reducers/treeToo.js b/src/reducers/tree/treeToo.ts similarity index 72% rename from src/reducers/treeToo.js rename to src/reducers/tree/treeToo.ts index e5c67a46b..fd5b75de7 100644 --- a/src/reducers/treeToo.js +++ b/src/reducers/tree/treeToo.ts @@ -1,18 +1,21 @@ -import * as types from "../actions/types"; -import { addNodeAttrs } from "../util/treeMiscHelpers"; -import { getDefaultTreeState } from "./tree"; -/* A version increase (i.e. props.version !== nextProps.version) necessarily implies -that the tree is loaded as they are set on the same action */ - -const treeToo = (state = getDefaultTreeState(), action) => { +import { AnyAction } from "@reduxjs/toolkit"; +import { getDefaultTreeState } from "."; +import { addNodeAttrs } from "../../util/treeMiscHelpers"; +import * as types from "../../actions/types"; +import { TreeTooState } from "./types"; +const treeToo = ( + state: TreeTooState = getDefaultTreeState(), + action: AnyAction, +): TreeTooState => { /* There are only a few actions we should always listen for, as they can change the presence / absence of the second tree */ switch (action.type) { case types.DATA_INVALID: - return Object.assign({}, state, { - loaded: false - }); + return { + ...state, + loaded: false, + }; case types.URL_QUERY_CHANGE_WITH_COMPUTED_STATE: /* fallthrough */ case types.CLEAN_START: if (action.treeToo) { @@ -37,7 +40,8 @@ const treeToo = (state = getDefaultTreeState(), action) => { case types.CHANGE_DATES_VISIBILITY_THICKNESS: /* fallthrough */ case types.UPDATE_VISIBILITY_AND_BRANCH_THICKNESS: if (action.tangleTipLookup) { - return Object.assign({}, state, { + return { + ...state, tangleTipLookup: action.tangleTipLookup, visibility: action.visibilityToo, visibilityVersion: action.visibilityVersionToo, @@ -45,20 +49,22 @@ const treeToo = (state = getDefaultTreeState(), action) => { branchThicknessVersion: action.branchThicknessVersionToo, idxOfInViewRootNode: action.idxOfInViewRootNodeToo, idxOfFilteredRoot: action.idxOfFilteredRootToo, - }); + }; } return state; case types.UPDATE_TIP_RADII: - return Object.assign({}, state, { + return { + ...state, tipRadii: action.dataToo, - tipRadiiVersion: action.version - }); + tipRadiiVersion: action.version, + }; case types.NEW_COLORS: if (action.nodeColorsToo) { - return Object.assign({}, state, { + return { + ...state, nodeColors: action.nodeColorsToo, - nodeColorsVersion: action.version - }); + nodeColorsVersion: action.version, + }; } return state; case types.ADD_EXTRA_METADATA: diff --git a/src/reducers/tree/types.ts b/src/reducers/tree/types.ts new file mode 100644 index 000000000..722b06fb2 --- /dev/null +++ b/src/reducers/tree/types.ts @@ -0,0 +1,86 @@ +import { NODE_NOT_VISIBLE, NODE_VISIBLE, NODE_VISIBLE_TO_MAP_ONLY } from "../../util/globals"; +import { PhyloNode } from "../../components/tree/phyloTree/types"; + +/** + * Maps mutation strings (in format gene:fromPosTo, e.g. 'nuc:A123T') + * to their occurrence count in the tree + */ +export type Mutations = Record + +export interface ReduxNode { + /** the index of the node in the nodes array. set so that we can access visibility / nodeColors if needed */ + arrayIdx?: number + + branch_attrs?: { + mutations?: { + [gene: string]: string[] + } + labels?: Record + } + children?: ReduxNode[] + currentGt?: string + + /** see the number of subtending tips (alive or dead) */ + fullTipCount?: number + + hasChildren?: boolean + inView?: boolean + name?: string + node_attrs?: { + div?: number + hidden?: "always" | "timetree" | "divtree" + num_date?: { + value: number + } + } + parent?: ReduxNode + parentInfo?: { + original: ReduxNode + } + shell?: PhyloNode + + /** the number of visible tips */ + tipCount?: number + + unexplodedChildren?: ReduxNode[] +} + +/** + * Keys: the traits + * Values: a Map of trait values to count + */ +export type TraitCounts = Record> + +export interface TreeState { + availableBranchLabels: string[] + branchThickness: number[] | null + branchThicknessVersion: number + cladeName?: string + idxOfFilteredRoot?: number + idxOfInViewRootNode: number + loaded: boolean + name?: string + nodeAttrKeys?: Set + nodeColors: string[] | null + nodeColorsVersion: number + nodes: ReduxNode[] | null + observedMutations: Mutations + selectedClade?: string + tipRadii: number[] | null + tipRadiiVersion: number + totalStateCounts: TraitCounts + vaccines: ReduxNode[] | false + /** + * A version increase (i.e. props.version !== nextProps.version) necessarily implies + * that the tree is loaded as they are set on the same action + */ + version: number + visibility: Visibility[] | null + visibilityVersion: number +} + +export interface TreeTooState extends TreeState { + tangleTipLookup?: unknown[][] +} + +export type Visibility = typeof NODE_NOT_VISIBLE | typeof NODE_VISIBLE_TO_MAP_ONLY | typeof NODE_VISIBLE diff --git a/src/util/colorScale.js b/src/util/colorScale.ts similarity index 77% rename from src/util/colorScale.js rename to src/util/colorScale.ts index 47a9f932a..87fb5a5eb 100644 --- a/src/util/colorScale.js +++ b/src/util/colorScale.ts @@ -9,19 +9,24 @@ import { isColorByGenotype, decodeColorByGenotype } from "./getGenotype"; import { setGenotype, orderOfGenotypeAppearance } from "./setGenotype"; import { getTraitFromNode } from "./treeMiscHelpers"; import { sortedDomain } from "./sortedDomain"; +import { ColoringInfo, Legend, Metadata } from "../metadata"; +import { ColorScale, ControlsState, Genotype, LegendBounds, LegendLabels, LegendValues, ScaleType } from "../reducers/controls"; +import { ReduxNode, TreeState, TreeTooState, Visibility } from "../reducers/tree/types"; export const unknownColor = "#ADB1B3"; /** * calculate the color scale. - * @param {string} colorBy - provided trait to use as color - * @param {object} controls - * @param {object} tree - * @param {object} treeToo - * @param {object} metadata - * @return {{scale: function, continuous: string, colorBy: string, version: int, legendValues: Array, legendBounds: Array|undefined, genotype: null|object, scaleType: null|string, visibleLegendValues: Array}} */ -export const calcColorScale = (colorBy, controls, tree, treeToo, metadata) => { +export const calcColorScale = ( + /** provided trait to use as color */ + colorBy: string, + + controls: ControlsState, + tree: TreeState, + treeToo: TreeTooState, + metadata: Metadata, +): ColorScale => { try { if (colorBy === "none") { throw new Error("colorBy is 'none'. Falling back to a default, uninformative color scale."); @@ -32,9 +37,13 @@ export const calcColorScale = (colorBy, controls, tree, treeToo, metadata) => { const colorings = metadata.colorings; const treeTooNodes = treeToo ? treeToo.nodes : undefined; let continuous = false; - let colorScale, legendValues, legendBounds, legendLabels, domain; + let colorScale: (val: any) => string; + let legendValues: LegendValues; + let legendBounds: LegendBounds; + let legendLabels: LegendLabels; + let domain: unknown[]; - let genotype; + let genotype: Genotype; if (isColorByGenotype(colorBy)) { genotype = decodeColorByGenotype(colorBy); setGenotype(tree.nodes, genotype.gene, genotype.positions, metadata.rootSequence); /* modifies nodes recursively */ @@ -42,7 +51,7 @@ export const calcColorScale = (colorBy, controls, tree, treeToo, metadata) => { setGenotype(treeToo.nodes, genotype.gene, genotype.positions, metadata.rootSequenceSecondTree); } } - const scaleType = genotype ? "categorical" : colorings[colorBy].type; + const scaleType: ScaleType = genotype ? "categorical" : colorings[colorBy].type; if (genotype) { ({legendValues, colorScale} = createScaleForGenotype(tree.nodes, treeToo?.nodes, genotype.aa)); domain = [...legendValues]; @@ -124,14 +133,23 @@ export const calcColorScale = (colorBy, controls, tree, treeToo, metadata) => { } }; -export function createNonContinuousScaleFromProvidedScaleMap(colorBy, providedScale, t1nodes, t2nodes) { +export function createNonContinuousScaleFromProvidedScaleMap( + colorBy: string, + providedScale: [string, string][], + t1nodes: ReduxNode[], + t2nodes: ReduxNode[] | undefined, +): { + continuous: boolean + legendValues: LegendValues + colorScale: ColorScale["scale"] +} { // console.log(`calcColorScale: colorBy ${colorBy} provided us with a scale (list of [trait, hex])`); if (!Array.isArray(providedScale)) { throw new Error(`${colorBy} has defined a scale which wasn't an array`); } /* The providedScale may have duplicate names (not ideal, but it happens). In this case we should filter out duplicates (taking the first of the duplicates is fine) & print a console warning */ - const colorMap = new Map(); + const colorMap = new Map(); for (const [name, colorHex] of providedScale) { if (colorMap.has(name)) { console.warn(`User provided color scale contained a duplicate entry for ${colorBy}→${name} which is ignored.`); @@ -142,7 +160,7 @@ export function createNonContinuousScaleFromProvidedScaleMap(colorBy, providedSc let domain = Array.from(colorMap).map((x) => x[0]); /* create shades of grey for values in the tree which weren't defined in the provided scale */ - const extraVals = getExtraVals(t1nodes, t2nodes, colorBy, domain); + const extraVals: string[] = getExtraVals(t1nodes, t2nodes, colorBy, domain); if (extraVals.length) { // we must add these to the domain + provide a color value domain = domain.concat(extraVals); const extraColors = createListOfColors(extraVals.length, ["#BDC3C6", "#868992"]); @@ -153,11 +171,18 @@ export function createNonContinuousScaleFromProvidedScaleMap(colorBy, providedSc return { continuous: false, /* colorMaps can't (yet) be continuous */ legendValues: domain, - colorScale: (val) => (colorMap.get(val) || unknownColor) + colorScale: (val: string) => (colorMap.get(val) || unknownColor) }; } -function createScaleForGenotype(t1nodes, t2nodes, aaGenotype) { +function createScaleForGenotype( + t1nodes: ReduxNode[], + t2nodes: ReduxNode[], + aaGenotype: boolean, +): { + colorScale: ColorScale["scale"] + legendValues: LegendValues +} { const legendValues = orderOfGenotypeAppearance(t1nodes, t2nodes, aaGenotype); const trueValues = aaGenotype ? legendValues.filter((x) => x !== "X" && x !== "-" && x !== "") : @@ -175,12 +200,21 @@ function createScaleForGenotype(t1nodes, t2nodes, aaGenotype) { range.push(rgb(102, 102, 102)); } return { - colorScale: scaleOrdinal().domain(domain).range(range), + colorScale: scaleOrdinal().domain(domain).range(range), legendValues }; } -function createOrdinalScale(colorBy, t1nodes, t2nodes) { +function createOrdinalScale( + colorBy: string, + t1nodes: ReduxNode[], + t2nodes: ReduxNode[], +): { + continuous: boolean + colorScale: ColorScale["scale"] + legendValues: LegendValues + legendBounds: LegendBounds +} { /* currently, ordinal scales are only implemented for those with integer values. TODO: we should be able to have non-numerical ordinal scales (e.g. `["small", "medium", "large"]`) however we currently cannot specify this ordering @@ -189,7 +223,8 @@ function createOrdinalScale(colorBy, t1nodes, t2nodes) { let legendValues = getDiscreteValuesFromTree(t1nodes, t2nodes, colorBy); const allInteger = legendValues.every((x) => Number.isInteger(x)); let continuous = false; - let colorScale, legendBounds; + let colorScale: ColorScale["scale"]; + let legendBounds: Record; if (allInteger) { const minMax = getMinMaxFromTree(t1nodes, t2nodes, colorBy); @@ -203,7 +238,7 @@ function createOrdinalScale(colorBy, t1nodes, t2nodes) { duplication, as this is identical to that of the continuous scale below */ console.warn("Using a continous scale as there are too many values in the ordinal scale"); continuous = true; - const scale = scaleLinear().domain(genericDomain.map((d) => minMax[0] + d * (minMax[1] - minMax[0]))).range(colors[9]); + const scale = scaleLinear().domain(genericDomain.map((d) => minMax[0] + d * (minMax[1] - minMax[0]))).range(colors[9]); colorScale = (val) => isValueValid(val) ? scale(val): unknownColor; const spread = minMax[1] - minMax[0]; const dp = spread > 5 ? 2 : 3; @@ -220,7 +255,17 @@ function createOrdinalScale(colorBy, t1nodes, t2nodes) { return {continuous, colorScale, legendValues, legendBounds}; } -function createContinuousScale(colorBy, providedScale, t1nodes, t2nodes) { +function createContinuousScale( + colorBy: string, + providedScale, + t1nodes: ReduxNode[], + t2nodes: ReduxNode[], +): { + continuous: boolean + colorScale: ColorScale["scale"] + legendBounds: LegendBounds + legendValues: LegendValues +} { const minMax = getMinMaxFromTree(t1nodes, t2nodes, colorBy); @@ -228,7 +273,8 @@ function createContinuousScale(colorBy, providedScale, t1nodes, t2nodes) { const anchorPoints = _validateAnchorPoints(providedScale, (val) => typeof val==="number"); /* make the continuous scale */ - let domain, range; + let domain: number[]; + let range: string[]; if (anchorPoints) { domain = anchorPoints.map((pt) => pt[0]); range = anchorPoints.map((pt) => pt[1]); @@ -236,7 +282,7 @@ function createContinuousScale(colorBy, providedScale, t1nodes, t2nodes) { range = colors[9]; domain = genericDomain.map((d) => minMax[0] + d * (minMax[1] - minMax[0])); } - const scale = scaleLinear().domain(domain).range(range); + const scale = scaleLinear().domain(domain).range(range); const spread = minMax[1] - minMax[0]; const dp = spread > 5 ? 2 : 3; @@ -251,16 +297,27 @@ function createContinuousScale(colorBy, providedScale, t1nodes, t2nodes) { return { continuous: true, - colorScale: (val) => isValueValid(val) ? scale(val) : unknownColor, + colorScale: (val: number) => isValueValid(val) ? scale(val) : unknownColor, legendBounds: createLegendBounds(legendValues), legendValues }; } -function createTemporalScale(colorBy, providedScale, t1nodes, t2nodes) { - - let domain, range; +function createTemporalScale( + colorBy: string, + providedScale, + t1nodes: ReduxNode[], + t2nodes: ReduxNode[], +): { + continuous: boolean + colorScale: ColorScale["scale"] + legendBounds: LegendBounds + legendValues: LegendValues +} { + + let domain: number[]; + let range: string[]; const anchorPoints = _validateAnchorPoints(providedScale, (val) => numDate(val)!==undefined); if (anchorPoints) { domain = anchorPoints.map((pt) => numDate(pt[0])); @@ -288,7 +345,7 @@ function createTemporalScale(colorBy, providedScale, t1nodes, t2nodes) { range = colors[domain.length]; /* use the right number of colours */ } - const scale = scaleLinear().domain(domain).range(range); + const scale = scaleLinear().domain(domain).range(range); const legendValues = anchorPoints ? domain.slice() : domain.slice(1); @@ -309,26 +366,40 @@ function createTemporalScale(colorBy, providedScale, t1nodes, t2nodes) { } -function getMinMaxFromTree(nodes, nodesToo, attr) { +function getMinMaxFromTree( + nodes: ReduxNode[], + nodesToo: ReduxNode[], + attr: string, +): [number, number] { const arr = nodesToo ? nodes.concat(nodesToo) : nodes.slice(); - const vals = arr.map((n) => getTraitFromNode(n, attr)) + const vals: number[] = arr.map((n) => getTraitFromNode(n, attr)) .filter((n) => n !== undefined) .filter((item, i, ar) => ar.indexOf(item) === i) .map((v) => +v); // coerce throw new Error(to numeric return [min(vals), max(vals)]; } -/* this creates a (ramped) list of colours - this is necessary as ordinal scales can't interpolate colours. - range: [a,b], the colours to go between */ -function createListOfColors(n, range) { - const scale = scaleLinear().domain([0, n]) +/** + * this creates a (ramped) list of colours + * this is necessary as ordinal scales can't interpolate colours. + */ +function createListOfColors( + n: number, + + /** the colours to go between */ + range: [string, string], +) { + const scale = scaleLinear().domain([0, n]) .interpolate(interpolateHcl) .range(range); return d3Range(0, n).map(scale); } -function getDiscreteValuesFromTree(nodes, nodesToo, attr) { +function getDiscreteValuesFromTree( + nodes: ReduxNode[], + nodesToo: ReduxNode[] | undefined, + attr: string, +): LegendValues { const stateCount = countTraitsAcrossTree(nodes, [attr], false, false)[attr]; if (nodesToo) { const stateCountSecondTree = countTraitsAcrossTree(nodesToo, [attr], false, false)[attr]; @@ -347,7 +418,12 @@ function getDiscreteValuesFromTree(nodes, nodesToo, attr) { * This code is in this file to help future refactors, as the colorScale code has grown a lot * and could be greatly improved. james, dec 2021 */ -export function getLegendOrder(attr, coloringInfo, nodesA, nodesB) { +export function getLegendOrder( + attr: string, + coloringInfo: ColoringInfo, + nodesA: ReduxNode[], + nodesB: ReduxNode[] | undefined, +): LegendValues { if (isColorByGenotype(attr)) { console.warn("legend ordering for genotypes not yet implemented"); return []; @@ -356,7 +432,7 @@ export function getLegendOrder(attr, coloringInfo, nodesA, nodesB) { console.warn("legend ordering for continuous scales not yet implemented"); return []; } - if (coloringInfo.scale) { /* scale set via JSON */ + if (coloringInfo.scale) { return createNonContinuousScaleFromProvidedScaleMap(attr, coloringInfo.scale, nodesA, nodesB).legendValues; } return getDiscreteValuesFromTree(nodesA, nodesB, attr); @@ -365,17 +441,35 @@ export function getLegendOrder(attr, coloringInfo, nodesA, nodesB) { /** * Dynamically create legend values based on visibility for ordinal and categorical scale types. */ -export function createVisibleLegendValues({colorBy, scaleType, genotype, legendValues, treeNodes, treeTooNodes, visibility, visibilityToo}) { +export function createVisibleLegendValues({ + colorBy, + scaleType, + genotype, + legendValues, + treeNodes, + treeTooNodes, + visibility, + visibilityToo, +}: { + colorBy: string + scaleType: ScaleType + genotype: Genotype + legendValues: LegendValues + treeNodes: ReduxNode[] + treeTooNodes?: ReduxNode[] | null + visibility: Visibility[] + visibilityToo?: Visibility[] +}): LegendValues { if (visibility) { // filter according to scaleType, e.g. continuous is different to categorical which is different to boolean // filtering will involve looping over reduxState.tree.nodes and comparing with reduxState.tree.visibility if (scaleType === "ordinal" || scaleType === "categorical") { - let legendValuesObserved = treeNodes + let legendValuesObserved: LegendValues = treeNodes .filter((n, i) => (!n.hasChildren && visibility[i]===NODE_VISIBLE)) .map((n) => genotype ? n.currentGt : getTraitFromNode(n, colorBy)); // if the 2nd tree is enabled, compute visible legend values and merge the values. if (treeTooNodes && visibilityToo) { - const legendValuesObservedToo = treeTooNodes + const legendValuesObservedToo: LegendValues = treeTooNodes .filter((n, i) => (!n.hasChildren && visibilityToo[i]===NODE_VISIBLE)) .map((n) => genotype ? n.currentGt : getTraitFromNode(n, colorBy)); legendValuesObserved = [...legendValuesObserved, ...legendValuesObservedToo]; @@ -388,33 +482,29 @@ export function createVisibleLegendValues({colorBy, scaleType, genotype, legendV return legendValues.slice(); } -function createDiscreteScale(domain, type) { +function createDiscreteScale(domain: string[], type: ScaleType) { // note: colors[n] has n colors - let colorList; + let colorList: string[]; if (type==="ordinal" || type==="categorical") { /* TODO: use different colours! */ colorList = domain.length < colors.length ? colors[domain.length].slice() : colors[colors.length - 1].slice(); } - const scale = scaleOrdinal().domain(domain).range(colorList); + const scale = scaleOrdinal().domain(domain).range(colorList); return (val) => ((val === undefined || domain.indexOf(val) === -1)) ? unknownColor : scale(val); } -function booleanColorScale(val) { +function booleanColorScale(val: unknown): string { if (!isValueValid(val)) return unknownColor; if (["true", "1", "yes"].includes(String(val).toLowerCase())) return "#4C90C0"; return "#CBB742"; } -/** - * @param {Array} legendValues - * @returns {Record} - */ -function createLegendBounds(legendValues) { - const valBetween = (x0, x1) => x0 + 0.5*(x1-x0); +function createLegendBounds(legendValues: number[]): LegendBounds { + const valBetween = (x0: number, x1: number) => x0 + 0.5*(x1-x0); const len = legendValues.length; - const legendBounds = {}; + const legendBounds: LegendBounds = {}; legendBounds[legendValues[0]] = [-Infinity, valBetween(legendValues[0], legendValues[1])]; for (let i = 1; i < len - 1; i++) { legendBounds[legendValues[i]] = [valBetween(legendValues[i-1], legendValues[i]), valBetween(legendValues[i], legendValues[i+1])]; @@ -423,7 +513,10 @@ function createLegendBounds(legendValues) { return legendBounds; } -function _validateAnchorPoints(providedScale, validator) { +function _validateAnchorPoints( + providedScale: unknown[], + validator: (val: unknown) => boolean, +): unknown[] | false { if (!Array.isArray(providedScale)) return false; const ap = providedScale.filter((item) => Array.isArray(item) && item.length===2 && @@ -436,20 +529,19 @@ function _validateAnchorPoints(providedScale, validator) { /** * Parse the user-defined `legend` for a given coloring to produce legendValues, legendLabels and legendBounds. - * - * @param {Array|undefined} providedLegend JSON-defined `legend` array of objects. Object keys: - * `value` used to compute the legend swatch colour. The type of this depends on the scaleType. - * Continuous scales demand numeric values, however few restrictions are placed on other scales. - * `display` -> string|numeric. Optional. Displayed in the legend. Falls back to `value` if missing. - * `bounds` -> array of 2 numerics. Optional. Custom legendBounds. Only considered for continuous scales. - * @param {Array} currentLegendValues Dynamically generated legendValues (via traversal of tree(s)). - * @param {string} scaleType - * @returns {false|object}. Returned object has keys: - * `legendValues` -> array of numeric legend values for display - * `legendBounds` -> {object|undefined} See `createLegendBounds()` for format - * `legendLabels` -> {Map|undefined} A map of legendValues to a value for display in the legend. */ -function parseUserProvidedLegendData(providedLegend, currentLegendValues, scaleType) { +function parseUserProvidedLegendData( + providedLegend: Legend | undefined, + + /** Dynamically generated legendValues (via traversal of tree(s)). */ + currentLegendValues: LegendValues, + + scaleType: ScaleType, +): { + legendValues: LegendValues + legendLabels: LegendLabels + legendBounds: LegendBounds +} | false { if (!Array.isArray(providedLegend)) return false; if (scaleType==='temporal') { console.error("Auspice currently doesn't allow a JSON-provided 'legend' for temporal colorings, "+ @@ -465,19 +557,19 @@ function parseUserProvidedLegendData(providedLegend, currentLegendValues, scaleT return false; } - const legendValues = data.map((d) => d.value); + const legendValues: LegendValues = data.map((d) => d.value); - const legendLabels = new Map( + const legendLabels: LegendLabels = new Map( data.map((d) => { return (typeof d.display === "string" || typeof d.display === "number") ? [d.value, d.display] : [d.value, d.value]; }) ); - let legendBounds = {}; + let legendBounds: LegendBounds = {}; if (scaleType==="continuous") { const boundArrays = data.map((d) => d.bounds) .filter((b) => Array.isArray(b) && b.length === 2 && typeof b[0] === "number" && typeof b[1] === "number") - .map(([a, b]) => a > b ? [b, a] : [a, b]) // ensure each bound is correctly ordered + .map(([a, b]): [number, number] => a > b ? [b, a] : [a, b]) // ensure each bound is correctly ordered .filter(([a, b], idx, arr) => { // ensure no overlap with previous bounds. for (let i=0; i { +export const determineLegendMatch = ( + /** e.g. "USA" or 2021 */ + selectedLegendItem: string | number, + + /** node (tip) in question */ + node: ReduxNode, + + /** used to get the value of the attribute being used for colouring */ + colorScale: ColorScale +): boolean => { let nodeAttr = getTipColorAttribute(node, colorScale); if (colorScale.scaleType === 'temporal') { nodeAttr = numDate(nodeAttr); @@ -29,34 +37,53 @@ export const determineLegendMatch = (selectedLegendItem: (string|number), node:a /** * Does the `node`s trait for the given `geoResolution` match the `geoValueToMatch`? - * @param {object} node - node (tip) in question - * @param {string} geoResolution - Geographic resolution (e.g. "division", "country", "region") - * @param {string} geoValueToMatch - Value to match (e.g. "New Zealand", "New York") - * @returns bool */ -const determineLocationMatch = (node:any, geoResolution:string, geoValueToMatch:string) => { +const determineLocationMatch = ( + /** node (tip) in question */ + node: ReduxNode, + + /** Geographic resolution (e.g. "division", "country", "region") */ + geoResolution: string, + + /** Value to match (e.g. "New Zealand", "New York") */ + geoValueToMatch: string +): boolean => { return geoValueToMatch === getTraitFromNode(node, geoResolution); }; /** * produces the array of tip radii - if nothing's selected this is the hardcoded tipRadius * if there's a selectedLegendItem, then values will be small (like normal) or big (for those tips selected) -* @param selectedLegendItem - value of the selected tip attribute (numeric or string) OPTIONAL -* @param tipSelectedIdx - idx of a single tip to show with increased tipRadius OPTIONAL -* @param colorScale - node (tip) in question -* @param tree * @returns null (if data not ready) or array of tip radii */ -export const calcTipRadii = ( - {tipSelectedIdx = false, selectedLegendItem = false, geoFilter = [], searchNodes = false, colorScale, tree}: - {tipSelectedIdx:(number|false), selectedLegendItem:(number|string|false), geoFilter:([string, string]|[]), searchNodes:any, colorScale:any, tree:any} -) => { +export const calcTipRadii = ({ + tipSelectedIdx = false, + selectedLegendItem = false, + geoFilter = [], + searchNodes = false, + colorScale, + tree +}: { + /** idx of a single tip to show with increased tipRadius */ + tipSelectedIdx?: number | false + + /** value of the selected tip attribute (numeric or string) */ + selectedLegendItem?: number | string | false + + geoFilter?: [string, string] | [] + + searchNodes?: PhyloNode[] | false + + colorScale: ColorScale + + tree: TreeState +}): number[] | null => { if (selectedLegendItem !== false && tree && tree.nodes) { - return tree.nodes.map((d:any) => determineLegendMatch(selectedLegendItem, d, colorScale) ? tipRadiusOnLegendMatch : tipRadius); + return tree.nodes.map((d) => determineLegendMatch(selectedLegendItem, d, colorScale) ? tipRadiusOnLegendMatch : tipRadius); } else if (geoFilter.length===2 && tree && tree.nodes) { - return tree.nodes.map((d:any) => determineLocationMatch(d, geoFilter[0], geoFilter[1]) ? tipRadiusOnLegendMatch : tipRadius); + return tree.nodes.map((d) => determineLocationMatch(d, geoFilter[0], geoFilter[1]) ? tipRadiusOnLegendMatch : tipRadius); } else if (searchNodes) { - return tree.nodes.map((d:any) => d.name.toLowerCase().includes(searchNodes) ? tipRadiusOnLegendMatch : tipRadius); + return tree.nodes.map((d) => d.name.toLowerCase().includes(searchNodes) ? tipRadiusOnLegendMatch : tipRadius); } else if (tipSelectedIdx) { const radii = tree.nodes.map(() => tipRadius); radii[tipSelectedIdx] = tipRadiusOnLegendMatch + 3; diff --git a/src/util/treeCountingHelpers.js b/src/util/treeCountingHelpers.ts similarity index 68% rename from src/util/treeCountingHelpers.js rename to src/util/treeCountingHelpers.ts index 1c440cb03..a26217224 100644 --- a/src/util/treeCountingHelpers.js +++ b/src/util/treeCountingHelpers.ts @@ -1,16 +1,25 @@ +import { Colorings } from "../metadata"; +import { ReduxNode, TraitCounts, Visibility } from "../reducers/tree/types"; import { NODE_VISIBLE } from "./globals"; import { getTraitFromNode } from "./treeMiscHelpers"; /** * traverse the tree to get state counts for supplied traits. -* @param {Array} nodes - list of nodes -* @param {Array} traits - list of traits to count across the tree -* @param {Array | false} visibility - if Array provided then only consider visible nodes. If false, consider all nodes. -* @param {bool} terminalOnly - only consider terminal / leaf nodes? -* @return {obj} keys: the traits. Values: an object mapping trait values -> INT */ -export const countTraitsAcrossTree = (nodes, traits, visibility, terminalOnly) => { - const counts = {}; +export const countTraitsAcrossTree = ( + /** list of nodes */ + nodes: ReduxNode[], + + /** list of traits to count across the tree */ + traits: string[], + + /** if Array provided then only consider visible nodes. If false, consider all nodes. */ + visibility: Visibility[] | false, + + /** only consider terminal / leaf nodes? */ + terminalOnly: boolean, +): TraitCounts => { + const counts: TraitCounts = {}; traits.forEach((trait) => {counts[trait] = new Map();}); nodes.forEach((node) => { @@ -39,16 +48,16 @@ export const countTraitsAcrossTree = (nodes, traits, visibility, terminalOnly) = * Includes a hardcoded list of trait names we will ignore, as well as any trait * which we know is continuous (via a colouring definition) because the * filtering is not designed for these kinds of data (yet). - * @param {Array} nodes - * @param {Object} colorings - * @returns {Array} list of trait names */ -export const gatherTraitNames = (nodes, colorings) => { +export const gatherTraitNames = ( + nodes: ReduxNode[], + colorings: Colorings, +): string[] => { const ignore = new Set([ 'num_date', ...Object.entries(colorings).filter(([_, info]) => info.type==='continuous').map(([name, _]) => name), ]) - const names = new Set(); + const names = new Set(); for (const node of nodes) { if (node.hasChildren) continue; for (const traitName in node.node_attrs || {}) { @@ -63,12 +72,15 @@ export const gatherTraitNames = (nodes, colorings) => { } /** -* for each node, calculate the number of subtending tips which are visible -* side effects: n.tipCount for each node -* @param {Node} node - deserialized JSON root to begin traversal -* @param {Array} visibility -*/ -export const calcTipCounts = (node, visibility) => { + * for each node, calculate the number of subtending tips which are visible + * side effects: n.tipCount for each node + */ +export const calcTipCounts = ( + /** deserialized JSON root to begin traversal */ + node: ReduxNode, + + visibility: Visibility[], +): void => { node.tipCount = 0; if (typeof node.children !== "undefined") { for (let i = 0; i < node.children.length; i++) { @@ -82,9 +94,8 @@ export const calcTipCounts = (node, visibility) => { /** * calculate the total number of tips in the tree - * @param {Array} nodes flat list of all nodes */ -export const calcTotalTipsInTree = (nodes) => { +export const calcTotalTipsInTree = (nodes: ReduxNode[]): number => { let count = 0; nodes.forEach((n) => { if (!n.hasChildren) count++; @@ -95,9 +106,11 @@ export const calcTotalTipsInTree = (nodes) => { /** * for each node, calculate the number of subtending tips (alive or dead) * side effects: n.fullTipCount for each node -* @param {Node} node - deserialized JSON root to begin traversal */ -export const calcFullTipCounts = (node) => { +export const calcFullTipCounts = ( + /** deserialized JSON root to begin traversal */ + node: ReduxNode, +): void => { node.fullTipCount = 0; if (typeof node.children !== "undefined") { for (let i = 0; i < node.children.length; i++) { diff --git a/src/util/treeJsonProcessing.js b/src/util/treeJsonProcessing.ts similarity index 75% rename from src/util/treeJsonProcessing.js rename to src/util/treeJsonProcessing.ts index 39e7fc91a..a4acaff78 100644 --- a/src/util/treeJsonProcessing.js +++ b/src/util/treeJsonProcessing.ts @@ -1,24 +1,25 @@ import { getDefaultTreeState } from "../reducers/tree"; +import { Mutations, ReduxNode, TreeState } from "../reducers/tree/types"; import { getVaccineFromNode, getTraitFromNode, getDivFromNode } from "./treeMiscHelpers"; import { calcFullTipCounts } from "./treeCountingHelpers"; const pseudoRandomName = () => (Math.random()*1e32).toString(36).slice(0, 6); /** - * Adds certain properties to the nodes array - for each node in nodes it adds - * node.fullTipCount - see calcFullTipCounts() description - * node.hasChildren {bool} - * node.arrayIdx {integer} - the index of the node in the nodes array - * @param {array} nodes redux tree nodes - * @return {Object} ret - * @return {Set} ret.nodeAttrKeys collection of all `node_attr` keys whose values are Objects - * @return {Array} ret.nodes input array (kinda unnecessary) - * - * side-effects: node.hasChildren (bool) and node.arrayIdx (INT) for each node in nodes + * Adds the following properties to each node: + * - fullTipCount + * - hasChildren + * - arrayIdx */ -const processNodes = (nodes) => { - const nodeNamesSeen = new Set(); - const nodeAttrKeys = new Set(); +const processNodes = (nodes: ReduxNode[]): { + /** collection of all `node_attr` keys whose values are Objects */ + nodeAttrKeys: Set + + /** input array (kinda unnecessary) */ + nodes: ReduxNode[] +} => { + const nodeNamesSeen = new Set(); + const nodeAttrKeys = new Set(); calcFullTipCounts(nodes[0]); /* recursive. Uses d.children */ nodes.forEach((d, idx) => { d.arrayIdx = idx; /* set an index so that we can access visibility / nodeColors if needed */ @@ -50,10 +51,9 @@ const processNodes = (nodes) => { /** * Scan the tree for `node.branch_attrs.labels` dictionaries and collect all available * (These are the options for the "Branch Labels" sidebar dropdown) - * @param {Array} nodes tree nodes (flat) */ -const processBranchLabelsInPlace = (nodes) => { - const availableBranchLabels = new Set(); +const processBranchLabelsInPlace = (nodes: ReduxNode[]): string[] => { + const availableBranchLabels = new Set(); nodes.forEach((n) => { if (n.branch_attrs && n.branch_attrs.labels) { Object.keys(n.branch_attrs.labels) @@ -68,8 +68,11 @@ const processBranchLabelsInPlace = (nodes) => { }; -const makeSubtreeRootNode = (nodesArray, subtreeIndicies) => { - const node = { +const makeSubtreeRootNode = ( + nodesArray: ReduxNode[], + subtreeIndicies: number[], +): ReduxNode => { + const node: ReduxNode = { name: "__ROOT", node_attrs: {hidden: "always"}, children: subtreeIndicies.map((idx) => nodesArray[idx]) @@ -87,11 +90,16 @@ const makeSubtreeRootNode = (nodesArray, subtreeIndicies) => { * Pre-order tree traversal visits each node using stack. * Checks if leaf node based on node.children * pushes all children into stack and continues traversal. -* @param root - deserialized JSON root to begin traversal -* @returns array - final array of nodes in order with no dups */ -const flattenTree = (root) => { - const stack = [], array = []; +const flattenTree = ( + /** deserialized JSON root to begin traversal */ + root: ReduxNode, +): ReduxNode[] => { + const stack: ReduxNode[] = []; + + /** final array of nodes in order with no dups */ + const array: ReduxNode[] = []; + stack.push(root); while (stack.length !== 0) { const node = stack.pop(); @@ -111,11 +119,13 @@ const flattenTree = (root) => { * Pre-order tree traversal visits each node using stack. * Checks if leaf node based on node.children * pushes all children into stack and continues traversal. -* @param root - deserialized JSON root to begin traversal */ -const appendParentsToTree = (root) => { +const appendParentsToTree = ( + /** deserialized JSON root to begin traversal */ + root: ReduxNode, +): void => { root.parent = root; - const stack = []; + const stack: ReduxNode[] = []; stack.push(root); while (stack.length !== 0) { @@ -133,9 +143,8 @@ const appendParentsToTree = (root) => { * Currently this is limited in scope, but is intended to parse * information on a branch_attr indicating information about minor/ * major parents (e.g. recombination, subtree position in another tree). - * @param {Array} nodes */ -const addParentInfo = (nodes) => { +const addParentInfo = (nodes: ReduxNode[]): void => { nodes.forEach((n) => { n.parentInfo = { original: n.parent @@ -145,16 +154,12 @@ const addParentInfo = (nodes) => { /** * Collects all mutations on the tree - * @param {Node[]} nodesArray - * @return {Object} - * keys are mutations in gene:fromPosTo format (e.g. nuc:A123T) - * values are integers representing occurrences on tree * @todo The original remit of this function was for homoplasy detection. * If storing all the mutations becomes an issue, we may be able use an array * of mutations observed more than once. */ -const collectObservedMutations = (nodesArray) => { - const mutations = {}; +const collectObservedMutations = (nodesArray: ReduxNode[]): Mutations => { + const mutations: Mutations = {}; nodesArray.forEach((n) => { if (!n.branch_attrs || !n.branch_attrs.mutations) return; Object.entries(n.branch_attrs.mutations).forEach(([gene, muts]) => { @@ -166,9 +171,9 @@ const collectObservedMutations = (nodesArray) => { return mutations; }; -export const treeJsonToState = (treeJSON) => { +export const treeJsonToState = (treeJSON): TreeState => { const trees = Array.isArray(treeJSON) ? treeJSON : [treeJSON]; - const nodesArray = []; + const nodesArray: ReduxNode[] = []; const subtreeIndicies = []; for (const treeRootNode of trees) { appendParentsToTree(treeRootNode); @@ -184,7 +189,13 @@ export const treeJsonToState = (treeJSON) => { }); const availableBranchLabels = processBranchLabelsInPlace(nodesArray); const observedMutations = collectObservedMutations(nodesArray); - return Object.assign({}, getDefaultTreeState(), { - nodes, nodeAttrKeys, vaccines, observedMutations, availableBranchLabels, loaded: true - }); + return { + ...getDefaultTreeState(), + nodes, + nodeAttrKeys, + vaccines, + observedMutations, + availableBranchLabels, + loaded: true, + }; }; From 402a5e65c85988b6ce565b9333ed2adb9b1df365 Mon Sep 17 00:00:00 2001 From: Victor Lin <13424970+victorlin@users.noreply.github.com> Date: Tue, 5 Nov 2024 11:36:06 -0800 Subject: [PATCH 07/31] Add other tree-related types These are not directly used by other TypeScript files, but I've inferred them from existing JavaScript files. --- src/reducers/controls.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/reducers/controls.ts b/src/reducers/controls.ts index 13f95f98f..15e98397d 100644 --- a/src/reducers/controls.ts +++ b/src/reducers/controls.ts @@ -109,6 +109,7 @@ export interface BasicControlsState { colorBy: string colorByConfidence: boolean coloringsPresentOnTree?: Set + coloringsPresentOnTreeWithConfidence?: Set colorScale?: ColorScale dateMax: string dateMaxNumeric: number From b1b24883fb95f9e1faf0fb4adba78de1ce407f85 Mon Sep 17 00:00:00 2001 From: Victor Lin <13424970+victorlin@users.noreply.github.com> Date: Tue, 29 Oct 2024 17:31:56 -0700 Subject: [PATCH 08/31] tsconfig: Disable strictNullChecks The conversion done in the previous commits came with hundreds of violations of this rule. While this could be addressed by better types, it will take some work and can be deferred. A new issue/PR will be created after this is merged. --- tsconfig.json | 1 + 1 file changed, 1 insertion(+) diff --git a/tsconfig.json b/tsconfig.json index 71ded34a2..c6275dc6e 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -32,6 +32,7 @@ Visit https://aka.ms/tsconfig.json for a detailed list of options. /* Type Checking */ "strict": true, /* Enable all strict type-checking options. */ + "strictNullChecks": false, /* Allow unhandled false/null/undefined values to make incremental TypeScript adoption easier. */ "noImplicitAny": false, /* Allow implicit any to make incremental TypeScript adoption easier. */ "noUnusedLocals": true, /* Enable error reporting when a local variables aren't read. */ "noUnusedParameters": true, /* Raise an error when a function parameter isn't read */ From 2e96210df71032a6ac59dfe6116b042bf0591236 Mon Sep 17 00:00:00 2001 From: Victor Lin <13424970+victorlin@users.noreply.github.com> Date: Tue, 5 Nov 2024 15:32:23 -0800 Subject: [PATCH 09/31] Add override keyword for React lifecycle methods To satisfy the TypeScript rule noImplicitOverride. --- src/components/tree/tree.tsx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/components/tree/tree.tsx b/src/components/tree/tree.tsx index 04c89b717..3d304b5bb 100644 --- a/src/components/tree/tree.tsx +++ b/src/components/tree/tree.tsx @@ -73,7 +73,7 @@ export class TreeComponent extends React.Component = {}; @@ -87,7 +87,7 @@ export class TreeComponent extends React.Component = {}; let rightTreeUpdated = false; @@ -124,7 +124,7 @@ export class TreeComponent extends React.Component Date: Fri, 25 Oct 2024 16:28:11 -0700 Subject: [PATCH 10/31] Add _ prefix for unused variables To satisfy the ESLint rule @typescript-eslint/no-unused-vars. --- src/actions/tree.ts | 2 +- src/components/tree/phyloTree/renderers.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/actions/tree.ts b/src/actions/tree.ts index 321c68b61..d996072f6 100644 --- a/src/actions/tree.ts +++ b/src/actions/tree.ts @@ -398,7 +398,7 @@ const _traverseAndCreateSubtrees = ( subtreeRootNode.parent = root; }); node.unexplodedChildren = originalChildren; - node.children = node.children.filter((c, idx) => { + node.children = node.children.filter((_c, idx) => { return !childrenToPrune.includes(idx); }); /* it may be the case that the node now has no children (they're all subtrees!) */ diff --git a/src/components/tree/phyloTree/renderers.ts b/src/components/tree/phyloTree/renderers.ts index d3c648d5d..1d8b87bb0 100644 --- a/src/components/tree/phyloTree/renderers.ts +++ b/src/components/tree/phyloTree/renderers.ts @@ -396,7 +396,7 @@ const handleBranchHoverColor = ( d: PhyloNode, /** colour of the parent (start of the branch) */ - c1: string, + _c1: string, /** colour of the node (end of the branch) */ c2: string, From 75fd6ad1250e0c1add95722bd72d427b59458bda Mon Sep 17 00:00:00 2001 From: Victor Lin <13424970+victorlin@users.noreply.github.com> Date: Tue, 5 Nov 2024 15:30:22 -0800 Subject: [PATCH 11/31] Use exported Layout type Define the list of layouts in a single place. --- src/components/controls/toggle-focus.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/components/controls/toggle-focus.tsx b/src/components/controls/toggle-focus.tsx index 06da849c5..c48fadf93 100644 --- a/src/components/controls/toggle-focus.tsx +++ b/src/components/controls/toggle-focus.tsx @@ -4,13 +4,14 @@ import { FaInfoCircle } from "react-icons/fa"; import Toggle from "./toggle"; import { SidebarIconContainer, StyledTooltip } from "./styles"; import { TOGGLE_FOCUS } from "../../actions/types"; +import { Layout } from "../../reducers/controls"; import { AppDispatch, RootState } from "../../store"; function ToggleFocus({ tooltip, focus, layout, dispatch, mobileDisplay }: { tooltip: React.ReactElement; focus: boolean; - layout: "rect" | "radial" | "unrooted" | "clock" | "scatter"; + layout: Layout; dispatch: AppDispatch; mobileDisplay: boolean; }) { From e02e0cefb81ada5f1783452344a651445d10e8be Mon Sep 17 00:00:00 2001 From: Victor Lin <13424970+victorlin@users.noreply.github.com> Date: Fri, 25 Oct 2024 15:46:09 -0700 Subject: [PATCH 12/31] Use Math.trunc instead of parseInt Previous usage of parseInt was to convert float to integer. However, this isn't the intended usage of parseInt, which is made apparent with type checking - the parameter is expected to be a string type. Replace it with Math.trunc which is intended for converting float to integer. --- src/util/colorScale.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/util/colorScale.ts b/src/util/colorScale.ts index 87fb5a5eb..1cd4e3c77 100644 --- a/src/util/colorScale.ts +++ b/src/util/colorScale.ts @@ -338,7 +338,7 @@ function createTemporalScale( vals = vals.sort(); domain = [rootDate]; const n = 10; - const spaceBetween = parseInt(vals.length / (n - 1), 10); + const spaceBetween = Math.trunc(vals.length / (n - 1)); for (let i = 0; i < (n-1); i++) domain.push(vals[spaceBetween*i]); domain.push(vals[vals.length-1]); domain = [...new Set(domain)]; /* filter to unique values only */ From a79226e94c8f509e4cc2201039332d1e3f735738 Mon Sep 17 00:00:00 2001 From: Victor Lin <13424970+victorlin@users.noreply.github.com> Date: Mon, 4 Nov 2024 21:01:43 -0800 Subject: [PATCH 13/31] Use Math.min/max instead of min/max Thse are typed to return a value that is not undefined. --- src/components/tree/phyloTree/layouts.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/components/tree/phyloTree/layouts.ts b/src/components/tree/phyloTree/layouts.ts index ac499a217..d64491f29 100644 --- a/src/components/tree/phyloTree/layouts.ts +++ b/src/components/tree/phyloTree/layouts.ts @@ -1,6 +1,5 @@ /* eslint-disable no-multi-spaces */ /* eslint-disable space-infix-ops */ -import { min, max } from "d3-array"; import { scaleLinear, scalePoint } from "d3-scale"; import { timerStart, timerEnd } from "../../../util/perf"; import { getTraitFromNode, getDivFromNode } from "../../../util/treeMiscHelpers"; @@ -317,7 +316,7 @@ export const setScales = function setScales(this: PhyloTreeType): void { // Force Square: TODO, harmonize with the map to screen const xExtend = width - this.margins.left - this.margins.right; const yExtend = height - this.margins.bottom - this.margins.top; - const minExtend = min([xExtend, yExtend]); + const minExtend = Math.min(xExtend, yExtend); const xSlack = xExtend - minExtend; const ySlack = yExtend - minExtend; this.xScale.range([0.5 * xSlack + this.margins.left, width - 0.5 * xSlack - this.margins.right]); @@ -422,7 +421,7 @@ export const mapToScreen = function mapToScreen(this: PhyloTreeType): void { /* Radial / Unrooted layouts need to be square since branch lengths depend on this */ if (this.layout === "radial" || this.layout === "unrooted") { - const maxSpan = max([spanY, spanX]); + const maxSpan = Math.max(spanY, spanX); const ySlack = (spanX>spanY) ? (spanX-spanY)*0.5 : 0.0; const xSlack = (spanX Date: Fri, 25 Oct 2024 16:35:17 -0700 Subject: [PATCH 14/31] Use separate variables for array and set of legend values This is easier for the TypeScript compiler to understand. --- src/util/colorScale.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/util/colorScale.ts b/src/util/colorScale.ts index 1cd4e3c77..0d5fbac37 100644 --- a/src/util/colorScale.ts +++ b/src/util/colorScale.ts @@ -474,8 +474,8 @@ export function createVisibleLegendValues({ .map((n) => genotype ? n.currentGt : getTraitFromNode(n, colorBy)); legendValuesObserved = [...legendValuesObserved, ...legendValuesObservedToo]; } - legendValuesObserved = new Set(legendValuesObserved); - const visibleLegendValues = legendValues.filter((v) => legendValuesObserved.has(v)); + const legendValuesObservedSet = new Set(legendValuesObserved); + const visibleLegendValues = legendValues.filter((v) => legendValuesObservedSet.has(v)); return visibleLegendValues; } } From aa3058139f841f5605cc3df970e926379c96c431 Mon Sep 17 00:00:00 2001 From: Victor Lin <13424970+victorlin@users.noreply.github.com> Date: Tue, 5 Nov 2024 15:30:54 -0800 Subject: [PATCH 15/31] Use null type for domRefs D3 expects the empty value to be null, not undefined. --- src/components/tree/tree.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/tree/tree.tsx b/src/components/tree/tree.tsx index 3d304b5bb..8c20e696f 100644 --- a/src/components/tree/tree.tsx +++ b/src/components/tree/tree.tsx @@ -36,8 +36,8 @@ export class TreeComponent extends React.Component Date: Fri, 25 Oct 2024 15:55:28 -0700 Subject: [PATCH 16/31] Properly handle unknown errors when creating color scales Previously, there was no guarantee that an unknown error would produce a helpful message. --- src/util/colorScale.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/util/colorScale.ts b/src/util/colorScale.ts index 0d5fbac37..a8966fedd 100644 --- a/src/util/colorScale.ts +++ b/src/util/colorScale.ts @@ -117,7 +117,8 @@ export const calcColorScale = ( }; } catch (err) { /* Catch all errors to avoid app crashes */ - console.error("Error creating color scales. Details:\n", err.message); + const errorMessage = err instanceof Error ? err.message : String(err); + console.error("Error creating color scales. Details:\n", errorMessage); return { scale: () => unknownColor, continuous: false, From 83326a298af55514da0933661c42c87916eb8407 Mon Sep 17 00:00:00 2001 From: Victor Lin <13424970+victorlin@users.noreply.github.com> Date: Fri, 25 Oct 2024 15:49:34 -0700 Subject: [PATCH 17/31] Properly check for stroke-width svgPropsToUpdate is Set, not Set. --- src/components/tree/phyloTree/change.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/tree/phyloTree/change.ts b/src/components/tree/phyloTree/change.ts index e3bbbfad4..992700771 100644 --- a/src/components/tree/phyloTree/change.ts +++ b/src/components/tree/phyloTree/change.ts @@ -418,7 +418,7 @@ export const change = function change( } /* mapToScreen */ if ( - svgPropsToUpdate.has(["stroke-width"]) || + svgPropsToUpdate.has("stroke-width") || newDistance || newLayout || changeNodeOrder || From 25294e899be20256844e26cdfb5d6b550616b6eb Mon Sep 17 00:00:00 2001 From: Victor Lin <13424970+victorlin@users.noreply.github.com> Date: Fri, 25 Oct 2024 16:11:34 -0700 Subject: [PATCH 18/31] Properly check window.event.shiftKey Only KeyboardEvent has the shiftKey property. --- src/components/tree/reactD3Interface/callbacks.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/tree/reactD3Interface/callbacks.ts b/src/components/tree/reactD3Interface/callbacks.ts index b833f95c6..e9c11804f 100644 --- a/src/components/tree/reactD3Interface/callbacks.ts +++ b/src/components/tree/reactD3Interface/callbacks.ts @@ -61,7 +61,7 @@ export const onBranchClick = function onBranchClick(this: TreeComponent, d: Phyl if (this.props.narrativeMode) return; /* if a branch was clicked while holding the shift key, we instead display a node-clicked modal */ - if (window.event.shiftKey) { + if (window.event instanceof KeyboardEvent && window.event.shiftKey) { // no need to dispatch a filter action this.props.dispatch({type: SELECT_NODE, name: d.n.name, idx: d.n.arrayIdx, isBranch: true, treeId: d.that.id}) return; From aedce7e99bb4679f1ecf5301ee34a1177e38a4b7 Mon Sep 17 00:00:00 2001 From: Victor Lin <13424970+victorlin@users.noreply.github.com> Date: Fri, 25 Oct 2024 15:48:16 -0700 Subject: [PATCH 19/31] Properly set tipSelectedIdx calcTipRadii takes a key name of tipSelectedIdx. Previously, the value of idx was unused. --- src/actions/tree.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/actions/tree.ts b/src/actions/tree.ts index d996072f6..84e3f60a0 100644 --- a/src/actions/tree.ts +++ b/src/actions/tree.ts @@ -259,7 +259,7 @@ export const updateTipRadii = ( d.data = calcTipRadii({tipSelectedIdx, colorScale, tree}); if (tt) { const idx = strainNameToIdx(treeToo.nodes, tree.nodes[tipSelectedIdx].name); - d.dataToo = calcTipRadii({idx, colorScale, tree: treeToo}); + d.dataToo = calcTipRadii({tipSelectedIdx: idx, colorScale, tree: treeToo}); } } else { d.data = calcTipRadii({selectedLegendItem, geoFilter, searchNodes, colorScale, tree}); From 02b72069b8c82ae15aa524322376eccb29d27236 Mon Sep 17 00:00:00 2001 From: Victor Lin <13424970+victorlin@users.noreply.github.com> Date: Fri, 25 Oct 2024 15:51:19 -0700 Subject: [PATCH 20/31] Properly set timeSliceHasPotentiallyChanged This should be a boolean, not whatever value newDistance is when defined. --- src/components/tree/phyloTree/change.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/tree/phyloTree/change.ts b/src/components/tree/phyloTree/change.ts index 992700771..f22f2edd7 100644 --- a/src/components/tree/phyloTree/change.ts +++ b/src/components/tree/phyloTree/change.ts @@ -436,7 +436,7 @@ export const change = function change( } const extras: Extras = { removeConfidences, showConfidences, newBranchLabellingKey }; - extras.timeSliceHasPotentiallyChanged = changeVisibility || newDistance; + extras.timeSliceHasPotentiallyChanged = changeVisibility || newDistance !== undefined; extras.hideTipLabels = animationInProgress || newTipLabelKey === 'none'; if (useModifySVGInStages) { this.modifySVGInStages(elemsToUpdate, svgPropsToUpdate, transitionTime, 1000, extras); From 5b81e889ce841022347b194dec64a62dada6051d Mon Sep 17 00:00:00 2001 From: Victor Lin <13424970+victorlin@users.noreply.github.com> Date: Mon, 28 Oct 2024 14:55:15 -0700 Subject: [PATCH 21/31] Properly set isSubtreeRoot This should be a boolean, not whatever value ReduxNode.parentInfo.original is when defined. --- src/components/tree/phyloTree/helpers.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/tree/phyloTree/helpers.ts b/src/components/tree/phyloTree/helpers.ts index 32c59cc57..9ff1417d8 100644 --- a/src/components/tree/phyloTree/helpers.ts +++ b/src/components/tree/phyloTree/helpers.ts @@ -263,7 +263,7 @@ export function guessAreMutationsPerSite( * Is the node a subtree root node? (implies that we have either exploded trees or * the dataset has multiple subtrees to display) */ -const isSubtreeRoot = (n: ReduxNode): boolean => (n.parent.name === "__ROOT" && n.parentInfo.original); +const isSubtreeRoot = (n: ReduxNode): boolean => (n.parent.name === "__ROOT" && n.parentInfo.original !== undefined); /** * Gets the parent node to be used for stem / branch calculation. From 79608a53b3c36d19702562d9543decc72a8fb1d4 Mon Sep 17 00:00:00 2001 From: Victor Lin <13424970+victorlin@users.noreply.github.com> Date: Fri, 25 Oct 2024 15:53:39 -0700 Subject: [PATCH 22/31] Properly set PhyloNode.branch to [string, string] Not [string], [], [[string], [string]], or [string, [string]]. --- src/components/tree/phyloTree/layouts.ts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/components/tree/phyloTree/layouts.ts b/src/components/tree/phyloTree/layouts.ts index d64491f29..e137eb5ad 100644 --- a/src/components/tree/phyloTree/layouts.ts +++ b/src/components/tree/phyloTree/layouts.ts @@ -477,11 +477,11 @@ export const mapToScreen = function mapToScreen(this: PhyloTreeType): void { if (this.scatterVariables.showBranches) { this.nodes.forEach((d) => { d.branch = d.xBase===hiddenXPosition || d.xTip===hiddenXPosition || d.yBase===hiddenYPosition || d.yTip===hiddenYPosition ? - [""] : + ["", ""] : [" M "+d.xBase.toString()+","+d.yBase.toString()+" L "+d.xTip.toString()+","+d.yTip.toString(), ""]; }); } else { - this.nodes.forEach((d) => {d.branch=[];}); + this.nodes.forEach((d) => {d.branch=["", ""];}); } } else if (this.layout==="rect") { this.nodes.forEach((d) => { // d is a @@ -491,8 +491,8 @@ export const mapToScreen = function mapToScreen(this: PhyloTreeType): void { // So we add a tiny amount of jitter (e.g 1/1000px) to the horizontal line (d.branch[0]) // see https://stackoverflow.com/questions/13223636/svg-gradient-for-perfectly-horizontal-path d.branch =[ - [` M ${d.xBase - stem_offset},${d.yBase} L ${d.xTip},${d.yTip+0.01}`], - [` M ${d.xTip},${stemRange[0]} L ${d.xTip},${stemRange[1]}`] + ` M ${d.xBase - stem_offset},${d.yBase} L ${d.xTip},${d.yTip+0.01}`, + ` M ${d.xTip},${stemRange[0]} L ${d.xTip},${stemRange[1]}` ]; if (this.params.confidence) { d.confLine =` M ${this.xScale(d.conf[0])},${d.yBase} L ${this.xScale(d.conf[1])},${d.yTip}`; @@ -508,11 +508,11 @@ export const mapToScreen = function mapToScreen(this: PhyloTreeType): void { " L "+d.xTip.toString()+" "+d.yTip.toString(), "" ]; if (d.n.hasChildren) { - d.branch[1] =[" M "+this.xScale(d.xCBarStart).toString()+" "+this.yScale(d.yCBarStart).toString()+ + d.branch[1] = " M "+this.xScale(d.xCBarStart).toString()+" "+this.yScale(d.yCBarStart).toString()+ " A "+(this.xScale(d.depth)-this.xScale(offset)).toString()+" "+ (this.yScale(d.depth)-this.yScale(offset)).toString()+ " 0 "+(d.smallBigArc?"1 ":"0 ") +" 1 "+ - " "+this.xScale(d.xCBarEnd).toString()+","+this.yScale(d.yCBarEnd).toString()]; + " "+this.xScale(d.xCBarEnd).toString()+","+this.yScale(d.yCBarEnd).toString(); } }); } From 9d57be84bce5cb45315a64514fdd88d3a5cd5b8a Mon Sep 17 00:00:00 2001 From: Victor Lin <13424970+victorlin@users.noreply.github.com> Date: Fri, 25 Oct 2024 15:52:13 -0700 Subject: [PATCH 23/31] Remove unused parameter to numDate The extra value `true` had no effect. --- src/components/tree/phyloTree/layouts.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/tree/phyloTree/layouts.ts b/src/components/tree/phyloTree/layouts.ts index e137eb5ad..3ddeaba2c 100644 --- a/src/components/tree/phyloTree/layouts.ts +++ b/src/components/tree/phyloTree/layouts.ts @@ -100,7 +100,7 @@ export const scatterplotLayout = function scatterplotLayout(this: PhyloTreeType) d.x = getTraitFromNode(d.n, this.scatterVariables.x); d.px = getTraitFromNode(stemParent(d.n), this.scatterVariables.x); if (this.scatterVariables.xTemporal) { - [d.x, d.px] = [numDate(d.x, true), numDate(d.px, true)] + [d.x, d.px] = [numDate(d.x), numDate(d.px)] } } // set y and parent values @@ -116,7 +116,7 @@ export const scatterplotLayout = function scatterplotLayout(this: PhyloTreeType) d.y = getTraitFromNode(d.n, this.scatterVariables.y); d.py = getTraitFromNode(stemParent(d.n), this.scatterVariables.y); if (this.scatterVariables.yTemporal) { - [d.y, d.py] = [numDate(d.y, true), numDate(d.py, true)] + [d.y, d.py] = [numDate(d.y), numDate(d.py)] } } } From 5803970824e36ef471084f25170f19ee34610918 Mon Sep 17 00:00:00 2001 From: Victor Lin <13424970+victorlin@users.noreply.github.com> Date: Tue, 29 Oct 2024 15:55:31 -0700 Subject: [PATCH 24/31] Remove unused mapLegendOpen, treeLegendOpen from controls state These were initialized with default values but unused in the code. --- src/reducers/controls.ts | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/reducers/controls.ts b/src/reducers/controls.ts index 15e98397d..39c3de277 100644 --- a/src/reducers/controls.ts +++ b/src/reducers/controls.ts @@ -126,7 +126,6 @@ export interface BasicControlsState { mapAnimationDurationInMilliseconds: number mapAnimationShouldLoop: boolean mapAnimationStartDate: unknown - mapLegendOpen?: boolean modal: 'download' | 'linkOut' | null normalizeFrequencies: boolean panelLayout: string @@ -145,7 +144,6 @@ export interface BasicControlsState { sidebarOpen: boolean temporalConfidence: TemporalConfidence tipLabelKey: string | symbol - treeLegendOpen?: boolean zoomMax?: number zoomMin?: number } @@ -235,8 +233,6 @@ export const getDefaultControlsState = (): ControlsState => { zoomMax: undefined, branchLengthsToDisplay: "divAndDate", sidebarOpen: initialSidebarState.sidebarOpen, - treeLegendOpen: undefined, - mapLegendOpen: undefined, showOnlyPanels: false, showTransmissionLines: true, normalizeFrequencies: true, From 4d2e6336e6732ec6f161f5e077517b38144e7bf3 Mon Sep 17 00:00:00 2001 From: Victor Lin <13424970+victorlin@users.noreply.github.com> Date: Mon, 4 Nov 2024 16:08:15 -0800 Subject: [PATCH 25/31] Remove unused selectedNode from state Follow-up to "Lift node-selected modal to redux state" (f7e944dd9). This is left over from before we shifted the selected node to redux state. --- src/components/tree/reactD3Interface/change.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/components/tree/reactD3Interface/change.ts b/src/components/tree/reactD3Interface/change.ts index 90151a824..24c309b60 100644 --- a/src/components/tree/reactD3Interface/change.ts +++ b/src/components/tree/reactD3Interface/change.ts @@ -121,7 +121,6 @@ export const changePhyloTreeViaPropsComparison = ( if (zoomChange) { const rootNode = phylotree.nodes[newTreeRedux.idxOfInViewRootNode]; args.zoomIntoClade = rootNode; - newState.selectedNode = {}; if (newProps.layout === "unrooted") { args.updateLayout = true; } From 8c6a3010d7864e871ee289f19df1fa7f0930288a Mon Sep 17 00:00:00 2001 From: Victor Lin <13424970+victorlin@users.noreply.github.com> Date: Wed, 6 Nov 2024 16:02:49 -0800 Subject: [PATCH 26/31] Remove unused default value for performanceFlags This value was causing a type error because {} is not a Map. Remove it entirely since it isn't actually used - the value is created from scratch in src/middleware/performanceFlags.js. --- src/components/tree/phyloTree/change.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/tree/phyloTree/change.ts b/src/components/tree/phyloTree/change.ts index f22f2edd7..e07d5c743 100644 --- a/src/components/tree/phyloTree/change.ts +++ b/src/components/tree/phyloTree/change.ts @@ -311,7 +311,7 @@ export const change = function change( branchThickness = undefined, focus = undefined, scatterVariables = undefined, - performanceFlags = {}, + performanceFlags = undefined, }: ChangeParams ): void { // console.log("\n** phylotree.change() (time since last run:", Date.now() - this.timeLastRenderRequested, "ms) **\n\n"); From 3bd4b555a274296f2ed14125bb0da54fcc23d820 Mon Sep 17 00:00:00 2001 From: Victor Lin <13424970+victorlin@users.noreply.github.com> Date: Tue, 29 Oct 2024 16:30:09 -0700 Subject: [PATCH 27/31] Remove broken/unused searchNodes parameter There is a type error that prevents this from working properly: includes() takes a single string, but an array of PhyloNodes is given. It's not used on any existing calls to updateTipRadii, so just remove it entirely. --- src/actions/tree.ts | 7 ++----- src/util/tipRadiusHelpers.ts | 6 ------ 2 files changed, 2 insertions(+), 11 deletions(-) diff --git a/src/actions/tree.ts b/src/actions/tree.ts index 84e3f60a0..34967f0e4 100644 --- a/src/actions/tree.ts +++ b/src/actions/tree.ts @@ -234,7 +234,6 @@ export const updateTipRadii = ( tipSelectedIdx = false, selectedLegendItem = false, geoFilter = [], - searchNodes = false }: { /** the strain to highlight (always tree 1) */ tipSelectedIdx?: number | false, @@ -244,8 +243,6 @@ export const updateTipRadii = ( /** a filter to apply to the strains. Empty array or array of len 2. [0]: geoResolution, [1]: value to filter to */ geoFilter?: [string, string] | [], - - searchNodes?: PhyloNode[] | false } = {} ): ThunkFunction => { return (dispatch, getState) => { @@ -262,8 +259,8 @@ export const updateTipRadii = ( d.dataToo = calcTipRadii({tipSelectedIdx: idx, colorScale, tree: treeToo}); } } else { - d.data = calcTipRadii({selectedLegendItem, geoFilter, searchNodes, colorScale, tree}); - if (tt) d.dataToo = calcTipRadii({selectedLegendItem, geoFilter, searchNodes, colorScale, tree: treeToo}); + d.data = calcTipRadii({selectedLegendItem, geoFilter, colorScale, tree}); + if (tt) d.dataToo = calcTipRadii({selectedLegendItem, geoFilter, colorScale, tree: treeToo}); } dispatch(d); }; diff --git a/src/util/tipRadiusHelpers.ts b/src/util/tipRadiusHelpers.ts index 564397b24..b7a31837e 100644 --- a/src/util/tipRadiusHelpers.ts +++ b/src/util/tipRadiusHelpers.ts @@ -1,7 +1,6 @@ import { tipRadius, tipRadiusOnLegendMatch } from "./globals"; import { getTipColorAttribute, numDate } from "./colorHelpers"; import { getTraitFromNode } from "./treeMiscHelpers"; -import { PhyloNode } from "../components/tree/phyloTree/types"; import { ColorScale } from "../reducers/controls"; import { ReduxNode, TreeState } from "../reducers/tree/types"; @@ -60,7 +59,6 @@ export const calcTipRadii = ({ tipSelectedIdx = false, selectedLegendItem = false, geoFilter = [], - searchNodes = false, colorScale, tree }: { @@ -72,8 +70,6 @@ export const calcTipRadii = ({ geoFilter?: [string, string] | [] - searchNodes?: PhyloNode[] | false - colorScale: ColorScale tree: TreeState @@ -82,8 +78,6 @@ export const calcTipRadii = ({ return tree.nodes.map((d) => determineLegendMatch(selectedLegendItem, d, colorScale) ? tipRadiusOnLegendMatch : tipRadius); } else if (geoFilter.length===2 && tree && tree.nodes) { return tree.nodes.map((d) => determineLocationMatch(d, geoFilter[0], geoFilter[1]) ? tipRadiusOnLegendMatch : tipRadius); - } else if (searchNodes) { - return tree.nodes.map((d) => d.name.toLowerCase().includes(searchNodes) ? tipRadiusOnLegendMatch : tipRadius); } else if (tipSelectedIdx) { const radii = tree.nodes.map(() => tipRadius); radii[tipSelectedIdx] = tipRadiusOnLegendMatch + 3; From 56b791b45f838f6982b596bf83d2d5cfa2befc0e Mon Sep 17 00:00:00 2001 From: Victor Lin <13424970+victorlin@users.noreply.github.com> Date: Fri, 25 Oct 2024 15:57:45 -0700 Subject: [PATCH 28/31] Convert RGBColor to hex string This previously worked because d3 was able to properly parse a color string out of RGBColor. However, it's better to keep things consistent and simple. Convert so that range is simply an array of hex color strings. --- src/util/colorScale.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/util/colorScale.ts b/src/util/colorScale.ts index a8966fedd..f85f80471 100644 --- a/src/util/colorScale.ts +++ b/src/util/colorScale.ts @@ -192,13 +192,13 @@ function createScaleForGenotype( const range = [unknownColor, ...genotypeColors.slice(0, trueValues.length)]; // Bases are returned by orderOfGenotypeAppearance in order, unknowns at end if (legendValues.indexOf("-") !== -1) { - range.push(rgb(217, 217, 217)); + range.push(rgb(217, 217, 217).formatHex()); } if (legendValues.indexOf("N") !== -1 && !aaGenotype) { - range.push(rgb(153, 153, 153)); + range.push(rgb(153, 153, 153).formatHex()); } if (legendValues.indexOf("X") !== -1) { - range.push(rgb(102, 102, 102)); + range.push(rgb(102, 102, 102).formatHex()); } return { colorScale: scaleOrdinal().domain(domain).range(range), From 5a681ffcaf02689668549abf5929b6f1490ffe82 Mon Sep 17 00:00:00 2001 From: Victor Lin <13424970+victorlin@users.noreply.github.com> Date: Tue, 29 Oct 2024 16:09:16 -0700 Subject: [PATCH 29/31] Set default colorings in getDefaultControlsState Centralize the defaults for consistency. Also, this avoids the need to mark the property as optional on the state. --- src/actions/recomputeReduxState.js | 2 -- src/reducers/controls.ts | 9 +++++++-- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/src/actions/recomputeReduxState.js b/src/actions/recomputeReduxState.js index 7ab386ea9..0345f4293 100644 --- a/src/actions/recomputeReduxState.js +++ b/src/actions/recomputeReduxState.js @@ -400,8 +400,6 @@ const modifyControlsStateViaTree = (state, tree, treeToo, colorings) => { * only when a file is dropped. (I've gone down too many rabbit holes in this PR to * do this now, unfortunately.) james, 2023 */ - state.coloringsPresentOnTree = new Set(); - state.coloringsPresentOnTreeWithConfidence = new Set(); // subset of above let coloringsToCheck = []; if (colorings) { diff --git a/src/reducers/controls.ts b/src/reducers/controls.ts index 39c3de277..c04271d0c 100644 --- a/src/reducers/controls.ts +++ b/src/reducers/controls.ts @@ -108,8 +108,11 @@ export interface BasicControlsState { canTogglePanelLayout: boolean colorBy: string colorByConfidence: boolean - coloringsPresentOnTree?: Set - coloringsPresentOnTreeWithConfidence?: Set + coloringsPresentOnTree: Set + + /** subset of coloringsPresentOnTree */ + coloringsPresentOnTreeWithConfidence: Set + colorScale?: ColorScale dateMax: string dateMaxNumeric: number @@ -207,6 +210,8 @@ export const getDefaultControlsState = (): ControlsState => { colorBy: defaults.colorBy, colorByConfidence: false, colorScale: undefined, + coloringsPresentOnTree: new Set(), + coloringsPresentOnTreeWithConfidence: new Set(), explodeAttr: undefined, selectedBranchLabel: "none", showAllBranchLabels: false, From 931f1e6552c2b19f0c6db516b340342bbd24f00b Mon Sep 17 00:00:00 2001 From: Victor Lin <13424970+victorlin@users.noreply.github.com> Date: Wed, 6 Nov 2024 16:27:12 -0800 Subject: [PATCH 30/31] Set default value of focus to false This is more consistent with other booleans. --- src/components/tree/phyloTree/change.ts | 2 +- src/components/tree/phyloTree/types.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/tree/phyloTree/change.ts b/src/components/tree/phyloTree/change.ts index e07d5c743..0c586f7df 100644 --- a/src/components/tree/phyloTree/change.ts +++ b/src/components/tree/phyloTree/change.ts @@ -297,6 +297,7 @@ export const change = function change( svgHasChangedDimensions = false, animationInProgress = false, changeNodeOrder = false, + focus = false, newDistance = undefined, newLayout = undefined, updateLayout = undefined, @@ -309,7 +310,6 @@ export const change = function change( visibility = undefined, tipRadii = undefined, branchThickness = undefined, - focus = undefined, scatterVariables = undefined, performanceFlags = undefined, }: ChangeParams diff --git a/src/components/tree/phyloTree/types.ts b/src/components/tree/phyloTree/types.ts index da7bb5684..3ed139188 100644 --- a/src/components/tree/phyloTree/types.ts +++ b/src/components/tree/phyloTree/types.ts @@ -196,6 +196,7 @@ export interface ChangeParams { svgHasChangedDimensions?: boolean animationInProgress?: boolean changeNodeOrder?: boolean + focus?: boolean // change these things to provided value (unless undefined) // newDistance?: Distance @@ -214,7 +215,6 @@ export interface ChangeParams { branchThickness?: number[] // other data // - focus?: boolean scatterVariables?: ScatterVariables performanceFlags?: PerformanceFlags } From 9c7644b4c8189c41e8bd9c2550d42db435b53896 Mon Sep 17 00:00:00 2001 From: Victor Lin <13424970+victorlin@users.noreply.github.com> Date: Tue, 5 Nov 2024 16:58:58 -0800 Subject: [PATCH 31/31] Allow setState to take a partial state MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit React will shallow merge the provided object into the existing state¹ which works for our usage. The type definition of setState does not allow partial state by default. Use 'never' to configure it to expect no fields in particular². ¹ ² --- src/components/tree/tree.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/tree/tree.tsx b/src/components/tree/tree.tsx index 8c20e696f..e4398230c 100644 --- a/src/components/tree/tree.tsx +++ b/src/components/tree/tree.tsx @@ -83,7 +83,7 @@ export class TreeComponent extends React.Component(newState); /* this will trigger an unnecessary CDU :( */ } } @@ -121,7 +121,7 @@ export class TreeComponent extends React.Component(newState); } override componentWillUnmount() {