Skip to content

Commit

Permalink
Allow date filtering for partially temporal trees
Browse files Browse the repository at this point in the history
Previously a tree was considered temporal (i.e. you could use a temporal
tree metric) or not based on the presence of `num_date` on the root
node. The allowed partially temporal trees (e.g. those where some tip
dates were known) to be displayed as div trees with a colouring of
`num_date`, however the date slider wasn't available.

Here we allow "partially temporal trees" which are defined as tree(s)
where the number of nodes with `num_date` is more than zero but fewer
than the total node count. These get a date-range filter, but aren't
able to be displayed as true time trees [1].

Note that this has one strange / misleading effect. For tips without
a `num_date` they are never filtered out by the date slider. This
is obvious when colouring by `num_date` (they are grey) but when using
a different colouring one must consider that a node may be visible
because it is inside the selected date range _or_ it doesn't have a
date!

[1] I explored hiding nodes without date information but the results
weren't ideal - it could be done, but it would require modifications to
the tree layout code.
  • Loading branch information
jameshadfield committed Jun 2, 2022
1 parent 1156578 commit c99f1a3
Show file tree
Hide file tree
Showing 4 changed files with 54 additions and 52 deletions.
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
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

0 comments on commit c99f1a3

Please sign in to comment.