auspice
Version:
Web app for visualizing pathogen evolution
195 lines (182 loc) • 7.75 kB
JavaScript
import { freqScale, NODE_NOT_VISIBLE, NODE_VISIBLE_TO_MAP_ONLY, NODE_VISIBLE } from "./globals";
import { calcTipCounts } from "./treeCountingHelpers";
import { getTraitFromNode } from "./treeMiscHelpers";
export const getVisibleDateRange = (nodes, visibility) => nodes
.filter((node, idx) => (visibility[idx] === NODE_VISIBLE && !node.hasChildren))
.reduce((acc, node) => {
const nodeDate = getTraitFromNode(node, "num_date");
if (nodeDate && nodeDate < acc[0]) return [nodeDate, acc[1]];
if (nodeDate && nodeDate > acc[1]) return [acc[0], nodeDate];
return acc;
}, [100000, -100000]);
export const strainNameToIdx = (nodes, name) => {
let i;
for (i = 0; i < nodes.length; i++) {
if (nodes[i].name === name) {
return i;
}
}
console.error("strainNameToIdx couldn't find strain");
return 0;
};
/**
* Find the node with the given label name & value
* NOTE: if there are multiple nodes with the same label then the first encountered is returned
* @param {Array} nodes tree nodes (flat)
* @param {string} labelName label name
* @param {string} labelValue label value
* @returns {int} the index of the matching node (0 if no match found)
*/
export const getIdxMatchingLabel = (nodes, labelName, labelValue) => {
let i;
for (i = 0; i < nodes.length; i++) {
if (
nodes[i].branch_attrs &&
nodes[i].branch_attrs.labels !== undefined &&
nodes[i].branch_attrs.labels[labelName] === labelValue
) {
return i;
}
}
console.error(`getIdxMatchingLabel couldn't find label ${labelName}===${labelValue}`);
return 0;
};
/** calcBranchThickness **
* returns an array of node (branch) thicknesses based on the tipCount at each node
* If the node isn't visible, the thickness is 1.
* Pure.
* @param nodes - JSON nodes
* @param visibility - visibility array (1-1 with nodes)
* @param rootIdx - nodes index of the currently in-view root
* @returns array of thicknesses (numeric)
*/
const calcBranchThickness = (nodes, visibility, rootIdx) => {
let maxTipCount = nodes[rootIdx].tipCount;
/* edge case: no tips selected */
if (!maxTipCount) {
maxTipCount = 1;
}
return nodes.map((d, idx) => {
if (visibility[idx] === NODE_VISIBLE) {
return freqScale((d.tipCount + 5) / (maxTipCount + 5));
}
return 0.5;
});
};
/* recursively mark the parents of a given node active
by setting the node idx to true in the param visArray */
const makeParentVisible = (visArray, node) => {
if (node.arrayIdx === 0 || visArray[node.parent.arrayIdx]) {
return; // this is the root of the tree or the parent was already visibile
}
visArray[node.parent.arrayIdx] = true;
makeParentVisible(visArray, node.parent);
};
/**
* Create a visibility array to show the path through the tree to the selected tip
* @param {array} nodes redux tree nodes
* @param {int} tipIdx idx of the selected tip
* @return {array} visibility array (values in {0, 1, 2})
*/
const identifyPathToTip = (nodes, tipIdx) => {
const visibility = new Array(nodes.length).fill(false);
visibility[tipIdx] = true;
makeParentVisible(visibility, nodes[tipIdx]); /* recursive */
return visibility.map((cv) => cv ? NODE_VISIBLE : NODE_NOT_VISIBLE);
};
/* calcVisibility
USES:
inView: attribute of phyloTree.nodes, but accessible through redux.tree.nodes[idx].shell.inView
Bool. Set by phyloTree, determines if the tip is within the view.
controls.filters
use dates NOT controls.dateMin & controls.dateMax
RETURNS:
visibility: array of integers in {0, 1, 2}
- 0: not displayed by map. Potentially displayed by tree as a thin branch.
- 1: available for display by the map. Displayed by tree as a thin branch.
- 2: Displayed by both the map and the tree.
ROUGH DESCRIPTION OF HOW FILTERING IS APPLIED:
- inView filtering (reflects tree zooming): Nodes which are not inView always have visibility=0
- time filtering is simple - all nodes (internal + terminal) not within (tmin, tmax) are excluded.
- filters are a bit more tricky - the visibile tips are calculated, and the parent
branches back to the MRCA are considered visibile. This is then intersected with
the time & inView visibile stuff
FILTERS:
- controls.filters (redux) is a dict of trait name -> values
- filters (in this code) is a list of filters to apply
e.g. [{trait: "country", values: [...]}, ...]
*/
const calcVisibility = (tree, controls, dates) => {
if (tree.nodes) {
/* inView represents nodes that are within the current view window (i.e. not off the screen) */
let inView;
try {
inView = tree.nodes.map((d) => d.shell.inView);
} catch (e) {
/* edge case: this fn may be called before the shell structure of the nodes
* has been created (i.e. phyloTree's not run yet). In this case, it's
* safe to assume that everything's in view */
inView = tree.nodes.map((d) => d.inView !== undefined ? d.inView : true);
}
// FILTERS
let filtered; // array of bools, same length as tree.nodes. true -> that node should be visible
const filters = [];
Object.keys(controls.filters).forEach((trait) => {
if (controls.filters[trait].length) {
filters.push({trait, values: controls.filters[trait]});
}
});
if (filters.length) {
/* find the terminal nodes that were (a) already visibile and (b) match the filters */
filtered = tree.nodes.map((d, idx) => (
!d.hasChildren && inView[idx] && filters.every((f) => f.values.includes(getTraitFromNode(d, f.trait)))
));
const idxsOfFilteredTips = filtered.reduce((a, e, i) => {
if (e) {a.push(i);}
return a;
}, []);
/* for each visibile tip, make the parent nodes visible (recursively) */
for (let i = 0; i < idxsOfFilteredTips.length; i++) {
makeParentVisible(filtered, tree.nodes[idxsOfFilteredTips[i]]);
}
}
/* intersect the various arrays contributing to visibility */
const visibility = tree.nodes.map((node, idx) => {
if (inView[idx] && (filtered ? filtered[idx] : true)) {
const nodeDate = getTraitFromNode(node, "num_date");
const parentNodeDate = getTraitFromNode(node.parent, "num_date");
if (!nodeDate || !parentNodeDate) {
return NODE_VISIBLE;
}
/* if branchLengthsToDisplay is "divOnly", then ensure node displayed */
if (controls.branchLengthsToDisplay === "divOnly") {
return NODE_VISIBLE;
}
/* is the actual node date (the "end" of the branch) in the time slice? */
if (nodeDate >= dates.dateMinNumeric && nodeDate <= dates.dateMaxNumeric) {
return NODE_VISIBLE;
}
/* is any part of the (parent date -> node date) in the time slice? */
if (!(nodeDate < dates.dateMinNumeric || parentNodeDate > dates.dateMaxNumeric)) {
return NODE_VISIBLE_TO_MAP_ONLY;
}
}
return NODE_NOT_VISIBLE;
});
return visibility;
}
console.error("calcVisibility ran without tree.nodes");
return NODE_VISIBLE;
};
export const calculateVisiblityAndBranchThickness = (tree, controls, dates, {idxOfInViewRootNode = 0, tipSelectedIdx = 0} = {}) => {
const visibility = tipSelectedIdx ? identifyPathToTip(tree.nodes, tipSelectedIdx) : calcVisibility(tree, controls, dates);
/* recalculate tipCounts over the tree - modifies redux tree nodes in place (yeah, I know) */
calcTipCounts(tree.nodes[0], visibility);
/* re-calculate branchThickness (inline) */
return {
visibility: visibility,
visibilityVersion: tree.visibilityVersion + 1,
branchThickness: calcBranchThickness(tree.nodes, visibility, idxOfInViewRootNode),
branchThicknessVersion: tree.branchThicknessVersion + 1
};
};