From 19bd74058190c7f775ddf14794cca8abde8f2f03 Mon Sep 17 00:00:00 2001 From: Victor Lin <13424970+victorlin@users.noreply.github.com> Date: Tue, 8 Oct 2024 11:52:28 -0700 Subject: [PATCH] =?UTF-8?q?=F0=9F=9A=A7=20Add=20types=20to=20tree=20compon?= =?UTF-8?q?ent?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Using non-null assertions to simplify conversion. These will be addressed properly in subsequent commits. --- package-lock.json | 538 ++++++++++++++++++ package.json | 1 + src/actions/{tree.js => tree.ts} | 35 +- src/components/tree/index.ts | 9 +- .../tree/phyloTree/{change.js => change.ts} | 214 ++++--- .../{defaultParams.js => defaultParams.ts} | 3 +- .../tree/phyloTree/{layouts.js => layouts.ts} | 49 +- .../phyloTree/{phyloTree.js => phyloTree.ts} | 79 ++- .../{regression.js => regression.ts} | 21 +- .../phyloTree/{renderers.js => renderers.ts} | 157 +++-- src/components/tree/phyloTree/types.ts | 155 +++++ .../reactD3Interface/{change.js => change.ts} | 26 +- .../{initialRender.js => initialRender.ts} | 47 +- src/components/tree/{tree.js => tree.tsx} | 156 +++-- src/{globalStyles.js => globalStyles.ts} | 37 +- src/reducers/controls.ts | 68 ++- 16 files changed, 1337 insertions(+), 258 deletions(-) rename src/actions/{tree.js => tree.ts} (93%) rename src/components/tree/phyloTree/{change.js => change.ts} (69%) rename src/components/tree/phyloTree/{defaultParams.js => defaultParams.ts} (92%) rename src/components/tree/phyloTree/{layouts.js => layouts.ts} (93%) rename src/components/tree/phyloTree/{phyloTree.js => phyloTree.ts} (58%) rename src/components/tree/phyloTree/{regression.js => regression.ts} (74%) rename src/components/tree/phyloTree/{renderers.js => renderers.ts} (79%) create mode 100644 src/components/tree/phyloTree/types.ts rename src/components/tree/reactD3Interface/{change.js => change.ts} (88%) rename src/components/tree/reactD3Interface/{initialRender.js => initialRender.ts} (60%) rename src/components/tree/{tree.js => tree.tsx} (66%) rename src/{globalStyles.js => globalStyles.ts} (84%) diff --git a/package-lock.json b/package-lock.json index 0a95d5a2f..efd284d38 100644 --- a/package-lock.json +++ b/package-lock.json @@ -95,6 +95,7 @@ "auspice": "auspice.js" }, "devDependencies": { + "@types/d3": "^7.4.3", "@types/leaflet": "^1.9.3", "@types/node": "^18.15.11", "@types/webpack-env": "^1.18.2", @@ -3074,6 +3075,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", @@ -16768,6 +17053,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 1acf4194d..ea263881a 100644 --- a/package.json +++ b/package.json @@ -122,6 +122,7 @@ "yaml-front-matter": "^4.0.0" }, "devDependencies": { + "@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 93% rename from src/actions/tree.js rename to src/actions/tree.ts index fb891e642..bec8fce90 100644 --- a/src/actions/tree.js +++ b/src/actions/tree.ts @@ -10,6 +10,8 @@ import { createVisibleLegendValues, getLegendOrder } from "../util/colorScale"; import { getTraitFromNode } from "../util/treeMiscHelpers"; import { warningNotification } from "./notifications"; import { calcFullTipCounts, calcTipCounts } from "../util/treeCountingHelpers"; +import { TreeState } from "../components/tree/tree"; +import { PhyloNode } from "../components/tree/phyloTree/types"; /** @@ -21,20 +23,20 @@ import { calcFullTipCounts, calcTipCounts } from "../util/treeCountingHelpers"; * @param {Int} idx - index of displayed root node * @param {ReduxTreeState} tree */ -export const applyInViewNodesToTree = (idx, tree) => { +export const applyInViewNodesToTree = (idx: number | undefined, tree: TreeState) => { const validIdxRoot = idx !== undefined ? idx : tree.idxOfInViewRootNode; - if (tree.nodes[0].shell) { + if (tree.nodes[0]!.shell) { tree.nodes.forEach((d) => { d.shell.inView = false; d.shell.update = true; }); - if (tree.nodes[validIdxRoot].hasChildren) { - applyToChildren(tree.nodes[validIdxRoot].shell, (d) => {d.inView = true;}); - } else if (tree.nodes[validIdxRoot].parent.arrayIdx===0) { + if (tree.nodes[validIdxRoot]!.hasChildren) { + 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; + 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 */ @@ -48,7 +50,7 @@ export const applyInViewNodesToTree = (idx, tree) => { for (const child of node.children) _markChildrenInView(child); } }; - const startingNode = tree.nodes[validIdxRoot].hasChildren ? tree.nodes[validIdxRoot] : tree.nodes[validIdxRoot].parent; + const startingNode = tree.nodes[validIdxRoot]!.hasChildren ? tree.nodes[validIdxRoot] : tree.nodes[validIdxRoot]!.parent; _markChildrenInView(startingNode); } @@ -61,15 +63,20 @@ 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 + * @param root Change the in-view part of the tree. [root idx tree1, root idx tree2]. + * [0, 0]: reset. [undefined, undefined]: do nothing + * @param 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 +}: { + root?: [number | undefined, number | undefined], + cladeSelected?: string, +} = {} ) => { - return (dispatch, getState) => { + return (dispatch: any, getState: any) => { const { tree, treeToo, controls, frequencies } = getState(); if (root[0] === undefined && !cladeSelected && tree.selectedClade) { /* if not resetting tree to root, maintain previous selectedClade if one exists */ diff --git a/src/components/tree/index.ts b/src/components/tree/index.ts index 07218e725..721594bca 100644 --- a/src/components/tree/index.ts +++ b/src/components/tree/index.ts @@ -1,8 +1,9 @@ -import { connect } from "react-redux"; +import { connect, MapStateToProps } from "react-redux"; import UnconnectedTree from "./tree"; import { RootState } from "../../store"; +import { TreeComponentPropsFromState } from "./tree"; -const Tree = connect((state: RootState) => ({ +const mapStateToProps: MapStateToProps = (state: RootState) => ({ tree: state.tree, treeToo: state.treeToo, selectedNode: state.controls.selectedNode, @@ -31,6 +32,8 @@ const Tree = connect((state: RootState) => ({ narrativeMode: state.narrative.display, animationPlayPauseButton: state.controls.animationPlayPauseButton, showOnlyPanels: state.controls.showOnlyPanels -}))(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 69% rename from src/components/tree/phyloTree/change.js rename to src/components/tree/phyloTree/change.ts index 78fd96e1d..e3d20cb18 100644 --- a/src/components/tree/phyloTree/change.js +++ b/src/components/tree/phyloTree/change.ts @@ -6,11 +6,14 @@ import { NODE_VISIBLE } from "../../../util/globals"; import { getBranchVisibility, strokeForBranch } from "./renderers"; import { shouldDisplayTemporalConfidence } from "../../../reducers/controls"; import { makeTipLabelFunc } from "./labels"; +import { PhyloTree } from "./phyloTree"; +import { Distance, Layout, PhyloNode, PropsForPhyloNodes, Visibility } from "./types"; +import { Selection } from "d3"; /* 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) => { // console.log("update nodes with data for these keys:", Object.keys(newNodeProps)); // let tmp = 0; nodes.forEach((d, i) => { @@ -35,34 +38,34 @@ 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 } } @@ -74,33 +77,40 @@ const svgSetters = { * 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"]) + * @param treeElem (e.g. ".tip" or ".branch") + * @param 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: string, properties: string[]) { + return (selection: 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, treeElem, updateCall, transitionTime) => { +const genericSelectAndModify = ( + svg: Selection, + treeElem: string, + updateCall: (selection: Selection) => void, + transitionTime: number, +) => { // console.log("general svg update for", treeElem); - svg.selectAll(treeElem) - .filter((d) => d.update) + svg.selectAll(treeElem) + .filter((d: PhyloNode) => !!d.update) .transition().duration(transitionTime) .call(updateCall); if (!transitionTime) { @@ -116,7 +126,7 @@ 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) { +export const modifySVG = function modifySVG(this: PhyloTree, elemsToUpdate, svgPropsToUpdate, transitionTime: number, extras) { let updateCall; const classesToPotentiallyUpdate = [".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 */ @@ -127,13 +137,13 @@ export const modifySVG = function modifySVG(elemsToUpdate, svgPropsToUpdate, tra ST.forEach((x, STidx) => { if (elemsToUpdate.has(`.branch${x}`)) { if (applyBranchPropsAlso) { - updateCall = (selection) => { + updateCall = (selection: Selection) => { createUpdateCall(".branch", svgPropsToUpdate)(selection); /* the "normal" branch changes to apply */ - selection.attr("d", (d) => d.branch[STidx]); /* change the path (differs between .S and .T) */ + selection.attr("d", (d) => d.branch![STidx]!); /* change the path (differs between .S and .T) */ }; } else { - updateCall = (selection) => { - selection.attr("d", (d) => d.branch[STidx]); + updateCall = (selection: Selection) => { + selection.attr("d", (d) => d.branch![STidx]!); }; } @@ -199,7 +209,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: PhyloTree, + elemsToUpdate, + svgPropsToUpdate, + transitionTimeFadeOut, + transitionTimeMoveTips, + extras +) { elemsToUpdate.delete(".tip"); this.hideGrid(); let inProgress = 0; /* counter of transitions currently in progress */ @@ -243,47 +260,88 @@ export const modifySVGInStages = function modifySVGInStages(elemsToUpdate, svgPr * 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 -}) { +export interface ChangeParams { + changeColorBy?: boolean + changeVisibility?: boolean + changeTipRadii?: boolean + changeBranchThickness?: boolean + showConfidences?: boolean + removeConfidences?: boolean + zoomIntoClade?: false | PhyloNode + svgHasChangedDimensions?: boolean + animationInProgress?: boolean + changeNodeOrder?: boolean + focus?: boolean + + newDistance?: Distance + newLayout?: Layout + updateLayout?: boolean + newBranchLabellingKey?: string + showAllBranchLabels?: boolean + newTipLabelKey?: string + + branchStroke?: string[] + tipStroke?: (string | undefined)[] + fill?: any[] + visibility?: Visibility[] + tipRadii?: any[] + branchThickness?: any[] + + scatterVariables?: any +} + +interface Extras { + removeConfidences: boolean + showConfidences: boolean + newBranchLabellingKey?: string + + timeSliceHasPotentiallyChanged?: boolean + hideTipLabels?: boolean +} + +export const change = function change(this: PhyloTree, params: ChangeParams) { + const { + /* 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, + focus = 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 */ + scatterVariables = undefined + }: ChangeParams = params; + // 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 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. */ /* calculate dt */ const idealTransitionTime = 500; let transitionTime = idealTransitionTime; - if ((Date.now() - this.timeLastRenderRequested) < idealTransitionTime * 2) { + if ((Date.now() - this.timeLastRenderRequested!) < idealTransitionTime * 2) { transitionTime = 0; } @@ -343,7 +401,7 @@ export const change = function change({ /* some things need to update d.inView and/or d.update. This should be centralised */ /* TODO: list all functions which modify these */ if (zoomIntoClade) { /* must happen below updateNodesWithNewData */ - this.nodes.forEach((d) => { + this.nodes.forEach((d: PhyloNode) => { d.inView = false; d.update = true; }); @@ -351,10 +409,10 @@ 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;}); + this.nodes.forEach((d: PhyloNode) => {d.update = true;}); } /* run calculations as needed - these update properties on the phylotreeNodes (similar to updateNodesWithNewData) */ @@ -393,7 +451,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/layouts.js b/src/components/tree/phyloTree/layouts.ts similarity index 93% rename from src/components/tree/phyloTree/layouts.js rename to src/components/tree/phyloTree/layouts.ts index 226ae2352..61767ddd3 100644 --- a/src/components/tree/phyloTree/layouts.js +++ b/src/components/tree/phyloTree/layouts.ts @@ -7,6 +7,8 @@ import { timerStart, timerEnd } from "../../../util/perf"; import { getTraitFromNode, getDivFromNode } from "../../../util/treeMiscHelpers"; import { stemParent, nodeOrdering } from "./helpers"; import { numDate } from "../../../util/colorHelpers"; +import { PhyloTree } from "./phyloTree"; +import { Distance, Layout, Params, PhyloNode, ScatterVariables } from "./types"; /** * assigns the attribute this.layout and calls the function that @@ -14,7 +16,11 @@ import { numDate } from "../../../util/colorHelpers"; * @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: PhyloTree, + layout?: Layout, + scatterVariables?: ScatterVariables, +) { // console.log("set layout"); timerStart("setLayout"); if (typeof layout === "undefined" || layout !== this.layout) { @@ -54,7 +60,7 @@ 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: PhyloTree) { this.nodes.forEach((d) => { d.y = d.displayOrder; // precomputed y-values d.x = d.depth; // depth according to current distance @@ -74,7 +80,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: PhyloTree) { if (!this.scatterVariables) { console.error("Scatterplot called without variables"); return; @@ -84,7 +90,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); @@ -117,7 +123,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) => { @@ -135,17 +141,15 @@ 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 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 +const unrootedPlaceSubtree = (node: PhyloNode, totalLeafWeight: number) => { + 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 + for (let i = 0; i < node.n.children!.length; i++) { + const ch = node.n.children![i]!.shell; ch.w = 2 * Math.PI * leafWeight(ch.n) / totalLeafWeight; ch.tau = eta; eta += ch.w; @@ -166,7 +170,7 @@ const unrootedPlaceSubtree = (node, totalLeafWeight) => { * done recursively via a the function unrootedPlaceSubtree * @return {null} */ -export const unrootedLayout = function unrootedLayout() { +export const unrootedLayout = function unrootedLayout(this: PhyloTree) { /* 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 @@ -216,7 +220,7 @@ export const unrootedLayout = function unrootedLayout() { * arcs and whether that arc is more than pi or not * @return {null} */ -export const radialLayout = function radialLayout() { +export const radialLayout = function radialLayout(this: PhyloTree) { const maxDisplayOrder = Math.max(...this.nodes.map((d) => d.displayOrder).filter((val) => val)); const offset = this.nodes[0].depth; this.nodes.forEach((d) => { @@ -252,7 +256,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: PhyloTree, + distanceAttribute?: Distance, +) { timerStart("setDistance"); this.nodes.forEach((d) => {d.update = true;}); if (distanceAttribute) { @@ -294,7 +301,7 @@ export const setDistance = function setDistance(distanceAttribute) { * 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: PhyloTree) { if (this.layout==="scatter" && !this.scatterVariables.xContinuous) { this.xScale = scalePoint().round(false).align(0.5).padding(0.5); @@ -339,7 +346,7 @@ export const setScales = function setScales() { * coordinates to their places on the screen * @return {null} */ -export const mapToScreen = function mapToScreen() { +export const mapToScreen = function mapToScreen(this: PhyloTree) { timerStart("mapToScreen"); const inViewTerminalNodes = this.nodes.filter((d) => !d.n.hasChildren).filter((d) => d.inView); @@ -550,7 +557,7 @@ function jitter(axis, scale, nodes) { } -function getTipLabelPadding(params, inViewTerminalNodes) { +function getTipLabelPadding(params: Params, inViewTerminalNodes: PhyloNode[]) { let padBy = 0; if (inViewTerminalNodes.length < params.tipLabelBreakL1) { @@ -571,6 +578,6 @@ function getTipLabelPadding(params, inViewTerminalNodes) { return padBy; } -function leafWeight(node) { +function leafWeight(node: PhyloNode) { 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 58% rename from src/components/tree/phyloTree/phyloTree.js rename to src/components/tree/phyloTree/phyloTree.ts index 9c1129c02..7ae479979 100644 --- a/src/components/tree/phyloTree/phyloTree.js +++ b/src/components/tree/phyloTree/phyloTree.ts @@ -8,9 +8,78 @@ import * as grid from "./grid"; import * as confidence from "./confidence"; import * as labels from "./labels"; import * as regression from "./regression"; +import { Callbacks, Layout, Params, PhyloNode, ReduxNode, ScatterVariables } from "./types"; +import { Selection } from "d3"; + +export interface PhyloTree { + grid: boolean + attributes: string[] + params: Params + groups: { + branchGradientDefs?: Selection + branchStem?: Selection + branchTee?: Selection + clipPath?: Selection + regression?: Selection + tips?: Selection + vaccines?: Selection + } + id: string + nodes: PhyloNode[] + zoomNode: PhyloNode + dateRange: [number, number] + strainToNode: Record + change: typeof change + modifySVG: typeof modifySVG + modifySVGInStages: typeof modifySVGInStages + render: typeof renderers.render + clearSVG: typeof renderers.clearSVG + setClipMask: typeof renderers.setClipMask + drawTips: typeof renderers.drawTips + drawBranches: typeof renderers.drawBranches + drawVaccines: typeof renderers.drawVaccines + drawRegression: typeof renderers.drawRegression + removeRegression: typeof renderers.removeRegression + updateColorBy: typeof renderers.updateColorBy + setDistance: typeof layouts.setDistance + setLayout: typeof layouts.setLayout + rectangularLayout: typeof layouts.rectangularLayout + scatterplotLayout: typeof layouts.scatterplotLayout + unrootedLayout: typeof layouts.unrootedLayout + radialLayout: typeof layouts.radialLayout + setScales: typeof layouts.setScales + mapToScreen: typeof layouts.mapToScreen + calculateRegression: typeof regression.calculateRegression + removeConfidence: typeof confidence.removeConfidence + drawConfidence: typeof confidence.drawConfidence + drawSingleCI: typeof confidence.drawSingleCI + drawBranchLabels: typeof labels.drawBranchLabels + removeBranchLabels: typeof labels.removeBranchLabels + updateBranchLabels: typeof labels.updateBranchLabels + updateTipLabels: typeof labels.updateTipLabels + removeTipLabels: typeof labels.removeTipLabels + hideGrid: typeof grid.hideGrid + addGrid: typeof grid.addGrid + showTemporalSlice: typeof grid.showTemporalSlice + hideTemporalSlice: typeof grid.hideTemporalSlice + + confidencesInSVG: boolean + regression?: regression.Regression + layout: Layout + svg: Selection + distance: "num_date" | "div" + vaccines?: PhyloNode[] + timeLastRenderRequested?: number + callbacks: Callbacks + + scatterVariables?: ScatterVariables + margins: any + xScale: any + yScale: any +} /* 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: PhyloTree, reduxNodes: ReduxNode[], id: string, idxOfInViewRootNode: number) { this.grid = false; this.attributes = ['r', 'cx', 'cy', 'id', 'class', 'd']; this.params = createDefaultParams(); @@ -24,17 +93,17 @@ 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]; + this.zoomNode = this.nodes[idxOfInViewRootNode]!; this.strainToNode = {}; this.nodes.forEach((phylonode) => {this.strainToNode[phylonode.n.name] = phylonode;}); /* debounced functions (AFAIK you can't define these as normal prototypes as they need "this") */ diff --git a/src/components/tree/phyloTree/regression.js b/src/components/tree/phyloTree/regression.ts similarity index 74% rename from src/components/tree/phyloTree/regression.js rename to src/components/tree/phyloTree/regression.ts index effcc84d1..1345209c6 100644 --- a/src/components/tree/phyloTree/regression.js +++ b/src/components/tree/phyloTree/regression.ts @@ -1,20 +1,27 @@ import { sum } from "d3-array"; import { formatDivergence, guessAreMutationsPerSite} from "./helpers"; import { NODE_VISIBLE } from "../../../util/globals"; +import { PhyloTree } from "./phyloTree"; +import { PhyloNode } from "./types"; +export interface Regression { + slope?: number + intercept?: number + r2?: number +} /** * this function calculates a regression between * 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) { return {slope: undefined, intercept: undefined, r2: undefined}; } - const offset = nodes[0].x; + const offset = nodes[0]!.x; const XY = sum( terminalNodes.map((d) => (d.y) * (d.x - offset)) ) / nTips; @@ -31,7 +38,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 +57,7 @@ function calculateRegressionWithFreeIntercept(nodes) { } /** sets this.regression */ -export function calculateRegression() { +export function calculateRegression(this: PhyloTree) { if (this.layout==="clock") { this.regression = calculateRegressionThroughRoot(this.nodes); } else { @@ -58,12 +65,12 @@ export function calculateRegression() { } } -export function makeRegressionText(regression, layout, yScale) { +export function makeRegressionText(regression: Regression, layout: string, yScale: any): string { if (layout==="clock") { if (guessAreMutationsPerSite(yScale)) { - return `rate estimate: ${regression.slope.toExponential(2)} subs per site per year`; + return `rate estimate: ${regression.slope!.toExponential(2)} subs per site per year`; } return `rate estimate: ${formatDivergence(regression.slope)} subs per year`; } - return `intercept = ${regression.intercept.toPrecision(3)}, slope = ${regression.slope.toPrecision(3)}, R^2 = ${regression.r2.toPrecision(3)}`; + return `intercept = ${regression.intercept!.toPrecision(3)}, slope = ${regression.slope!.toPrecision(3)}, R^2 = ${regression.r2!.toPrecision(3)}`; } diff --git a/src/components/tree/phyloTree/renderers.js b/src/components/tree/phyloTree/renderers.ts similarity index 79% rename from src/components/tree/phyloTree/renderers.js rename to src/components/tree/phyloTree/renderers.ts index b2cd90298..a06b0f612 100644 --- a/src/components/tree/phyloTree/renderers.js +++ b/src/components/tree/phyloTree/renderers.ts @@ -3,35 +3,89 @@ 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 { PhyloTree } from "./phyloTree"; +import { Callbacks, Distance, Layout, Params, PhyloNode, ReduxNode, ScatterVariables, Visibility } from "./types"; +import { Selection } from "d3"; + +export const render = function render( + this: PhyloTree, +{ + 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[] + + /** 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; /* set nodes stroke / fill */ - this.nodes.forEach((d, i) => { + this.nodes.forEach((d: PhyloNode, i: number) => { d.branchStroke = branchStroke[i]; d.tipStroke = tipStroke[i]; d.fill = tipFill[i]; @@ -69,19 +123,19 @@ 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: PhyloTree) { if (!this.vaccines || !this.vaccines.length) return; if (!("vaccines" in this.groups)) { this.groups.vaccines = this.svg.append("g").attr("id", "vaccines"); } - this.groups.vaccines + this.groups.vaccines! .selectAll(".vaccineCross") .data(this.vaccines) .enter() .append("path") .attr("class", "vaccineCross") - .attr("d", (d) => d.vaccineCross) + .attr("d", (d) => d.vaccineCross!) .style("stroke", "#333") .style("stroke-width", 2 * this.params.branchStrokeWidth) .style("fill", "none") @@ -97,13 +151,13 @@ 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: PhyloTree) { timerStart("drawTips"); const params = this.params; if (!("tips" in this.groups)) { this.groups.tips = this.svg.append("g").attr("id", "tips").attr("clip-path", "url(#treeClip)"); } - this.groups.tips + this.groups.tips! .selectAll(".tip") .data(this.nodes.filter((d) => !d.n.hasChildren)) .enter() @@ -130,9 +184,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) => { const hiddenSetting = d.n.node_attrs && d.n.node_attrs.hidden; if (hiddenSetting && ( @@ -151,7 +204,7 @@ export const getBranchVisibility = (d) => { * @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, _b?: 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 @@ -168,7 +221,7 @@ 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: PhyloTree) { timerStart("drawBranches"); const params = this.params; @@ -178,9 +231,9 @@ export const drawBranches = function drawBranches() { this.groups.branchTee = this.svg.append("g").attr("id", "branchTee").attr("clip-path", "url(#treeClip)"); } if (this.layout === "clock" || this.layout === "scatter" || this.layout === "unrooted") { - this.groups.branchTee.selectAll("*").remove(); + this.groups.branchTee!.selectAll("*").remove(); } else { - this.groups.branchTee + this.groups.branchTee! .selectAll('.branch') .data(this.nodes.filter((d) => d.n.hasChildren && d.displayOrder !== undefined)) .enter() @@ -212,7 +265,7 @@ export const drawBranches = function drawBranches() { if (!("branchStem" in this.groups)) { this.groups.branchStem = this.svg.append("g").attr("id", "branchStem").attr("clip-path", "url(#treeClip)"); } - this.groups.branchStem + this.groups.branchStem! .selectAll('.branch') .data(this.nodes.filter((d) => d.displayOrder !== undefined)) .enter() @@ -241,14 +294,14 @@ 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: PhyloTree) { /* check we have computed a sensible regression before attempting to draw */ - if (this.regression.slope===undefined) { + if (this.regression!.slope===undefined) { return; } - const leftY = this.yScale(this.regression.intercept + this.xScale.domain()[0] * this.regression.slope); - const rightY = this.yScale(this.regression.intercept + this.xScale.domain()[1] * this.regression.slope); + const leftY = this.yScale(this.regression!.intercept! + this.xScale.domain()[0] * this.regression!.slope); + const rightY = this.yScale(this.regression!.intercept! + this.xScale.domain()[1] * this.regression!.slope); const path = "M " + this.xScale.range()[0].toString() + " " + leftY.toString() + " L " + this.xScale.range()[1].toString() + " " + rightY.toString(); @@ -257,7 +310,7 @@ export const drawRegression = function drawRegression() { this.groups.regression = this.svg.append("g").attr("id", "regression").attr("clip-path", "url(#treeClip)"); } - this.groups.regression + this.groups.regression! .append("path") .attr("d", path) .attr("class", "regression") @@ -268,9 +321,9 @@ export const drawRegression = function drawRegression() { /* Compute & draw regression text. Note that the text hasn't been created until now, as we need to wait until rendering time when the scales have been calculated */ - this.groups.regression + this.groups.regression! .append("text") - .text(makeRegressionText(this.regression, this.layout, this.yScale)) + .text(makeRegressionText(this.regression!, this.layout, this.yScale)) .attr("class", "regression") .attr("x", this.xScale.range()[1] / 2 - 75) .attr("y", this.yScale.range()[0] + 50) @@ -280,16 +333,16 @@ export const drawRegression = function drawRegression() { .style("font-family", this.params.fontFamily); }; -export const removeRegression = function removeRegression() { +export const removeRegression = function removeRegression(this: PhyloTree) { if ("regression" in this.groups) { - this.groups.regression.selectAll("*").remove(); + this.groups.regression!.selectAll("*").remove(); } }; /* * add and remove elements from tree, initial render */ -export const clearSVG = function clearSVG() { +export const clearSVG = function clearSVG(this: PhyloTree) { this.svg.selectAll("*").remove(); }; @@ -338,11 +391,11 @@ 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) + * @param d node + * @param c1 colour of the parent (start of the branch) + * @param c2 colour of the node (end of the branch) */ -const handleBranchHoverColor = (d, c1, c2) => { +const handleBranchHoverColor = (d: PhyloNode, c1: string, c2: string) => { 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 +412,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); + 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 +427,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: PhyloTree) { 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..494cef082 --- /dev/null +++ b/src/components/tree/phyloTree/types.ts @@ -0,0 +1,155 @@ +import { NODE_NOT_VISIBLE, NODE_VISIBLE_TO_MAP_ONLY, NODE_VISIBLE } from "../../../util/globals"; +import { PhyloTree } from "./phyloTree"; + + +export type Layout = "rect" | "radial" | "unrooted" | "clock" | "scatter" + +export type Distance = "num_date" | "div" + +export type Visibility = typeof NODE_NOT_VISIBLE | typeof NODE_VISIBLE_TO_MAP_ONLY | typeof NODE_VISIBLE + +export interface ReduxNode { + arrayIdx?: number + children?: ReduxNode[] + currentGt?: number + hasChildren: boolean + inView?: boolean + name: string + node_attrs?: { + hidden?: "always" | "timetree" | "divtree" + } + parent: ReduxNode + shell: PhyloNode +} + +/** Properties can be any property on PhyloNode but as an array for multiple nodes */ +export interface PropsForPhyloNodes { + branchStroke?: string[] + fill?: string[] + r?: number[] + tipStroke?: (string | undefined)[] + visibility?: Visibility[] +} + +export interface PhyloNode { + angle?: number + arrayIdx?: number + branch?: [string, string] + branchStroke?: string + children?: Node[] + conf?: [number, number] + + /** SVG path */ + confLine?: string + + crossDepth?: number + depth?: number + displayOrder?: number + displayOrderRange?: number[] + fill?: string + fullTipCount?: number + hasChildren?: boolean + inView: boolean + n: ReduxNode + name?: string + parent?: Node + pDepth?: number + px?: number + py?: number + r?: number + rot?: number + shell?: Node + smallBigArc?: boolean + "stroke-width"?: number + tau?: number + that: PhyloTree + tipCount?: number + tipStroke?: string + update?: boolean + + /** SVG path */ + vaccineCross?: string + + visibility?: Visibility + w?: number + x: number + xBase?: number + xCBarEnd?: number + xCBarStart?: number + xCross?: number + xTip?: number + y: number + yBase?: number + yCBarEnd?: number + yCBarStart?: number + yCross?: number + yTip?: number +} + +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 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 Callbacks { + onTipHover: (d: PhyloNode) => void + onTipLeave: (d: PhyloNode) => void + onTipClick: (d: PhyloNode) => void + onBranchHover: (d: PhyloNode) => void + onBranchLeave: (d: PhyloNode) => void + onBranchClick: (d: PhyloNode) => void + tipLabel: (d: PhyloNode) => void +} diff --git a/src/components/tree/reactD3Interface/change.js b/src/components/tree/reactD3Interface/change.ts similarity index 88% rename from src/components/tree/reactD3Interface/change.js rename to src/components/tree/reactD3Interface/change.ts index 67a3b6707..50144235c 100644 --- a/src/components/tree/reactD3Interface/change.js +++ b/src/components/tree/reactD3Interface/change.ts @@ -1,8 +1,19 @@ import { calculateStrokeColors, getBrighterColor } from "../../../util/colorHelpers"; - -export const changePhyloTreeViaPropsComparison = (mainTree, phylotree, oldProps, newProps) => { - const args = {}; - const newState = {}; +import { ChangeParams } from "../phyloTree/change"; +import { PhyloTree } from "../phyloTree/phyloTree"; +import { TreeComponentProps, TreeComponentState } from "../tree"; + +export const changePhyloTreeViaPropsComparison = ( + mainTree: boolean, + phylotree: PhyloTree, + 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,11 +135,14 @@ 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"; // 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 60% rename from src/components/tree/reactD3Interface/initialRender.js rename to src/components/tree/reactD3Interface/initialRender.ts index 9b44fa0b6..69e20c194 100644 --- a/src/components/tree/reactD3Interface/initialRender.js +++ b/src/components/tree/reactD3Interface/initialRender.ts @@ -3,8 +3,15 @@ import 'd3-transition'; import { calculateStrokeColors, getBrighterColor } from "../../../util/colorHelpers"; import * as callbacks from "./callbacks"; import { makeTipLabelFunc } from "../phyloTree/labels"; +import { PhyloTree } from "../phyloTree/phyloTree"; +import { TreeComponent, TreeComponentProps } from "../tree"; -export const renderTree = (that, main, phylotree, props) => { +export const renderTree = ( + that: TreeComponent, + main: boolean, + phylotree: PhyloTree, + props: TreeComponentProps, +) => { const ref = main ? that.domRefs.mainTree : that.domRefs.secondTree; const treeState = main ? props.tree : props.treeToo; if (!treeState.loaded) { @@ -17,13 +24,13 @@ 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( - 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: { /* parameters (modifies PhyloTree's defaults) */ grid: true, confidence: props.temporalConfidence.display, branchLabelKey: renderBranchLabels && props.selectedBranchLabel, @@ -32,7 +39,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 +48,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, + }); }; diff --git a/src/components/tree/tree.js b/src/components/tree/tree.tsx similarity index 66% rename from src/components/tree/tree.js rename to src/components/tree/tree.tsx index adee6cf43..268fc35b5 100644 --- a/src/components/tree/tree.js +++ b/src/components/tree/tree.tsx @@ -1,10 +1,11 @@ import React from "react"; -import { withTranslation } from "react-i18next"; +import { withTranslation, WithTranslation } from "react-i18next"; import { FaSearchMinus } from "react-icons/fa"; import { updateVisibleTipsAndBranchThicknesses } from "../../actions/tree"; import Card from "../framework/card"; import Legend from "./legend/legend"; -import PhyloTree from "./phyloTree/phyloTree"; +import PhyloTreeConstructor, { PhyloTree } from "./phyloTree/phyloTree"; +import { Layout, PhyloNode, ReduxNode, ScatterVariables, Visibility } from "./phyloTree/types"; import { getParentBeyondPolytomy } from "./phyloTree/helpers"; import HoverInfoPanel from "./infoPanels/hover"; import NodeClickedPanel from "./infoPanels/click"; @@ -22,8 +23,92 @@ export const spaceBetweenTrees = 100; export const lhsTreeId = "LEFT"; const rhsTreeId = "RIGHT"; -class Tree extends React.Component { - constructor(props) { + +export interface TreeState { + branchThickness: number[] + branchThicknessVersion: number + idxOfFilteredRoot?: number + idxOfInViewRootNode: number + loaded: boolean + name: string + nodeColors: string[] + nodeColorsVersion: number + nodes: ReduxNode[] + observedMutations: Record + tipRadii: number[] + tipRadiiVersion: number + vaccines: any + visibility: Visibility[] + visibilityVersion: number +} + +interface TreeTooState extends TreeState { + tangleTipLookup: any[][] +} + +export interface TreeComponentProps extends WithTranslation, TreeComponentPropsFromState { + dispatch: (action: any) => void + height: number + width: number +} + +// FIXME: source these types from state +export interface TreeComponentPropsFromState { + animationPlayPauseButton: "Play" | "Pause" + canRenderBranchLabels: boolean + colorBy: any + colorByConfidence: boolean + colorings: any + colorScale: any + dateMaxNumeric: number + dateMinNumeric: number + distanceMeasure: any + explodeAttr: any + filters: Record + focus: boolean + genomeMap: any + layout: Layout + narrativeMode: boolean + panelsToDisplay: string[] + quickdraw: boolean + scatterVariables: ScatterVariables + selectedBranchLabel: string + selectedNode: any + showAllBranchLabels: boolean + showOnlyPanels: boolean + showTangle: boolean + showTreeToo: boolean + temporalConfidence: { + display: boolean + on: boolean + } + tipLabelKey: string + tree: TreeState + treeToo: TreeTooState +} + + +// FIXME: is this Partial? +export interface TreeComponentState { + hoveredNode: PhyloNode | null + tree: PhyloTree | null + treeToo: PhyloTree | null + geneSortFn?: any + selectedNode?: {} +} + +// FIXME: convert to functional component first? +export class TreeComponent extends React.Component { + + // FIXME: check these + domRefs: { + mainTree?: string; + secondTree?: string; + }; + tangleRef?: Tangle; + clearSelectedNode: (node: any) => void; + + constructor(props: TreeComponentProps) { super(props); this.domRefs = { mainTree: undefined, @@ -47,42 +132,45 @@ 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: TreeComponentState) { /* 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); + newState.treeToo = new PhyloTreeConstructor(props.treeToo.nodes, rhsTreeId, props.treeToo.idxOfInViewRootNode); if (attemptUntangle) { untangleTreeToo(newState.tree, newState.treeToo); } - renderTree(this, false, newState.treeToo, props); + renderTree(this, false, newState.treeToo!, props); } - componentDidMount() { + override componentDidMount() { document.addEventListener('keyup', this.handlekeydownEvent); if (this.props.tree.loaded) { - const newState = {}; - newState.tree = new PhyloTree(this.props.tree.nodes, lhsTreeId, this.props.tree.idxOfInViewRootNode); - renderTree(this, true, newState.tree, this.props); + const newState: Partial = {}; + newState.tree = new PhyloTreeConstructor(this.props.tree.nodes, lhsTreeId, this.props.tree.idxOfInViewRootNode); + renderTree(this, true, newState.tree!, this.props); if (this.props.showTreeToo) { - this.setUpAndRenderTreeToo(this.props, newState); /* modifies newState in place */ + this.setUpAndRenderTreeToo(this.props, newState as TreeComponentState); /* modifies newState in place */ } newState.geneSortFn = sortByGeneOrder(this.props.genomeMap); - this.setState(newState); /* this will trigger an unnecessary CDU :( */ + this.setState(newState as TreeComponentState); /* this will trigger an unnecessary CDU :( */ } } - componentDidUpdate(prevProps) { - let newState = {}; + override 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? */ @@ -94,27 +182,33 @@ class Tree extends React.Component { this.state.treeToo.clearSVG(); } newState.tree = this.state.tree; // setUpAndRenderTreeToo needs newState.tree - this.setUpAndRenderTreeToo(this.props, newState); /* modifies newState in place */ + this.setUpAndRenderTreeToo(this.props, newState as TreeComponentState); /* modifies newState in place */ 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 */ if (this.tangleRef && (leftTreeUpdated || rightTreeUpdated)) { this.tangleRef.drawLines(); } - if (Object.keys(newState).length) this.setState(newState); + if (Object.keys(newState).length) this.setState(newState as TreeComponentState); } - componentWillUnmount() { + override componentWillUnmount() { 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 +250,7 @@ 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: [number | undefined, number | undefined] = [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})); } - render() { + override render() { const { t } = this.props; const styles = this.getStyles(); const widthPerTree = this.props.showTreeToo ? (this.props.width - spaceBetweenTrees) / 2 : this.props.width; @@ -272,5 +365,4 @@ class Tree extends React.Component { } } -const WithTranslation = withTranslation()(Tree); -export default WithTranslation; +export default withTranslation()(TreeComponent); 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/reducers/controls.ts b/src/reducers/controls.ts index 5fc96bb77..dacd118df 100644 --- a/src/reducers/controls.ts +++ b/src/reducers/controls.ts @@ -12,8 +12,8 @@ import * as types from "../actions/types"; import { calcBrowserDimensionsInitialState } from "./browserDimensions"; import { doesColorByHaveConfidence } from "../actions/recomputeReduxState"; import { hasMultipleGridPanels } from "../actions/panelDisplay"; +import { Layout, ScatterVariables } from "../components/tree/phyloTree/types"; -type Layout = "rect" | "radial" | "unrooted" | "clock" | "scatter" interface Defaults { distanceMeasure: string @@ -24,23 +24,71 @@ interface Defaults { filtersInFooter: string[] colorBy: string selectedBranchLabel: string - tipLabelKey: typeof strainSymbol + tipLabelKey: string showTransmissionLines: boolean sidebarOpen?: boolean } export interface BasicControlsState { defaults: Defaults + + available?: boolean + canTogglePanelLayout: boolean + temporalConfidence: { + exists: boolean + display: boolean + on: boolean + } layout: Layout + scatterVariables: ScatterVariables + distanceMeasure: string + focus: boolean + dateMin: string + dateMinNumeric: number + dateMax: string + dateMaxNumeric: number + absoluteDateMin: string + absoluteDateMinNumeric: number + absoluteDateMax: string + absoluteDateMaxNumeric: number + colorBy: string + colorByConfidence: boolean + colorScale?: { + visibleLegendValues: any + } + explodeAttr?: any + selectedBranchLabel: string + showAllBranchLabels: boolean + selectedNode: any | null + canRenderBranchLabels: boolean + analysisSlider: boolean + geoResolution: string + filters: Record + filtersInFooter: string[] + modal: 'download' | 'linkOut' | null + quickdraw: boolean + mapAnimationDurationInMilliseconds: number + mapAnimationStartDate: any + mapAnimationCumulative: boolean + mapAnimationShouldLoop: boolean + animationPlayPauseButton: "Play" | "Pause" panelsAvailable: string[] panelsToDisplay: string[] + panelLayout: string + tipLabelKey: string showTreeToo: boolean - canTogglePanelLayout: boolean - focus: boolean + showTangle: boolean + zoomMin?: number + zoomMax?: number + branchLengthsToDisplay: string + sidebarOpen: boolean + treeLegendOpen?: boolean // FIXME: unused? + mapLegendOpen?: boolean // FIXME: unused? + showOnlyPanels: boolean + showTransmissionLines: boolean + normalizeFrequencies: boolean - // This allows arbitrary prop names while TypeScript adoption is incomplete. - // TODO: add all other props explicitly and remove this. - [propName: string]: any; + coloringsPresentOnTree?: Set } export interface MeasurementsControlState { @@ -59,7 +107,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, @@ -84,6 +132,7 @@ export const getDefaultControlsState = () => { const dateMinNumeric = calendarToNumeric(dateMin); const dateMaxNumeric = calendarToNumeric(dateMax); return { + // FIXME: add a default for coloringsPresentOnTree defaults, available: undefined, canTogglePanelLayout: true, @@ -357,7 +406,8 @@ const Controls = (state: ControlsState = getDefaultControlsState(), action): Con return Object.assign({}, state, { legendOpen: action.value }); case types.ADD_EXTRA_METADATA: { for (const colorBy of Object.keys(action.newColorings)) { - state.coloringsPresentOnTree.add(colorBy); + // FIXME: add a default for coloringsPresentOnTree + state.coloringsPresentOnTree!.add(colorBy); } let newState = Object.assign({}, state, { coloringsPresentOnTree: state.coloringsPresentOnTree, filters: state.filters }); if (action.newGeoResolution && !state.panelsAvailable.includes("map")) {