Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improve date handling #1519

Draft
wants to merge 2 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
90 changes: 40 additions & 50 deletions src/actions/recomputeReduxState.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import queryString from "query-string";
import { cloneDeep } from 'lodash';
import { numericToCalendar, calendarToNumeric } from "../util/dateHelpers";
import { reallySmallNumber, twoColumnBreakpoint, defaultColorBy, defaultGeoResolution, defaultDateRange, nucleotide_gene, strainSymbol, genotypeSymbol } from "../util/globals";
import { reallySmallNumber, twoColumnBreakpoint, defaultColorBy, defaultGeoResolution, nucleotide_gene, strainSymbol, genotypeSymbol } from "../util/globals";
import { calcBrowserDimensionsInitialState } from "../reducers/browserDimensions";
import { getIdxMatchingLabel, calculateVisiblityAndBranchThickness } from "../util/treeVisibilityHelpers";
import { constructVisibleTipLookupBetweenTrees } from "../util/treeTangleHelpers";
Expand All @@ -24,25 +24,23 @@ import { hasMultipleGridPanels } from "./panelDisplay";
export const doesColorByHaveConfidence = (controlsState, colorBy) =>
controlsState.coloringsPresentOnTreeWithConfidence.has(colorBy);

export const getMinCalDateViaTree = (nodes, state) => {
/* slider should be earlier than actual day */
/* if no date, use some default dates - slider will not be visible */
const minNumDate = getTraitFromNode(nodes[0], "num_date");
return (minNumDate === undefined) ?
numericToCalendar(state.dateMaxNumeric - defaultDateRange) :
numericToCalendar(minNumDate - 0.01);
};

export const getMaxCalDateViaTree = (nodes) => {
let maxNumDate = reallySmallNumber;
nodes.forEach((node) => {
/**
* returns numeric [min, max] values
*/
const getObservedMinMaxDates = (nodes, nodes2) => {
let [minNumDate, maxNumDate] = [10000, reallySmallNumber]; // revisit in 8000 years
let counter = 0;
nodes.concat(nodes2 || []).forEach((node) => {
const numDate = getTraitFromNode(node, "num_date");
if (numDate !== undefined && numDate > maxNumDate) {
maxNumDate = numDate;
}
if (numDate===undefined) return;
counter++;
if (numDate > maxNumDate) maxNumDate = numDate;
if (numDate < minNumDate) minNumDate = numDate;
});
maxNumDate += 0.01; /* slider should be later than actual day */
return numericToCalendar(maxNumDate);
/* expand rang them slightly so that the slider will capture the true range */
return counter ?
[minNumDate-0.01, maxNumDate + 0.01] :
[undefined, undefined];
};

/* need a (better) way to keep the queryParams all in "sync" */
Expand Down Expand Up @@ -318,31 +316,16 @@ const modifyStateViaMetadata = (state, metadata) => {
};

const modifyControlsStateViaTree = (state, tree, treeToo, colorings) => {
state["dateMin"] = getMinCalDateViaTree(tree.nodes, state);
state["dateMax"] = getMaxCalDateViaTree(tree.nodes);
state.dateMinNumeric = calendarToNumeric(state.dateMin);
state.dateMaxNumeric = calendarToNumeric(state.dateMax);

if (treeToo) {
const min = getMinCalDateViaTree(treeToo.nodes, state);
const max = getMaxCalDateViaTree(treeToo.nodes);
const minNumeric = calendarToNumeric(min);
const maxNumeric = calendarToNumeric(max);
if (minNumeric < state.dateMinNumeric) {
state.dateMinNumeric = minNumeric;
state.dateMin = min;
}
if (maxNumeric > state.dateMaxNumeric) {
state.dateMaxNumeric = maxNumeric;
state.dateMax = max;
}
}

/* set absolutes */
state["absoluteDateMin"] = state["dateMin"];
state["absoluteDateMax"] = state["dateMax"];
state.absoluteDateMinNumeric = calendarToNumeric(state.absoluteDateMin);
state.absoluteDateMaxNumeric = calendarToNumeric(state.absoluteDateMax);
const [dateMinNumeric, dateMaxNumeric] = getObservedMinMaxDates(tree.nodes, treeToo?.nodes);
state.dateMinNumeric = dateMinNumeric;
state.dateMaxNumeric = dateMaxNumeric;
state.dateMin = numericToCalendar(dateMinNumeric);
state.dateMax = numericToCalendar(dateMaxNumeric);
/* set absolutes -- i.e. the bounds of the date slider */
state.absoluteDateMinNumeric = dateMinNumeric;
state.absoluteDateMaxNumeric = dateMaxNumeric;
state.absoluteDateMin = state.dateMin;
state.absoluteDateMax = state.dateMax;

/* For the colorings (defined in the JSON) we need to check whether they
(a) actually exist on the tree and (b) have confidence values.
Expand All @@ -356,6 +339,7 @@ const modifyControlsStateViaTree = (state, tree, treeToo, colorings) => {
coloringsToCheck = Object.keys(colorings);
}
let [aaMuts, nucMuts] = [false, false];
let nodesWithDates = 0;
const examineNodes = function examineNodes(nodes) {
nodes.forEach((node) => {
/* check colorBys */
Expand All @@ -375,6 +359,8 @@ const modifyControlsStateViaTree = (state, tree, treeToo, colorings) => {
if (keys.length > 1 || (keys.length === 1 && keys[0]!=="nuc")) aaMuts = true;
if (keys.includes("nuc")) nucMuts = true;
}
/* counter of num_date notes */
if (node?.node_attrs?.num_date?.value!==undefined) nodesWithDates++;
});
};
examineNodes(tree.nodes);
Expand All @@ -394,17 +380,21 @@ const modifyControlsStateViaTree = (state, tree, treeToo, colorings) => {
}

/* does the tree have date information? if not, disable controls, modify view */
const numDateAtRoot = getTraitFromNode(tree.nodes[0], "num_date") !== undefined;
const allNodesHaveDates = nodesWithDates === (tree.nodes.length + (treeToo?.nodes?.length || 0));
const someNodesHaveDates = nodesWithDates>0;
// const numDateAtRoot = getTraitFromNode(tree.nodes[0], "num_date") !== undefined;
const divAtRoot = getDivFromNode(tree.nodes[0]) !== undefined;
state.branchLengthsToDisplay = (numDateAtRoot && divAtRoot) ? "divAndDate" :
numDateAtRoot ? "dateOnly" :
"divOnly";
state.branchLengthsToDisplay = (allNodesHaveDates && divAtRoot) ? "divAndDate" :
allNodesHaveDates ? "dateOnly" :
someNodesHaveDates ? "divAndSomeDate" :
"divOnly";

/* if branchLengthsToDisplay is "divOnly", force to display by divergence
/* if we don't have allNodesHaveDates then we are forced to display by divergence
* if branchLengthsToDisplay is "dateOnly", force to display by date
*/
state.distanceMeasure = state.branchLengthsToDisplay === "divOnly" ? "div" :
state.branchLengthsToDisplay === "dateOnly" ? "num_date" : state.distanceMeasure;
state.distanceMeasure = (!allNodesHaveDates) ? "div" :
state.branchLengthsToDisplay === "dateOnly" ? "num_date" :
state.distanceMeasure;

/* if clade is available as a branch label, then set this as the "default". This
is largely due to historical reasons. Note that it *can* and *will* be overridden
Expand Down
2 changes: 1 addition & 1 deletion src/components/controls/animation-controls.js
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ class AnimationControls extends React.Component {
}

render() {
if (this.props.branchLengthsToDisplay === "divOnly") {
if (["divAndSomeDate", "divOnly"].includes(this.props.branchLengthsToDisplay)) {
return null;
}
return (
Expand Down
2 changes: 1 addition & 1 deletion src/components/controls/animation-options.js
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ class AnimationOptions extends React.Component {

render() {
const { t } = this.props;
if (this.props.branchLengthsToDisplay === "divOnly") return null;
if (["divAndSomeDate", "divOnly"].includes(this.props.branchLengthsToDisplay)) return null;

return (
<div id="mapAnimationControls">
Expand Down
9 changes: 8 additions & 1 deletion src/components/tree/infoPanels/click.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
Expand All @@ -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 <Link title={name} url={url} value={value}/>;
Expand Down
7 changes: 6 additions & 1 deletion src/components/tree/infoPanels/hover.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 <InfoLine name={`${name}:`} value={numericToCalendar(value)}/>;
}

/* helper function to avoid code duplication */
const showCurrentColorByWithoutConfidence = () => {
const value = getTraitFromNode(node, colorBy);
return isValueValid(value) ?
<InfoLine name={`${name}:`} value={value}/> :
null;
Expand Down
2 changes: 1 addition & 1 deletion src/components/tree/legend/legend.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 */
Expand Down
57 changes: 30 additions & 27 deletions src/util/colorScale.js
Original file line number Diff line number Diff line change
Expand Up @@ -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));
Expand Down Expand Up @@ -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 */
Expand All @@ -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();
Expand All @@ -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 */

Expand Down
12 changes: 12 additions & 0 deletions src/util/treeVisibilityHelpers.js
Original file line number Diff line number Diff line change
Expand Up @@ -209,6 +209,18 @@ export const calcVisibility = (tree, controls, dates, inView, filtered) => {
if (inView[idx] && (filtered ? filtered[idx] : true)) {
const nodeDate = getTraitFromNode(node, "num_date");
const parentNodeDate = getTraitFromNode(node.parent, "num_date");

/* if the tree is partially temporal then we use a different approach:
we hide nodes if they define a date + it's outside the range, else we show them
(note that most trees are either temporal (num_date on all nodes) or not -- the mixed case
is rare! */
if (controls.branchLengthsToDisplay === "divAndSomeDate") {
if (nodeDate && (nodeDate <= dates.dateMinNumeric || nodeDate >= dates.dateMaxNumeric)) {
return NODE_NOT_VISIBLE;
}
return NODE_VISIBLE;
}
/* missing date information means we must show the node */
if (!nodeDate || !parentNodeDate) {
return NODE_VISIBLE;
}
Expand Down