auspice
Version:
Web app for visualizing pathogen evolution
182 lines (171 loc) • 5.91 kB
JavaScript
import { getAttrsOnTerminalNodes, getDefaultTreeState } from "../reducers/tree";
import { calendarToNumeric } from "./dateHelpers";
/**
* for each node, calculate the number of subtending tips (alive or dead)
* side effects: n.fullTipCount for each node
* @param root - deserialized JSON root to begin traversal
*/
const calcFullTipCounts = (node) => {
node.fullTipCount = 0;
if (typeof node.children !== "undefined") {
for (let i = 0; i < node.children.length; i++) {
calcFullTipCounts(node.children[i]);
node.fullTipCount += node.children[i].fullTipCount;
}
} else {
node.fullTipCount = 1;
}
};
/**
* Adds certain properties to the nodes array - for each node in nodes it adds
* node.fullTipCount - see calcFullTipCounts() description
* node.hasChildren {bool}
* node.arrayIdx {integer} - the index of the node in the nodes array
* @param {array} nodes redux tree nodes
* @return {array} input array (kinda unneccessary)
* side-effects: node.hasChildren (bool) and node.arrayIdx (INT) for each node in nodes
*/
const processNodes = (nodes) => {
calcFullTipCounts(nodes[0]); /* recursive. Uses d.children */
nodes.forEach((d, idx) => {
if (typeof d.attr === "undefined") d.attr = {};
d.arrayIdx = idx; /* set an index so that we can access visibility / nodeColors if needed */
d.hasChildren = typeof d.children !== "undefined";
d.yvalue = undefined; /* calculate later in auspice */
});
return nodes;
};
/**
* this function is changing as augur changes
* @param {obj} nodes - nodes
* @returns {list} avialble branch labels, with "none" the first element
* side-effects: deletes "clade_name", "named_clades", "clade_assignment" out of node.attrs for all nodes.
* adds node.attrs.labels {obj} to certain nodes.
*/
const processBranchLabelsInPlace = (nodes) => {
const availableBranchLabels = new Set();
nodes.forEach((n) => {
const labels = []; /* [clade (str), aa (str)] */
/* CLADE */
if (n.attr.clade_name) {
labels[0] = n.attr.clade_name;
delete n.attr.clade_name;
}
if (n.attr.clade_annotation) {
labels[0] = n.attr.clade_annotation;
delete n.attr.clade_annotation;
}
/* AA */
const muts = [];
if (n.aa_muts) {
for (const aa in n.aa_muts) { // eslint-disable-line
if (n.aa_muts[aa].length) {
muts.push(`${aa}: ${n.aa_muts[aa].join(", ")}`);
}
}
}
if (muts.length) {
labels[1] = muts.join("; ");
}
/* ADD TO ATTR */
if (labels.length) {
n.attr.labels = {};
if (labels[0]) {
n.attr.labels.clade = labels[0];
availableBranchLabels.add("clade");
}
if (labels[1]) {
n.attr.labels.aa = labels[1];
availableBranchLabels.add("aa");
}
}
});
return ["none", ...availableBranchLabels];
};
/**
* For each node visit if node not a hashMap key, insert
* into array. Then append node into end of the array.
* @params node - object to check
* @param hashMap - object literal used for deduping
* @param array - final array that nodes are inserted
*/
const visitNode = (node, hashMap, array) => {
if (!hashMap[node.clade]) {
hashMap[node.clade] = true;
array.push(node);
}
};
/**
* Pre-order tree traversal visits each node using stack.
* Checks if leaf node based on node.children
* pushes all children into stack and continues traversal.
* hashMap object literal used for deduping.
* @param root - deserialized JSON root to begin traversal
* @returns array - final array of nodes in order with no dups
*/
const flattenTree = (root) => {
const stack = [], array = [], hashMap = {};
stack.push(root);
while (stack.length !== 0) {
const node = stack.pop();
visitNode(node, hashMap, array);
if (node.children) {
for (let i = node.children.length - 1; i >= 0; i -= 1) {
stack.push(node.children[i]);
}
}
}
return array;
};
/**
* Add reference to node.parent for each node in tree
* For root add root.parent = root
* Pre-order tree traversal visits each node using stack.
* Checks if leaf node based on node.children
* pushes all children into stack and continues traversal.
* @param root - deserialized JSON root to begin traversal
*/
const appendParentsToTree = (root) => {
root.parent = root;
const stack = [];
stack.push(root);
while (stack.length !== 0) {
const node = stack.pop();
if (node.children) {
for (let i = node.children.length - 1; i >= 0; i -= 1) {
node.children[i].parent = node;
stack.push(node.children[i]);
}
}
}
};
/**
* if the metadata JSON defines vaccine strains then create an array of the nodes
* @param nodes - nodes
* @param vaccineChoices - undefined or the object from the metadata JSON linking strain names to dates
* @returns array - array of nodes that are vaccines. NOTE these are references to the nodes array
* side-effects: adds the vaccineDate property to the relevent nodes
*/
const processVaccines = (nodes, vaccineChoices) => {
if (!vaccineChoices) {return false;}
const names = Object.keys(vaccineChoices);
const vaccines = nodes.filter((d) => names.indexOf(d.strain) !== -1);
vaccines.forEach((d) => {
d.vaccineDate = vaccineChoices[d.strain];
d.vaccineDateNumeric = calendarToNumeric(vaccineChoices[d.strain]);
});
return vaccines;
};
export const treeJsonToState = (treeJSON, vaccineChoices) => {
appendParentsToTree(treeJSON);
const nodesArray = flattenTree(treeJSON);
const nodes = processNodes(nodesArray);
const vaccines = vaccineChoices ?
processVaccines(nodes, vaccineChoices) :
[];
const availableBranchLabels = processBranchLabelsInPlace(nodesArray);
const attrs = getAttrsOnTerminalNodes(nodes);
return Object.assign({}, getDefaultTreeState(), {
nodes, vaccines, availableBranchLabels, attrs, loaded: true
});
};