diff --git a/CHANGELOG.md b/CHANGELOG.md index a324770f8..a5628b8b4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,9 +2,38 @@ title: Changelog --- -## version 2.25.1 - 2021/04/07 - +## version 2.28.0 - 2021/07/05 + + +* Remove ability to download metadata TSV from GISAID datasets. Replace with acknowledgments TSV. See [PR 1366](https://github.com/nextstrain/auspice/pull/1366). +* Cleanup header fields when downloading metadata TSV, including switch to use name (eg `pango_lineage`) instead of title (eg `PANGO lineage`). See [PR 1367](https://github.com/nextstrain/auspice/pull/1367). +* Update styling of footer text. See [PR 1364](https://github.com/nextstrain/auspice/pull/1364). + +## version 2.27.0 - 2021/06/05 + +* Adjust greyscale colour ramp. +See [PR 1353](https://github.com/nextstrain/auspice/pull/1353) for more. +* (Bugfix) Fixed the situation where the regression toggle would not appear for clock layouts. +See [PR 1352](https://github.com/nextstrain/auspice/pull/1352) for more. + +## version 2.26.0 - 2021/05/25 +* Scatterplot improvements: + * Non-continuous variables can now be used, which allows all colourings (including Genotype, if that's the current colouring) to be scatterplot variables. + * Jittering is applied when the spacing between axis variables is more than 50 pixels. + * See [PR 1346](https://github.com/nextstrain/auspice/pull/1346) for more. +* Normalized frequency values now tend to zero in the absence of data. +See [PR 1325](https://github.com/nextstrain/auspice/pull/1325) for more. +* Colour scale improvements: + * Continuous colourings can provide a scale, which we interpolate between to get the colour scheme + * Custom legend data can be provided, including display text and, for continuous variables, bounds to map legend entries to values in the data. + * Displayed legend entires may be restricted by specifying them in the dataset JSON. + * See [PR 1340](https://github.com/nextstrain/auspice/pull/1340) for more. +* Filtering via the sidebar UI now returns options which match each of the space-separated queries, rather than requiring an exact match of the query. +See [PR 1344](https://github.com/nextstrain/auspice/pull/1344) for more. +* Legend text now takes the maximum available space. +See [PR 1328](https://github.com/nextstrain/auspice/pull/1328) for more. +## version 2.25.1 - 2021/04/07 * Bugfix for cases where certain interactions with scatterplot variables would cause auspice to crash. See [PR 1332](https://github.com/nextstrain/auspice/pull/1332) for more. @@ -145,7 +174,7 @@ This version reverts the change to URL parsing introduced in 2.18.2 which broke * Improve parsing of auspice URLs with colon characters in the pathname. See [PR 1210](https://github.com/nextstrain/auspice/pull/1210). ## version 2.18.1 - 2020/08/07 -* Add between-paragraph padding for text rendering in (non-mobile) narratives. +* Add between-paragraph padding for text rendering in (non-mobile) narratives. ## version 2.18.0 - 2020/08/03 * Parse narratives client side. @@ -208,7 +237,7 @@ See [PR 1166](https://github.com/nextstrain/auspice/pull/1166). [See PR 1126](https://github.com/nextstrain/auspice/pull/1126) for more. * Add a toggle for whether or not to show transmission lines on the map. [See PR 1147](https://github.com/nextstrain/auspice/pull/1147) and [PR 1103](https://github.com/nextstrain/auspice/pull/1147) for more. -* Dynamically adjust deme circle size on the map when filtering. +* Dynamically adjust deme circle size on the map when filtering. [See PR 1135](https://github.com/nextstrain/auspice/pull/1135) for more. * Allow the genomic diversity data (the data behind the entropy panel) to be downloaded as a TSV. [See PR 1144](https://github.com/nextstrain/auspice/pull/1144) for more. @@ -219,7 +248,7 @@ See [PR 1166](https://github.com/nextstrain/auspice/pull/1166). #### Other * Temporarily disable integration tests from the GitHub CI. [See PR 1148](https://github.com/nextstrain/auspice/pull/1148) for more. * Add a CC-BY license for the downloaded SVG (screenshots) . [See PR 1140](https://github.com/nextstrain/auspice/pull/1140) for more. -* Improvement in code which decides which footers to show. +* Improvement in code which decides which footers to show. [See PR 1118](https://github.com/nextstrain/auspice/pull/1118) for more. * Documentation improvements -- see [PR 1127](https://github.com/nextstrain/auspice/pull/1127) for more. * Fix an error in map positioning in some narrative slides. [See PR 958](https://github.com/nextstrain/auspice/pull/958) for more. @@ -273,7 +302,7 @@ This will allow narratives to render slides with the CI displayed. [See PR 1046](https://github.com/nextstrain/auspice/pull/1046) * Add the ability to export per-strain metadata of only those strains currently being displayed. [See PR 1067](https://github.com/nextstrain/auspice/pull/1067) -* Move to using `react-icons` which allows the removal of the font-awesome CSS. +* Move to using `react-icons` which allows the removal of the font-awesome CSS. This improves ease-of-use and reduces the bundle size. [See PR 1065](https://github.com/nextstrain/auspice/pull/1065), [PR 1041](https://github.com/nextstrain/auspice/pull/1041) & [PR 1073](https://github.com/nextstrain/auspice/pull/1073) @@ -353,8 +382,8 @@ This improves ease-of-use and reduces the bundle size. * Update Spanish locale data (still in a partially complete state). See [commit f9c8ad2](https://github.com/nextstrain/auspice/commit/f9c8ad209a1e5d304fc6f15ec708f3d0be3dec43) * Reorganisation and general improvements to documentation around contributing to auspice development. -[See PR 978](https://github.com/nextstrain/auspice/pull/978), -[commit 707f563](https://github.com/nextstrain/auspice/commit/707f563aab0a62e0504e393af0cd23da3e4504e0) and +[See PR 978](https://github.com/nextstrain/auspice/pull/978), +[commit 707f563](https://github.com/nextstrain/auspice/commit/707f563aab0a62e0504e393af0cd23da3e4504e0) and [commit 9f002c9](https://github.com/nextstrain/auspice/commit/9f002c96a676e4603b7b9c06ef7df8a26be6d04c) * Fix a bug where the narrative table styling introduced in 2.9.0 were applied outside the narratives. * Fix all linting errors and warnings (potentially the first time this has happened!) diff --git a/package.json b/package.json index f6a057467..c56c2c827 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "auspice", - "version": "2.25.1", + "version": "2.28.0", "description": "Web app for visualizing pathogen evolution", "author": "James Hadfield, Trevor Bedford and Richard Neher", "license": "AGPL-3.0-only", diff --git a/src/actions/recomputeReduxState.js b/src/actions/recomputeReduxState.js index 734646a1e..b6e4c3058 100644 --- a/src/actions/recomputeReduxState.js +++ b/src/actions/recomputeReduxState.js @@ -568,7 +568,7 @@ const checkAndCorrectErrorsInState = (state, metadata, query, tree, viewingNarra // todo: these should be JSON definable (via display_defaults) if (state.layout==="scatter" || state.layout==="clock") { state.scatterVariables = validateScatterVariables( - state.scatterVariables, metadata.colorings, state.distanceMeasure, state.colorBy, state.layout==="clock" + state, metadata, tree, state.layout==="clock" ); if (query.scatterX && query.scatterX!==state.scatterVariables.x) delete query.scatterX; if (query.scatterY && query.scatterY!==state.scatterVariables.y) delete query.scatterY; diff --git a/src/components/controls/choose-layout.js b/src/components/controls/choose-layout.js index d914242f8..d2332862f 100644 --- a/src/components/controls/choose-layout.js +++ b/src/components/controls/choose-layout.js @@ -1,6 +1,3 @@ -/* eslint-disable react/jsx-no-bind */ -/* ^^^ We can get away with this because doesn't rerender frequently, but fixes are welcome */ - import React from "react"; import PropTypes from 'prop-types'; import { connect } from "react-redux"; @@ -9,9 +6,9 @@ import { withTranslation } from 'react-i18next'; import Select from "react-select/lib/Select"; import * as icons from "../framework/svg-icons"; import { controlsWidth } from "../../util/globals"; -import { collectAvailableScatterVariables, validateScatterVariables} from "../../util/scatterplotHelpers"; -import { CHANGE_LAYOUT } from "../../actions/types"; +import { collectAvailableScatterVariables} from "../../util/scatterplotHelpers"; import { SidebarSubtitle, SidebarButton } from "./styles"; +import { changeLayout } from "../../actions/layout"; import Toggle from "./toggle"; @@ -29,9 +26,8 @@ export const RowContainer = styled.div` return { layout: state.controls.layout, scatterVariables: state.controls.scatterVariables, - colorBy: state.controls.colorBy, - distanceMeasure: state.controls.distanceMeasure, colorings: state.metadata.colorings, + colorBy: state.controls.colorBy, showTreeToo: state.controls.showTreeToo, branchLengthsToDisplay: state.controls.branchLengthsToDisplay }; @@ -41,19 +37,8 @@ class ChooseLayout extends React.Component { layout: PropTypes.string.isRequired, dispatch: PropTypes.func.isRequired } - constructor(props) { - super(props); - this.updateLayout = (layout, modifiedScatterVariables=undefined) => { - if (window.NEXTSTRAIN && window.NEXTSTRAIN.animationTickReference) return; - const scatterVariables = modifiedScatterVariables ? - {...this.props.scatterVariables, ...modifiedScatterVariables} : - this.props.scatterVariables; - this.props.dispatch({type: CHANGE_LAYOUT, layout, scatterVariables}); - }; - } - renderScatterplotAxesSelector() { - const options = collectAvailableScatterVariables(this.props.colorings); + const options = collectAvailableScatterVariables(this.props.colorings, this.props.colorBy); const selectedX = options.filter((o) => o.value===this.props.scatterVariables.x)[0]; const selectedY = options.filter((o) => o.value===this.props.scatterVariables.y)[0]; const miscSelectProps = {options, clearable: false, searchable: false, multi: false, valueKey: "label"}; @@ -66,7 +51,7 @@ class ChooseLayout extends React.Component { this.updateLayout("scatter", {y: value.value, yLabel: value.label})} + onChange={(value) => this.props.dispatch(changeLayout({y: value.value, yLabel: value.label}))} /> ); } - - renderScatterplotToggles() { + renderBranchToggle() { return ( - <> -
- - this.updateLayout(this.props.layout, {showBranches: !this.props.scatterVariables.showBranches})} - label={"Show branches"} - /> - -
- - this.updateLayout(this.props.layout, {showRegression: !this.props.scatterVariables.showRegression})} - label={"Show regression"} - /> - -
- + + this.props.dispatch(changeLayout({showBranches: !this.props.scatterVariables.showBranches}))} + label={"Show branches"} + /> + + ); + } + renderRegressionToggle() { + if (this.props.layout === "scatter" && !(this.props.scatterVariables.xContinuous && this.props.scatterVariables.yContinuous)) { + return null; // scatterplot regressions only available if _both_ variables are continuous + } + return ( + + this.props.dispatch(changeLayout({showRegression: !this.props.scatterVariables.showRegression}))} + label={"Show regression"} + /> + ); } - render() { const { t } = this.props; if (this.props.showTreeToo) return null; @@ -124,7 +109,7 @@ class ChooseLayout extends React.Component { this.updateLayout("rect")} + onClick={() => this.props.dispatch(changeLayout({layout: "rect"}))} > {t("sidebar:rectangular")} @@ -133,7 +118,7 @@ class ChooseLayout extends React.Component { this.updateLayout("radial")} + onClick={() => this.props.dispatch(changeLayout({layout: "radial"}))} > {t("sidebar:radial")} @@ -142,7 +127,7 @@ class ChooseLayout extends React.Component { this.updateLayout("unrooted")} + onClick={() => this.props.dispatch(changeLayout({layout: "unrooted"}))} > {t("sidebar:unrooted")} @@ -155,14 +140,12 @@ class ChooseLayout extends React.Component { this.updateLayout( - "clock", - validateScatterVariables(this.props.scatterVariables, this.props.colorings, this.props.distanceMeasure, this.props.colorBy, true) - )} + onClick={() => this.props.dispatch(changeLayout({layout: "clock"}))} > {t("sidebar:clock")} - {selected==="clock" && this.renderScatterplotToggles()} + {selected==="clock" && this.renderBranchToggle()} + {selected==="clock" && this.renderRegressionToggle()} ) : null @@ -172,19 +155,13 @@ class ChooseLayout extends React.Component { this.updateLayout( - "scatter", - validateScatterVariables(this.props.scatterVariables, this.props.colorings, this.props.distanceMeasure, this.props.colorBy, false) - )} + onClick={() => this.props.dispatch(changeLayout({layout: "scatter"}))} > {t("sidebar:scatter")} - {selected==="scatter" && ( - <> - {this.renderScatterplotAxesSelector()} - {this.renderScatterplotToggles()} - - )} + {selected==="scatter" && this.renderScatterplotAxesSelector()} + {selected==="scatter" && this.renderBranchToggle()} + {selected==="scatter" && this.renderRegressionToggle()}
); @@ -206,7 +183,7 @@ const ScatterVariableContainer = styled.div` flex-shrink: 1; flex-basis: auto; align-self: auto; - padding: 0px 0px 2px 15px; + padding: ${(props) => props.padAbove?"2":"0"}px 0px 2px 15px; `; const ScatterAxisName = styled.div` diff --git a/src/components/tree/phyloTree/grid.js b/src/components/tree/phyloTree/grid.js index 3952a56dd..3b0a3d0f3 100644 --- a/src/components/tree/phyloTree/grid.js +++ b/src/components/tree/phyloTree/grid.js @@ -245,6 +245,13 @@ export const addGrid = function addGrid() { (this.layout!=="scatter" && this.distance==="num_date") ) { xGridPoints = computeTemporalGridPoints(xmin, xmax, xAxisPixels, "x"); + } else if (this.layout==="scatter" && !this.scatterVariables.xContinuous) { + xGridPoints = { + majorGridPoints: this.xScale.domain().map((name) => ({ + name, visibility: "visible", axis: "x", position: name + })), + minorGridPoints: [] + }; } else { xGridPoints = computeNumericGridPoints(xmin, xmax, layout, this.params.minorTicks, "x"); } @@ -319,6 +326,10 @@ export const addGrid = function addGrid() { const yAxisPixels = this.yScale.range()[1] - this.yScale.range()[0]; const temporalGrid = computeTemporalGridPoints(ymin, ymax, yAxisPixels, "y"); majorGridPoints.push(...temporalGrid.majorGridPoints); + } else if (this.layout==="scatter" && !this.scatterVariables.yContinuous) { + majorGridPoints.push(...this.yScale.domain().map((name) => ({ + name, visibility: "visible", axis: "y", position: name + }))); } else { const numericGrid = computeNumericGridPoints(ymin, ymax, layout, 1, "y"); majorGridPoints.push(...numericGrid.majorGridPoints); diff --git a/src/components/tree/phyloTree/layouts.js b/src/components/tree/phyloTree/layouts.js index 034a0a6bb..081568379 100644 --- a/src/components/tree/phyloTree/layouts.js +++ b/src/components/tree/phyloTree/layouts.js @@ -1,6 +1,8 @@ /* eslint-disable no-multi-spaces */ /* eslint-disable space-infix-ops */ import { min, max } from "d3-array"; +import scaleLinear from "d3-scale/src/linear"; +import {point as scalePoint} from "d3-scale/src/band"; import { addLeafCount} from "./helpers"; import { calculateRegressionThroughRoot, calculateRegressionWithFreeIntercept } from "./regression"; import { timerStart, timerEnd } from "../../../util/perf"; @@ -84,6 +86,9 @@ export const scatterplotLayout = function scatterplotLayout() { if (this.scatterVariables.x==="div") { d.x = getDivFromNode(d.n); d.px = getDivFromNode(d.n.parent); + } else if (this.scatterVariables.x==="gt") { + d.x = d.n.currentGt; + d.px = d.n.parent.currentGt; } else { d.x = getTraitFromNode(d.n, this.scatterVariables.x); d.px = getTraitFromNode(d.n.parent, this.scatterVariables.x); @@ -92,6 +97,9 @@ export const scatterplotLayout = function scatterplotLayout() { if (this.scatterVariables.y==="div") { d.y = getDivFromNode(d.n); d.py = getDivFromNode(d.n.parent); + } else if (this.scatterVariables.y==="gt") { + d.y = d.n.currentGt; + d.py = d.n.parent.currentGt; } else { d.y = getTraitFromNode(d.n, this.scatterVariables.y); d.py = getTraitFromNode(d.n.parent, this.scatterVariables.y); @@ -259,10 +267,23 @@ export const setDistance = function setDistance(distanceAttribute) { /** - * sets the range of the scales used to map the x,y coordinates to the screen + * Initializes and sets the range of the scales (this.xScale, this.yScale) + * which are used to map the x,y coordinates to the screen * @param {margins} -- object with "right, left, top, bottom" margins */ export const setScales = function setScales(margins) { + + if (this.layout==="scatter" && !this.scatterVariables.xContinuous) { + this.xScale = scalePoint().round(false).align(0.5); + } else { + this.xScale = scaleLinear(); + } + if (this.layout==="scatter" && !this.scatterVariables.yContinuous) { + this.yScale = scalePoint().round(false).align(0.5); + } else { + this.yScale = scaleLinear(); + } + const width = parseInt(this.svg.attr("width"), 10); const height = parseInt(this.svg.attr("height"), 10); if (this.layout === "radial" || this.layout === "unrooted") { @@ -276,7 +297,7 @@ export const setScales = function setScales(margins) { this.yScale.range([0.5 * ySlack + margins["top"] || 0, height - 0.5 * ySlack - (margins["bottom"] || 0)]); } else { - // for rectancular layout, allow flipping orientation of left right and top/botton + // for rectangular layout, allow flipping orientation of left/right and top/bottom if (this.params.orientation[0] > 0) { this.xScale.range([margins["left"] || 0, width - (margins["right"] || 0)]); } else { @@ -331,63 +352,78 @@ export const mapToScreen = function mapToScreen() { /* set the range of the x & y scales */ this.setScales(tmpMargins); - /* find minimum & maximum x & y values */ - let [minY, maxY, minX, maxX] = [1000000, -100000, 1000000, -100000]; let nodesInDomain = this.nodes.filter((d) => d.inView && d.y!==undefined && d.x!==undefined); // scatterplots further restrict nodes used for domain calcs - if not rendering branches, // then we don't consider internal nodes for the domain calc if (this.layout==="scatter" && this.scatterVariables.showBranches===false) { nodesInDomain = nodesInDomain.filter((d) => d.terminal); } - nodesInDomain.forEach((d) => { - if (d.x > maxX) maxX = d.x; - if (d.y > maxY) maxY = d.y; - if (d.x < minX) minX = d.x; - if (d.y < minY) minY = d.y; - }); - /* fixes state of 0 length domain */ - if (minX === maxX) { - minX -= 0.005; - maxX += 0.005; - } - - /* slightly pad min and max y to account for small clades */ - if (inViewTerminalNodes.length < 30) { - const delta = 0.05 * (maxY - minY); - minY -= delta; - maxY += delta; + /* Compute the domains to pass to the d3 scales for the x & y axes */ + let xDomain, yDomain, spanX, spanY; + if (this.layout!=="scatter" || this.scatterVariables.xContinuous) { + let [minX, maxX] = [1000000, -100000]; + nodesInDomain.forEach((d) => { + if (d.x < minX) minX = d.x; + if (d.x > maxX) maxX = d.x; + }); + /* fixes state of 0 length domain */ + if (minX === maxX) { + minX -= 0.005; + maxX += 0.005; + } + /* Don't allow tiny x-axis domains -- e.g. if zoomed into a polytomy where the + divergence values are all tiny, then we don't want to display the tree topology */ + const minimumXAxisSpan = 1E-8; + spanX = maxX-minX; + if (spanX < minimumXAxisSpan) { + maxX = minimumXAxisSpan - minX; + spanX = minimumXAxisSpan; + } + xDomain = [minX, maxX]; + } else { + const seenValues = new Set(nodesInDomain.map((d) => d.x)); + xDomain = this.scatterVariables.xDomain.filter((v) => seenValues.has(v)); + padCategoricalScales(xDomain, this.xScale); } - /* Don't allow tiny x-axis domains -- e.g. if zoomed into a polytomy where the - divergence values are all tiny, then we don't want to display the tree topology */ - const minimumXAxisSpan = 1E-8; - let spanX = maxX-minX; - if (spanX < minimumXAxisSpan) { - maxX = minimumXAxisSpan - minX; - spanX = minimumXAxisSpan; + if (this.layout!=="scatter" || this.scatterVariables.yContinuous) { + let [minY, maxY] = [1000000, -100000]; + nodesInDomain.forEach((d) => { + if (d.y < minY) minY = d.y; + if (d.y > maxY) maxY = d.y; + }); + /* slightly pad min and max y to account for small clades */ + if (inViewTerminalNodes.length < 30) { + const delta = 0.05 * (maxY - minY); + minY -= delta; + maxY += delta; + } + spanY = maxY-minY; + yDomain = [minY, maxY]; + } else { + const seenValues = new Set(nodesInDomain.map((d) => d.y)); + yDomain = this.scatterVariables.yDomain.filter((v) => seenValues.has(v)); + padCategoricalScales(yDomain, this.yScale); } - /* set the domain of the x & y scales */ + /* Radial / Unrooted layouts need to be square since branch lengths + depend on this */ if (this.layout === "radial" || this.layout === "unrooted") { - // handle "radial and unrooted differently since they need to be square - // since branch length move in x and y direction - // TODO: should be tied to svg dimensions - const spanY = maxY-minY; const maxSpan = max([spanY, spanX]); const ySlack = (spanX>spanY) ? (spanX-spanY)*0.5 : 0.0; const xSlack = (spanX { if (isNaN(d.xTip)) d.xTip = hiddenXPosition; if (isNaN(d.yTip)) d.yTip=hiddenYPosition; @@ -471,3 +509,32 @@ export const mapToScreen = function mapToScreen() { } timerEnd("mapToScreen"); }; + +function padCategoricalScales(domain, scale) { + if (domain.length<=4) return scale.padding(0.4); + if (domain.length<=6) return scale.padding(0.3); + if (domain.length<=10) return scale.padding(0.2); + return scale.padding(0.1); +} + +/** + * Add jitter to the already-computed node positions. + */ +function jitter(axis, scale, nodes) { + const step = scale.step(); + if (step < 50) return; // don't jitter if there's little space between bands + const rand = []; // pre-compute a small set of pseudo random numbers for speed + for (let i=1e2; i--;) { + rand.push((Math.random()-0.5)*step*0.5); // occupy 50% + } + const [base, tip, randLen] = [`${axis}Base`, `${axis}Tip`, rand.length]; + let j = 0; + function recurse(n) { + n[base] = n.parent[tip]; + n[tip] += rand[j++]; + if (j>=randLen) j=0; + if (!n.children) return; + for (const child of n.children) recurse(child); + } + recurse(nodes[0]); +} diff --git a/src/reducers/controls.js b/src/reducers/controls.js index 39171b7ac..a3cc7525d 100644 --- a/src/reducers/controls.js +++ b/src/reducers/controls.js @@ -123,7 +123,7 @@ const Controls = (state = getDefaultControlsState(), action) => { case types.CHANGE_LAYOUT: return Object.assign({}, state, { layout: action.layout, - canRenderBranchLabels: (action.layout!=="scatter" && action.layout!=="clock") || (action.scatterVariables && action.scatterVariables.showBranches), + canRenderBranchLabels: action.canRenderBranchLabels, scatterVariables: action.scatterVariables, /* temporal confidence can only be displayed for rectangular trees */ temporalConfidence: Object.assign({}, state.temporalConfidence, { @@ -219,6 +219,9 @@ const Controls = (state = getDefaultControlsState(), action) => { colorScale: action.colorScale, colorByConfidence: doesColorByHaveConfidence(state, action.colorBy) }); + if (action.scatterVariables) { + newState.scatterVariables = action.scatterVariables; + } return newState; } case types.CHANGE_GEO_RESOLUTION: diff --git a/src/util/processFrequencies.js b/src/util/processFrequencies.js index 8a06cfc36..9e87c2402 100644 --- a/src/util/processFrequencies.js +++ b/src/util/processFrequencies.js @@ -46,25 +46,37 @@ export const checkIfNormalizableFromRawData = (data, pivots, nodes, visibility) } } }); - const minFrequency = Math.min(...pivotTotals); - const allowNormalization = minFrequency > 0.001; + // const minFrequency = Math.min(...pivotTotals); + const allowNormalization = true; return allowNormalization; }; -export const computeMatrixFromRawData = (data, pivots, nodes, visibility, colorScale, colorBy, normalizeFrequencies) => { +export const computeMatrixFromRawData = ( + data, + pivots, + nodes, + visibility, + colorScale, + colorBy, + normalizeFrequencies +) => { /* color scale domain forms the categories in the stream graph */ const categories = colorScale.legendValues.filter((d) => d !== undefined); categories.push(unassigned_label); /* for tips without a colorBy */ const isGenotype = isColorByGenotype(colorBy); const matrix = {}; /* SHAPE: rows: categories (colorBys), columns: pivots */ const pivotsLen = pivots.length; - categories.forEach((x) => {matrix[x] = new Array(pivotsLen).fill(0);}); + categories.forEach((x) => { + matrix[x] = new Array(pivotsLen).fill(0); + }); // let debugTipsSeen = 0; const debugPivotTotals = new Array(pivotsLen).fill(0); data.forEach((d) => { if (visibility[d.idx] === NODE_VISIBLE) { // debugTipsSeen++; - const category = assignCategory(colorScale, categories, nodes[d.idx], colorBy, isGenotype) || unassigned_label; + const category = + assignCategory(colorScale, categories, nodes[d.idx], colorBy, isGenotype) || + unassigned_label; // if (category === unassigned_label) return; for (let i = 0; i < pivotsLen; i++) { matrix[category][i] += d.values[i]; @@ -77,11 +89,14 @@ export const computeMatrixFromRawData = (data, pivots, nodes, visibility, colorS }); if (normalizeFrequencies) { - const nCategories = Object.keys(matrix).length; - const minVal = 1e-10; + const minVal = 1e-7; Object.keys(matrix).forEach((cat) => { debugPivotTotals.forEach((norm, i) => { - matrix[cat][i] = (matrix[cat][i] + minVal) / (nCategories * minVal + norm); + if (norm > minVal) { + matrix[cat][i] /= norm; + } else { + matrix[cat][i] = 0.0; + } }); }); } @@ -104,19 +119,22 @@ export const processFrequenciesJSON = (rawJSON, tree, controls) => { throw new Error("tree not loaded"); } const data = []; - tree.nodes.filter((d) => !d.hasChildren).forEach((n) => { - if (!rawJSON[n.name]) { - console.warn(`No tip frequency information for ${n.name}`); - return; - } - data.push({ - idx: n.arrayIdx, - values: rawJSON[n.name].frequencies, - weight: rawJSON[n.name].weight + tree.nodes + .filter((d) => !d.hasChildren) + .forEach((n) => { + if (!rawJSON[n.name]) { + console.warn(`No tip frequency information for ${n.name}`); + return; + } + data.push({ + idx: n.arrayIdx, + values: rawJSON[n.name].frequencies, + weight: rawJSON[n.name].weight + }); }); - }); - const normalizeFrequencies = controls.normalizeFrequencies && + const normalizeFrequencies = + controls.normalizeFrequencies && checkIfNormalizableFromRawData(data, pivots, tree.nodes, tree.visibility); const matrix = computeMatrixFromRawData( diff --git a/src/util/scatterplotHelpers.js b/src/util/scatterplotHelpers.js index 84f44b111..1335df4f0 100644 --- a/src/util/scatterplotHelpers.js +++ b/src/util/scatterplotHelpers.js @@ -1,23 +1,35 @@ +import { calcColorScale } from "./colorScale"; +import { makeGenotypeLabel, isColorByGenotype} from "./getGenotype"; - -export function collectAvailableScatterVariables(colorings) { - // todo: genotype (special case) - const options = Object.keys(colorings) - .filter((key) => key!=="gt") - .filter((key) => colorings[key].type==="continuous") // work needed to render non-continuous scales in PhyloTree +export function collectAvailableScatterVariables(colorings, colorBy) { + let options = Object.keys(colorings) + .filter((key) => colorings[key].type!=="boolean") .map((key) => ({ value: key, label: colorings[key].title || key })); + + /* If colorBy is genotype, then we allow it to be an option, else we remove it */ + const genotypeLabel = makeGenotypeLabel(colorBy); + if (genotypeLabel) { + options.forEach((o) => { + if (o.value==="gt") o.label = genotypeLabel; + }); + } else { + options = options.filter((o) => o.value!=="gt"); + } options.unshift({value: "div", label: "Divergence"}); + return options; } /** * Return a (validated) `scatterVariables` object, given any existing scatterVariables (which may themselves be invalid) */ -export function validateScatterVariables(existingScatterVariables={}, colorings, distanceMeasure, colorBy, isClock) { - const availableOptions = collectAvailableScatterVariables(colorings); +export function validateScatterVariables(controls, metadata, tree, isClock) { + const {distanceMeasure, colorBy } = controls; + const existingScatterVariables = {...controls.scatterVariables}; + const availableOptions = collectAvailableScatterVariables(metadata.colorings, controls.colorBy); const scatterVariables = {}; // default is to show branches, unless the existing state says otherwise scatterVariables.showBranches = Object.prototype.hasOwnProperty.call(existingScatterVariables, "showBranches") ? @@ -28,13 +40,17 @@ export function validateScatterVariables(existingScatterVariables={}, colorings, // we only validate the x & y values if we're _not_ in clock mode, as we don't use them there! if (!isClock) { // default X value is existing state, or the distanceMeasure. It should not be the existing y value (if that's set) - const xOption = _getFirstMatchingOption(availableOptions, [existingScatterVariables.x, distanceMeasure], existingScatterVariables.y); + const xOption = getFirstMatchingScatterVariable(availableOptions, [existingScatterVariables.x, distanceMeasure], existingScatterVariables.y); scatterVariables.x = xOption.value; scatterVariables.xLabel = xOption.label; // default Y value is similar, but we default to the colorBy (if available) - const yOption = _getFirstMatchingOption(availableOptions, [existingScatterVariables.y, colorBy], xOption.value); + const yOption = getFirstMatchingScatterVariable(availableOptions, [existingScatterVariables.y, colorBy], xOption.value); scatterVariables.y = yOption.value; scatterVariables.yLabel = yOption.label; + for (const axis of ["x", "y"]) { + addScatterAxisInfo(scatterVariables, axis, controls, tree, metadata); + } + if (!scatterVariables.xContinuous || !scatterVariables.yContinuous) scatterVariables.showRegression= false; } return scatterVariables; } @@ -45,11 +61,12 @@ export function validateScatterVariables(existingScatterVariables={}, colorings, * First scans through a list of values to try (`tryTheseFirst`) * Will not return an option whose key matches `notThisValue` */ -function _getFirstMatchingOption(options, tryTheseFirst, notThisValue) { +export function getFirstMatchingScatterVariable(options, tryTheseFirst, notThisValue) { const availableValues = options.map((opt) => opt.value); for (let i=0; i