From 48606784915ed755ef4199da4631a5c7d4d583dd Mon Sep 17 00:00:00 2001 From: James Hadfield Date: Thu, 2 Jun 2022 20:23:55 +1200 Subject: [PATCH] Allow temporal scales This implements a feature I've long wanted & it does it the quick and easy way: by requiring that the value already be numeric! In the future it'd be nice to allow YYYY-MM-DD values, but that's for another PR (and comes with its own complexities - what about -XX dates?) This commit will require a corresponding one in augur to update the schema --- src/components/tree/infoPanels/click.js | 9 +++- src/components/tree/infoPanels/hover.js | 7 ++- src/components/tree/legend/legend.js | 2 +- src/util/colorScale.js | 57 +++++++++++++------------ 4 files changed, 45 insertions(+), 30 deletions(-) diff --git a/src/components/tree/infoPanels/click.js b/src/components/tree/infoPanels/click.js index 78f9fe052..d1d584c10 100644 --- a/src/components/tree/infoPanels/click.js +++ b/src/components/tree/infoPanels/click.js @@ -197,8 +197,9 @@ const getTraitsToDisplay = (node) => { const Trait = ({node, trait, colorings, isTerminal}) => { let value = getTraitFromNode(node, trait); const confidence = getTraitFromNode(node, trait, {confidence: true}); + const isTemporal = colorings[trait]?.type==="temporal"; - if (typeof value === "number") { + if (typeof value === "number" && !isTemporal) { if (!Number.isInteger(value)) { value = Number.parseFloat(value).toPrecision(3); } @@ -215,6 +216,12 @@ const Trait = ({node, trait, colorings, isTerminal}) => { const name = (colorings && colorings[trait] && colorings[trait].title) ? colorings[trait].title : trait; + + /* case where the colorScale is temporal */ + if (isTemporal && typeof value === "number") { + return item(name, numericToCalendar(value)); + } + const url = getUrlFromNode(node, trait); if (url) { return ; diff --git a/src/components/tree/infoPanels/hover.js b/src/components/tree/infoPanels/hover.js index 47ce1723f..e83796bf0 100644 --- a/src/components/tree/infoPanels/hover.js +++ b/src/components/tree/infoPanels/hover.js @@ -107,10 +107,15 @@ const ColorBy = ({node, colorBy, colorByConfidence, colorScale, colorings}) => { const name = (colorings && colorings[colorBy] && colorings[colorBy].title) ? colorings[colorBy].title : colorBy; + const value = getTraitFromNode(node, colorBy); + + /* case where the colorScale is temporal */ + if (colorScale.scaleType==="temporal" && typeof value === "number") { + return ; + } /* helper function to avoid code duplication */ const showCurrentColorByWithoutConfidence = () => { - const value = getTraitFromNode(node, colorBy); return isValueValid(value) ? : null; diff --git a/src/components/tree/legend/legend.js b/src/components/tree/legend/legend.js index fd7bed525..856280c45 100644 --- a/src/components/tree/legend/legend.js +++ b/src/components/tree/legend/legend.js @@ -138,7 +138,7 @@ class Legend extends React.Component { return this.props.colorScale.legendLabels.get(label); } /* depending on the colorBy, we display different labels! */ - if (this.props.colorBy === "num_date") { + if (this.props.colorBy === "num_date" || this.props.colorScale.scaleType==="temporal") { const legendValues = this.props.colorScale.visibleLegendValues; if ( (legendValues[legendValues.length-1] - legendValues[0] > 10) && /* range spans more than 10 years */ diff --git a/src/util/colorScale.js b/src/util/colorScale.js index faff8e3e9..59541c96f 100644 --- a/src/util/colorScale.js +++ b/src/util/colorScale.js @@ -45,9 +45,9 @@ export const calcColorScale = (colorBy, controls, tree, treeToo, metadata) => { ({legendValues, colorScale} = createScaleForGenotype(tree.nodes, controls.mutType)); domain = [...legendValues]; } else if (colorings && colorings[colorBy]) { - if (scaleType === "continuous") { + if (scaleType === "continuous" || scaleType==="temporal") { ({continuous, colorScale, legendBounds, legendValues} = - createContinuousScale(colorBy, colorings[colorBy].scale, tree.nodes, treeTooNodes)); + createContinuousScale(colorBy, colorings[colorBy].scale, tree.nodes, treeTooNodes, scaleType==="temporal")); } else if (colorings[colorBy].scale) { /* scale set via JSON */ ({continuous, legendValues, colorScale} = createNonContinuousScaleFromProvidedScaleMap(colorBy, colorings[colorBy].scale, tree.nodes, treeTooNodes)); @@ -204,17 +204,22 @@ function createOrdinalScale(colorBy, t1nodes, t2nodes) { return {continuous, colorScale, legendValues, legendBounds}; } -function createContinuousScale(colorBy, providedScale, t1nodes, t2nodes) { +function createContinuousScale(colorBy, providedScale, t1nodes, t2nodes, isTemporal) { + /* Note that a temporal scale is treated very similar to a continuous one... for the time being. + In the future it'd be nice to allow YYYY-MM-DD values, but that's for another PR (and comes + with its own complexities - what about -XX dates?) james june 2022 */ // console.log("making a continuous color scale for ", colorBy); + if (colorBy==="num_date") { + /* before numeric scales were a definable type, num_date was specified as continuous */ + isTemporal = true; // eslint-disable-line no-param-reassign + } let minMax; - switch (colorBy) { - case "lbi": - minMax = [0, 0.7]; - break; - case "num_date": - break; /* minMax not needed for num_date */ - default: - minMax = getMinMaxFromTree(t1nodes, t2nodes, colorBy); + if (isTemporal) { + // empty - minMax not needed + } else if (colorBy==="lbi") { + minMax = [0, 0.7]; /* TODO: this is for historical reasons, and we should switch to a provided scale */ + } else { + minMax = getMinMaxFromTree(t1nodes, t2nodes, colorBy); } /* user-defined anchor points across the scale */ @@ -225,17 +230,17 @@ function createContinuousScale(colorBy, providedScale, t1nodes, t2nodes) { if (anchorPoints) { domain = anchorPoints.map((pt) => pt[0]); range = anchorPoints.map((pt) => pt[1]); - } else if (colorBy==="num_date") { + } else if (isTemporal) { /* we want the colorScale to "focus" on the tip dates, and be spaced according to sampling */ - let rootDate = getTraitFromNode(t1nodes[0], "num_date"); + let rootDate = getTraitFromNode(t1nodes[0], colorBy); let vals = t1nodes.filter((n) => !n.hasChildren) - .map((n) => getTraitFromNode(n, "num_date")); + .map((n) => getTraitFromNode(n, colorBy)); if (t2nodes) { - const treeTooRootDate = getTraitFromNode(t2nodes[0], "num_date"); + const treeTooRootDate = getTraitFromNode(t2nodes[0], colorBy); if (treeTooRootDate < rootDate) rootDate = treeTooRootDate; vals.concat( t2nodes.filter((n) => !n.hasChildren) - .map((n) => getTraitFromNode(n, "num_date")) + .map((n) => getTraitFromNode(n, colorBy)) ); } vals = vals.sort(); @@ -253,17 +258,15 @@ function createContinuousScale(colorBy, providedScale, t1nodes, t2nodes) { const scale = scaleLinear().domain(domain).range(range); let legendValues; - switch (colorBy) { - case "lbi": - legendValues = [0, 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7]; - break; - case "num_date": - legendValues = domain.slice(1); - break; - default: - const spread = minMax[1] - minMax[0]; - const dp = spread > 5 ? 2 : 3; - legendValues = genericDomain.map((d) => parseFloat((minMax[0] + d*spread).toFixed(dp))); + if (isTemporal) { + legendValues = domain.slice(1); + } else if (colorBy==="lbi") { + /* TODO: this is for historical reasons, and we should switch to a provided scale */ + legendValues = [0, 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7]; + } else { + const spread = minMax[1] - minMax[0]; + const dp = spread > 5 ? 2 : 3; + legendValues = genericDomain.map((d) => parseFloat((minMax[0] + d*spread).toFixed(dp))); } if (legendValues[0] === -0) legendValues[0] = 0; /* hack to avoid bugs */