patristic
Version:
Patristic Inference library for Node and Browser
1,255 lines (1,167 loc) • 39.9 kB
JavaScript
/**
* The [SemVer](https://semver.org/) version string of the patristic library
* @type {String} A string specifying the current version of the Patristic Library.
* If not given, the version of patristic you are using if less than or equal to 0.2.2.
* @example
* console.log(patristic.version);
*/
export const version = "0.5.7";
/**
* A class for representing Branches in trees.
* It's written predominantly for phylogenetic trees (hence the
* [Newick parser](#parseNewick),
* [neighbor-joining implementation](#parseMatrix), etc.), but could
* conceivably be useful for representing other types of trees as well.
* @param {Object} [data] An object containing data you wish to assign to
* this Branch object. In particular, intended to overwrite the default
* attributes of a Branch, namely `id`, `parent`, `length`, and `children`.
* @constructor
*/
export function Branch(data, children) {
if (!data) data = {};
if (!children) children = d => d.children;
Object.assign(this, {
_guid: guid(),
id: data.id || "",
data: data,
depth: data.depth || 0,
height: data.height || 0,
length: data.length || 0,
parent: data.parent || null,
children: children(data) || [],
value: data.value || 1,
respresenting: 1
});
}
function guid(){
return ([1e7] + -1e3 + -4e3 + -8e3 + -1e11).replace(/[018]/g, a => {
return (a ^ ((Math.random() * 16) >> (a / 4))).toString(16);
});
}
/**
* Adds a new child to this Branch
* @param {(Branch|Object)} [data={}] The new Branch, or data to attach to it.
* @return {Branch} The (possibly new) child Branch
*/
Branch.prototype.addChild = function(data) {
let c;
if (data instanceof Branch) {
c = data;
c.parent = this;
} else {
if (!data) data = {};
c = new Branch(
Object.assign(data, {
parent: this
})
);
}
this.children.push(c);
return c;
};
/**
* Adds a new parent to this Branch. This is a bit esoteric and generally not
* recommended.
* @param {(Branch|Object)} [data={}] A Branch object, or the data to attach to one
* @param {Array} [siblings=[]] An array of Branches to be the children of the new parent Branch (i.e. siblings of this Branch)
* @return {Branch} The Branch on which this was called
*/
Branch.prototype.addParent = function(data, siblings) {
if (!siblings) siblings = [];
let c;
if (data instanceof Branch) {
c = data;
} else {
if (!data) data = {};
c = new Branch(Object.assign(data));
}
siblings.forEach(sib => sib.setParent(c));
c.children = [this].concat(siblings);
this.parent = c;
return this;
};
/**
* Returns an array of Branches from this Branch to the root.
* [d3-hierarchy compatibility method.](https://github.com/d3/d3-hierarchy#node_ancestors)
* @type {Array} An array of Branches
*/
Branch.prototype.ancestors = function() {
return this.getAncestors(true);
};
/**
* Returns a deep clone of the Branch on which it is called. Note that this does
* not clone all descendants, rather than providing references to the existing
* descendant Branches.
* @return {Branch} A clone of the Branch on which it is called.
*/
Branch.prototype.clone = function() {
return parseJSON(this.toObject());
};
/**
* All descendant Branches with near-zero length are excised
* @return {Branch} The Branch on which this method was called.
*/
Branch.prototype.consolidate = function() {
return this.eachAfter(branch => {
if (branch.isRoot() || branch.length >= 0.0005) return;
if(branch.parent.id == ""){
branch.parent.id = branch.id;
} else {
branch.parent.id += '+'+branch.id;
}
branch.excise();
}).fixDistances();
};
/**
* Returns a clone of the Branch on which it is called. Note that this also
* clones all descendants, rather than providing references to the existing
* descendant Branches. (For a shallow clone, see [Branch.clone](#clone).
* Finally, the cloned Branch will become the root of the cloned tree, having a
* parent of `null`.
* [d3-hierarchy compatibility method.](https://github.com/d3/d3-hierarchy#node_copy)
* @return {Branch} A clone of the Branch on which it is called.
*/
Branch.prototype.copy = function() {
let newThis = parseJSON(JSON.stringify(this));
newThis.parent = null;
return newThis.fixDistances();
};
/**
* Sets the values of all nodes to be equal to the number of their descendants.
* @return {Branch} The Branch on which it was called
*/
Branch.prototype.count = function() {
return this.sum(() => 1);
};
/**
* Returns an array pf descendants, starting with this Branch.
* [d3-hierarchy compatibility method.](https://github.com/d3/d3-hierarchy#node_descendants)
* @type {Array} An Array of Branches, starting with this one.
*/
Branch.prototype.descendants = function() {
return this.getDescendants(true);
};
/**
* Returns the depth of a given child, relative to the Branch on which it is
* called.
* @param {(Branch|String)} descendant A descendant Branch (or `id` string
* thereof)
* @return {Number} The sum of the lengths of all Branches between the Branch on
* which it is called and `descendant`. Throws an error if `descendant` is not a
* descendant of this Branch.
*/
Branch.prototype.depthOf = function(descendant) {
let distance = 0;
if (typeof descendant == "string")
descendant = this.getDescendant(descendant);
if (typeof descendant == "undefined")
throw Error("Cannot compute depth of undefined descendant!");
let current = descendant;
while (current != this) {
distance += current.length;
current = current.parent;
}
return distance;
};
/**
* Computes the patristic distance between `descendantA` and `descendantB`.
* @param {Branch} descendantA The Branch from which you wish to compute
* distance
* @param {Branch} descendantB The Branch to which you wish to compute distance
* @return {number} The patristic distance between the given descendants.
*/
Branch.prototype.distanceBetween = function(descendantA, descendantB) {
let mrca = descendantA.getMRCA(descendantB);
return mrca.depthOf(descendantA) + mrca.depthOf(descendantB);
};
/**
* Computes the patristic distance between `cousin` and the Branch on which
* this method is called.
* @param {Branch} cousin The Branch to which you wish to compute distance
* @return {number} The patristic distance between `cousin` and the Branch on
* this method is called.
*/
Branch.prototype.distanceTo = function(cousin) {
let mrca = this.getMRCA(cousin);
return mrca.depthOf(this) + mrca.depthOf(cousin);
};
/**
* Visits each Branch descended from the Branch on which it is called in
* [Breadth First Search](https://en.wikipedia.org/wiki/Breadth-first_search)
* order and returns the Branch on which it was called.
* @param {Function} callback The function to be run on each Branch
* @return {Branch} The Branch on which it was called.
*/
Branch.prototype.each = function(callback) {
let branch = this,
next = [branch],
current;
while (next.length) {
current = next.reverse();
next = [];
while ((branch = current.pop())) {
callback(branch);
branch.eachChild(child => next.push(child));
}
}
return this;
};
/**
* Visits each Branch descended from the Branch on which it is called in
* [post-traversal order](https://en.wikipedia.org/wiki/Tree_traversal#Post-order)
* and returns the Branch on which it was called.
* @param {Function} callback Function to run on each Branch
* @return {Branch} The Branch on which it was called
*/
Branch.prototype.eachAfter = function(callback) {
this.eachChild(child => child.eachAfter(callback));
callback(this);
return this;
};
/**
* Visits each Branch descended from the Branch on which it is called in
* [pre-traversal order](https://en.wikipedia.org/wiki/Tree_traversal#Pre-order)
* and returns the Branch on which it was called.
* @param {Function} callback [description]
* @return {[type]} [description]
*/
Branch.prototype.eachBefore = function(callback) {
callback(this);
this.eachChild(child => child.eachBefore(callback));
return this;
};
/**
* Runs a function on each child of the Branch on which it is called.
* @param {Function} callback The function to run on each child.
* @return {Branch} The Branch on which it was called.
*/
Branch.prototype.eachChild = function(callback) {
this.children.forEach(callback);
return this;
};
/**
* Excises the Branch on which it is called and updates its parent and children.
* @return {Branch} The parent of the excised Branch.
*/
Branch.prototype.excise = function() {
if (this.isRoot() && this.children.length > 1) {
throw new Error("Cannot excise a root Branch with multiple children.");
}
this.eachChild(child => {
child.length += this.length;
child.parent = this.parent;
if (!this.isRoot()) this.parent.children.push(child);
});
this.parent.children.splice(this.parent.children.indexOf(this), 1);
this.parent.representing++;
return this.parent;
};
/**
* Sets the distance values (height and depth) for each Branch
* @return {Branch} The Branch on which it is called.
*/
Branch.prototype.fixDistances = function() {
let maxdepth = 0,
root = this.getRoot();
root.depth = 0;
this.eachBefore(d => {
if (d.isRoot()) return;
d.depth = d.parent.depth + 1;
if (d.depth > maxdepth) maxdepth = d.depth;
}).eachAfter(d => {
d.height = maxdepth - d.depth;
d.value = d.value + d.children.reduce((a, c) => a + c.value, 0);
});
return this;
};
/**
* Repairs incorrect links by recurively confirming that children reference
* their parents, and correcting those references if they do not.
*
* If you need to call this, something has messed up the state of your tree
* and you should be concerned about that. Just FYI. ¯\\_(ツ)_/¯
* @param {Boolean} nonrecursive Should this just fix the children of the
* Branch on which it is called, or all descendants?
* @return {Branch} The Branch on which it was called.
*/
Branch.prototype.fixParenthood = function(nonrecursive) {
this.children.forEach(child => {
if (!child.parent) child.parent = this;
if (child.parent !== this) child.parent = this;
if (!nonrecursive && child.children.length > 0) {
child.fixParenthood();
}
});
return this;
};
/**
* Reverses the order of (all of) the descendants of the Branch.
* @return {Branch} The Branch on which this was called.
*/
Branch.prototype.flip = function() {
return this.each(c => c.rotate());
};
/**
* Returns an Array of all the ancestors of the Branch on which it is called.
* Note that this does not include itself. For all ancestors and itself, see
* [Branch.ancestors](#ancestors)
* @param {Boolean} includeSelf Should the Branch on which this is called be
* included in the results?
* @return {Array} Every Ancestor of the Branch on which it was called.
*/
Branch.prototype.getAncestors = function(includeSelf) {
let ancestors = includeSelf ? [this] : [];
let current = this;
while ((current = current.parent)) ancestors.push(current);
return ancestors;
};
/**
* Given an `childID`, returns the child with that id (or `undefined` if no such
* child is present).
* @param {String} childID the ID of the child to return.
* @return {(Branch|undefined)} The desired child Branch, or `undefined` if the
* child doesn't exist.
*/
Branch.prototype.getChild = function(childID) {
if (!typeof childID == "string") throw Error("childID is not a String!");
return this.children.find(c => c.id === childID);
};
/**
* Given an id string, returns the descendant Branch with that ID, or
* `undefined` if it doesn't exist.
* @param {String} id The id string of the Branch to find
* @return {(Branch|undefined)} The descendant Branch, or `undefined` if it
* doesn't exist
*/
Branch.prototype.getDescendant = function(id) {
if (this.id === id) return this;
let children = this.children,
n = children.length;
if (children) {
for (let i = 0; i < n; i++) {
let descendant = children[i].getDescendant(id);
if (descendant) return descendant;
}
}
};
/**
* Returns an array of all Branches which are descendants of this Branch
* @param {Boolean} [includeSelf] Is this not the Branch on which the user
* called the function? This is used internally and should be ignored.
* @return {Array} An array of all Branches descended from this Branch
*/
Branch.prototype.getDescendants = function(includeSelf) {
let descendants = includeSelf ? [this] : [];
if (!this.isLeaf()) {
this.children.forEach(child => {
child.getDescendants(true).forEach(d => descendants.push(d));
});
}
return descendants;
};
/**
* Returns an array of all leaves which are descendants of this Branch.
* Alias of [getLeaves](#getLeaves) for people whose strong suit isn't spelling.
* @return {Array} An array of all leaves descended from this Branch
*/
Branch.prototype.getLeafs = function() {
return this.getLeaves();
};
/**
* Returns an array of all leaves which are descendants of this Branch
* See also: [getLeafs](#getLeafs)
* @return {Array} An array of all leaves descended from this Branch
*/
Branch.prototype.getLeaves = function() {
if (this.isLeaf()) {
return [this];
} else {
let descendants = [];
this.children.forEach(child => {
child.getLeaves().forEach(d => descendants.push(d));
});
return descendants;
}
throw new Error("Something very weird happened. Sorry about that!");
};
/**
* Traverses the tree upward until it finds the Most Recent Common Ancestor
* (i.e. the first Branch for which both the Branch on which it was called and
* `cousin` are descendants).
* @return {Branch} The Most Recent Common Ancestor of both the Branch on
* which it was called and the `cousin`.
*/
Branch.prototype.getMRCA = function(cousin) {
let mrca = this;
while (!mrca.hasDescendant(cousin)) {
if (mrca.isRoot())
throw Error(
"Branch and cousin do not appear to share a common ancestor!"
);
mrca = mrca.parent;
}
return mrca;
};
/**
* Traverses the tree upward until it finds the root Branch, and returns the
* root.
* @return {Branch} The root Branch of the tree
*/
Branch.prototype.getRoot = function() {
let branch = this;
while (!branch.isRoot()) branch = branch.parent;
return branch;
};
/**
* Determines if a given Branch (or ID) is a child of this Branch
* @param {(Branch|String)} child The Branch (or the id thereof) to check for
* @return {Boolean}
*/
Branch.prototype.hasChild = function(child) {
if (child instanceof Branch) {
return this.children.includes(child);
} else if (typeof child === "string") {
return this.children.some(c => c.id === child);
}
throw Error(
`Unknown type of child (${typeof child}) passed to Branch.hasChild!`
);
};
/**
* Checks to see if `descendant` is a descendant of the Branch on which this
* method is called.
* @param {(Branch|String)} descendant Either the descendant Branch or its'
* `id`.
* @return {Boolean} True if `descendant` is descended from the Branch from
* which this is called, otherwise false.
*/
Branch.prototype.hasDescendant = function(descendant) {
let descendants = this.getDescendants();
if (descendant instanceof Branch) {
return descendants.some(d => d === descendant);
} else if (typeof descendant === "string") {
return descendants.some(d => d.id === descendant);
}
throw Error("Unknown type of descendant passed to Branch.hasDescendant!");
};
/**
* Checks to see if a Branch has a descendant leaf.
* @return {Boolean} True if leaf is both a leaf and a descendant of the
* Branch on which this method is called, False otherwise.
*/
Branch.prototype.hasLeaf = function(leaf) {
let leaves = this.getleaves();
if (leaf instanceof Branch) {
return leaves.includes(leaf);
} else if (typeof leaf === "string") {
return leaves.some(d => d.id === leaf);
}
throw Error("Unknown type of leaf passed to Branch.hasLeaf.");
};
/**
* Swaps the branch on which it is called with its parent. This method is
* probably only useful as an internal component of [Branch.reroot](#reroot).
* @return {Branch} The Branch object on which it was called.
*/
Branch.prototype.invert = function() {
let oldParent = this.parent;
if (oldParent) {
let temp = this.parent.length;
this.parent.length = this.length;
this.length = temp;
this.parent = oldParent.parent;
this.children.push(oldParent);
oldParent.parent = this;
oldParent.children.splice(oldParent.children.indexOf(this), 1);
} else {
throw Error("Cannot invert root node!");
}
return this;
};
/**
* Returns whether the Branch on which it is called is a child of a given parent
* (or parent ID).
* @param {(Branch|String)} parent A Branch (or ID thereof) to test for
* paternity of this Branch.
* @return {Boolean} True is `parent` is the parent of this Branch, false
* otherwise.
*/
Branch.prototype.isChildOf = function(parent) {
if (parent instanceof Branch) return this.parent === parent;
if (typeof parent === "string") return this.parent.id === parent;
throw Error("Unknown parent type passed to Branch.isChildOf");
};
/**
* Tests whether this and each descendant Branch holds correct links to both
* its parent and its children.
* @return {Boolean} True if consistent, otherwise false
*/
Branch.prototype.isConsistent = function() {
if (!this.isRoot()) {
if (!this.parent.children.includes(this)) return false;
}
if (!this.isLeaf()) {
if (this.children.some(c => c.parent !== this)) return false;
return this.children.every(c => c.isConsistent());
}
return true;
};
/**
* Returns whether a given Branch is an ancestor of the Branch on which this
* method is called. Uses recursive tree-climbing.
* @param {Branch} ancestor The Branch to check for ancestorhood
* @return {Boolean} If this Branch is descended from `ancestor`
*/
Branch.prototype.isDescendantOf = function(ancestor) {
if (!ancestor || !this.parent) return false;
if (this.parent === ancestor || this.parent.id === ancestor) return true;
return this.parent.isDescendantOf(ancestor);
};
/**
* Returns a boolean indicating if this Branch is a leaf (i.e. has no
* children).
* @return {Boolean} True is this Branch is a leaf, otherwise false.
*/
Branch.prototype.isLeaf = function() {
return this.children.length === 0;
};
/**
* Returns a boolean indicating whether or not this Branch is olate.
*
* ...Just kidding!
*
* Isolates a Branch and its subtree (i.e. removes everything above it, making
* it the root Branch). Similar to [Branch.remove](#remove), only it returns
* the Branch on which it is called.
* @return {Branch} The Branch object on which it was called.
*/
Branch.prototype.isolate = function() {
let index = this.parent.children.indexOf(this);
this.parent.children.splice(index, 1);
this.setParent(null);
return this;
};
/**
* Returns a boolean indicating if this Branch is the root of a tree (i.e. has
* no parents).
* @return {Boolean} True if this Branch is the root, otherwise false.
*/
Branch.prototype.isRoot = function() {
return this.parent === null;
};
/**
* Returns the array of leaf nodes in traversal order; leaves are nodes with no
* children. Alias of [Branch.getLeaves](#getLeaves) `cuz spelling is hard.
* @type {Array} An Array of Branches which are descended from this Branch and
* have no children.
*/
Branch.prototype.leafs = function() {
return this.getLeaves();
};
/**
* Returns the array of leaf nodes in traversal order; leaves are nodes with no
* children. Alias of [Branch.getLeaves](#getLeaves).
* [d3-hierarchy compatibility method.](https://github.com/d3/d3-hierarchy#node_leaves)
* @type {Array} An Array of Branches which are descended from this Branch and
* have no children.
*/
Branch.prototype.leaves = function() {
return this.getLeaves();
};
/**
* Returns an Array of links, which are plain javascript objects containing a
* `source` attribute (which is a reference to the parent Branch) and a `target`
* attribute (which is a reference to the child Branch).
* [d3-hierarchy compatibility method](https://github.com/d3/d3-hierarchy#node_links)
* @return {Array} An array of plain Javascript objects
*/
Branch.prototype.links = function() {
let links = [];
this.each(d => {
if (d.isRoot()) return;
links.push({
source: d.parent,
target: d
});
});
return links;
};
/**
* Normalizes this and all descendant Branches `value` attributes to between
* `newmin` and `newmax`. Note that normalize can function as its own inverse
* when passed an original range. For example:
* @example tree.normalize().normalize(1, tree.getDescendants().length + 1);
* @param {Number} newmin The desired minimum value.
* @param {Number} newmax The desired maximum value.
* @return {Branch} The Branch on which it was called.
*/
Branch.prototype.normalize = function(newmin, newmax) {
if (typeof newmax !== "number") newmax = 1;
if (typeof newmin !== "number") newmin = 0;
let min = Infinity,
max = -Infinity;
this.each(d => {
if (d.value < min) min = d.value;
if (d.value > max) max = d.value;
});
let ratio = (newmax - newmin) / (max - min);
return this.each(d => (d.value = (d.value - min) * ratio + newmin));
};
/**
* Gets the path from this Branch to `target`. If this Branch and `target` are
* the same, returns an array containing only the Branch on which it is called.
* @param {Branch} target A Branch object
* @return {Array} An ordered Array of Branches following the path between this
* Branch and `target`
*/
Branch.prototype.path = function(target) {
let current = this;
let branches = [this];
let mrca = this.getMRCA(target);
while (current !== mrca) {
current = current.parent;
branches.push(current);
}
let k = branches.length;
current = target;
while (current !== mrca) {
branches.splice(k, 0, current);
current = current.parent;
}
return branches;
};
/**
* Removes a Branch and its subtree from the tree. Similar to
* [Branch.isolate](#isolate), only it returns the root Branch of the tree
* from which this Branch is removed.
* @param {Boolean} pruneAncestors - If true, removes ancestors with no remaining children as well
* @return {Branch} The root of the remaining tree.
*/
Branch.prototype.remove = function(pruneAncestors) {
let root = this.getRoot();
this.isolate();
if (pruneAncestors) {
this.parent?.removeIfNoChildren();
}
return root;
};
/**
* Removes the branch if it has no children. Then recursively removes all
* ancestors with no children.
*
* @param {Boolean} nonrecursive - If true, does not remove ancestors with no children
* @return {Branch} The root of the modified tree.
*/
Branch.prototype.removeIfNoChildren = function(nonrecursive) {
let root = this.getRoot();
if (this.children.length === 0) {
this.remove();
if (!nonrecursive) {
this.parent.removeIfNoChildren();
}
}
return root;
}
/**
* Removes a Branch and its subtree from the tree, and replaces it.
* @param {Branch} replacement - The branch to replace the branch on which the
* method is called.
* @return {Branch} The root of the modified tree.
*/
Branch.prototype.replace = function(replacement) {
let root = this.getRoot();
let parent = this.parent;
let index = this.parent.children.indexOf(this);
this.parent.children.splice(index, 1, replacement);
return root;
};
/**
* Reroots a tree on this Branch. Use with caution, this returns the new root,
* which should typically supplant the existing root Branch object, but does
* not replace that root automatically.
* @example
* tree = tree.children[0].children[0].reroot();
* @return {Branch} The new root Branch, which is the Branch on which this was
* called
*/
Branch.prototype.reroot = function() {
let current = this;
let toInvert = [];
while (!current.isRoot()) {
toInvert.push(current);
current = current.parent;
}
toInvert.reverse().forEach(c => c.invert());
return this.fixDistances();
};
/**
* Reverses the order of the children of the branch on which it is called.
* @return {Branch} The Branch on which this was called.
*/
Branch.prototype.rotate = function(recursive) {
if (!this.children) return this;
this.children.reverse();
return this;
};
/**
* Set the length of a Branch
* @param {number} length The new length to assign to the Branch
* @return {Branch} The Branch object on which this was called
*/
Branch.prototype.setLength = function(length) {
this.length = length;
return this;
};
/**
* Sets the parent of the Branch on which it is called.
* @param {Branch} parent The Branch to set as parent
* @return {Branch} The Branch on which this method was called.
*/
Branch.prototype.setParent = function(parent) {
if (!parent instanceof Branch && parent !== null)
throw Error("Cannot set parent to non-Branch object!");
this.parent = parent;
return this;
};
/**
* Collapses each descendant Branch with exactly one child into a single
* continuous branch.
* @return {Branch} The Branch on which this method was called.
*/
Branch.prototype.simplify = function() {
this.eachAfter(branch => {
if(branch.children.length == 1){
let child = branch.children[0];
if(child.id == ''){
child.id = branch.id;
} else {
child.id = branch.id + "+" + child.id;
}
branch.excise();
}
});
return this.fixDistances();
};
/**
* Sorts the Tree from the branch on which it is called downward.
* @param {Function} [comparator] A Function taking two Branches and returning
* a numberic value. For details, see [MDN Array.sort](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/sort#Description)
* @return {Branch} The Branch on which it was called
*/
Branch.prototype.sort = function(comparator) {
if (!comparator) comparator = (a, b) => a.value - b.value;
return this.eachBefore(d => d.children.sort(comparator));
};
/**
* Determines whether this Branch is likelier to be a source of `cousin`, or
* if `cousin` is a source of this Branch.
* @param {Branch} cousin The other Branch to test
* @return {Boolean} True if this might be the source of cousin, otherwise
* false.
*/
Branch.prototype.sources = function(cousin) {
let mrca = this.getMRCA(cousin);
return mrca.depthOf(this) < mrca.depthOf(cousin);
};
/**
* Computes the value of each Branch according to some valuator function
* @param {Function} value A Function taking a Branch and returning a
* (numeric?) value.
* @return {Branch} The Branch on which it was called.
*/
Branch.prototype.sum = function(value) {
if (!value) value = d => d.value;
return this.eachAfter(
d => (d.value = value(d) + d.children.reduce((a, c) => a + c.value, 0))
);
};
/**
* Determines whether this Branch is likelier to be a target of `cousin`, or
* if `cousin` is a target of this Branch.
* @param {Branch} cousin The other Branch to test
* @return {Boolean} True if this might be the target of cousin, otherwise
* false.
*/
Branch.prototype.targets = function(cousin) {
return cousin.sources(this);
};
/**
* toJSON is an alias for [toObject](#toObject), enabling the safe use of
* `JSON.stringify` on Branch objects (in spite of their circular references).
* @type {Function}
* @returns {Object} A serializable Object
*/
Branch.prototype.toJSON = function() {
return this.toObject();
};
/**
* Computes a matrix of all patristic distances between all leaves which are
* descendants of the Branch on which this method is called.
* @return {Object} An Object containing a matrix (an Array of Arrays) and
* Array of `id`s corresponding to the rows (and columns) of the matrix.
*/
Branch.prototype.toMatrix = function() {
let leafs = this.getLeaves();
let n = leafs.length;
let matrix = new Array(n);
for (let i = 0; i < n; i++) {
matrix[i] = new Array(n);
matrix[i][i] = 0;
for (let j = 0; j < i; j++) {
let distance = leafs[i].distanceTo(leafs[j]);
matrix[i][j] = distance;
matrix[j][i] = distance;
}
}
return {
matrix: matrix,
ids: leafs.map(d => d.id)
};
};
/**
* Returns the Newick representation of this Branch and its descendants.
* @param {Boolean} [nonterminus=falsy] Is this not the terminus of the
* Newick Tree? This should be falsy when called by a user (i.e. you). It's
* used internally to decide whether or not in include a semicolon in the
* returned string.
* @return {String} The [Newick](https://en.wikipedia.org/wiki/Newick_format)
* representation of the Branch.
*/
Branch.prototype.toNewick = function(nonterminus) {
let out = "";
if (!this.isLeaf()) {
out +=
"(" + this.children.map(child => child.toNewick(true)).join(",") + ")";
}
out += this.id;
if (this.length) out += ":" + numberToString(this.length);
if (!nonterminus) out += ";";
return out;
};
//This function takes a number and returns a string representation that does
//not use Scientific Notation.
//It's adapted from [StackOverflow](https://stackoverflow.com/a/46545519/521121),
//Which makes it available under the [CC BY-SA 3.0 License](https://creativecommons.org/licenses/by-sa/3.0/)
function numberToString(num) {
let numStr = String(num);
if (Math.abs(num) < 1.0) {
let e = parseInt(num.toString().split("e-")[1]);
if (e) {
let negative = num < 0;
if (negative) num *= -1;
num *= Math.pow(10, e - 1);
numStr = "0." + new Array(e).join("0") + num.toString().substring(2);
if (negative) numStr = "-" + numStr;
}
} else {
let e = parseInt(num.toString().split("+")[1]);
if (e > 20) {
e -= 20;
num /= Math.pow(10, e);
numStr = num.toString() + new Array(e + 1).join("0");
}
}
return numStr;
}
/**
* Returns a simple Javascript object version of this Branch and its
* descendants. This is useful in cases where you want to serialize the tree
* (e.g. `JSON.stringify(tree)`) but can't because the tree contains circular
* references (for simplicity, elegance, and performance reasons, each Branch
* tracks both its children and its parent).
* @return {Object} A serializable bare Javascript Object representing this
* Branch and its descendants.
*/
Branch.prototype.toObject = function() {
let output = {
id: this.id,
length: this.length
};
if (this.children.length > 0)
output.children = this.children.map(c => c.toObject());
return output;
};
/**
* Returns a valid JSON-string version of this Branch and its descendants.
* @param {Function} replacer A replacer function to [pass to `JSON.stringify`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/stringify#Parameters).
* @param {(Number|String)} space A string or number of spaces to use for
* indenting the output. See https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/stringify#Parameters
* for additional details.
* @return {Object} A valid JSON string representing this Branch and its
* descendants.
*/
Branch.prototype.toString = function(replacer, width) {
if (!replacer) replacer = null;
if (!width) width = 0;
return JSON.stringify(this, replacer, width);
};
/**
* Parses a hierarchical JSON string (or Object) as a Branch object.
* @param {(String|Object)} json A json string (or Javascript Object)
* representing hierarchical data.
* @param {String} [idLabel="id"] The key used in the objects of `json` to
* indicate their identifiers.
* @param {String} [lengthLabel='length'] The key used in the objects of `json`
* to indicate their length.
* @param {String} [childrenLabel=`children`] The key used in the objects of
* `json` to indicate their children.
* @return {Branch} The Branch representing the root of the hierarchy
* represented by `json`.
*/
export function parseJSON(json, idLabel, lengthLabel, childrenLabel) {
if (!idLabel) idLabel = "id";
if (!lengthLabel) lengthLabel = "length";
if (!childrenLabel) childrenLabel = "children";
if (typeof json === "string") json = JSON.parse(json);
let root = new Branch({
id: json[idLabel],
length: json[lengthLabel]
});
if (json[childrenLabel] instanceof Array) {
json[childrenLabel].forEach(child => {
root.addChild(parseJSON(child));
});
}
return root.fixDistances();
}
/**
* Parses a matrix of distances and returns the root Branch of the output tree.
* This is adapted from Maciej Korzepa's [neighbor-joining](https://github.com/biosustain/neighbor-joining),
* which is released for modification under the [MIT License](https://opensource.org/licenses/MIT).
* @param {Array} matrix An array of `n` arrays of length `n`
* @param {Array} labels An array of `n` strings, each corresponding to the
* values in `matrix`.
* @return {Branch} A Branch object representing the root Branch of the tree
* inferred by neighbor joining on `matrix`.
*/
export function parseMatrix(matrix, labels) {
let that = {};
let N = (that.N = matrix.length);
if (!labels) labels = [...Array(N).keys()];
that.cN = that.N;
that.D = matrix;
that.labels = labels;
that.labelToTaxon = {};
that.currIndexToLabel = new Array(N);
that.rowChange = new Array(N);
that.newRow = new Array(N);
that.labelToNode = new Array(2 * N);
that.nextIndex = N;
that.I = new Array(that.N);
that.S = new Array(that.N);
for (let i = 0; i < that.N; i++) {
let sortedRow = sortWithIndices(that.D[i], i, true);
that.S[i] = sortedRow;
that.I[i] = sortedRow.sortIndices;
}
that.removedIndices = new Set();
that.indicesLeft = new Set();
for (let i = 0; i < N; i++) {
that.currIndexToLabel[i] = i;
that.indicesLeft.add(i);
}
that.rowSumMax = 0;
that.PNewick = "";
let minI, minJ, d1, d2, l1, l2, node1, node2, node3;
function setUpNode(labelIndex, distance) {
let node;
if (labelIndex < that.N) {
node = new Branch({ id: that.labels[labelIndex], length: distance });
that.labelToNode[labelIndex] = node;
} else {
node = that.labelToNode[labelIndex];
node.setLength(distance);
}
return node;
}
that.rowSums = sumRows(that.D);
for (let i = 0; i < that.cN; i++) {
if (that.rowSums[i] > that.rowSumMax) that.rowSumMax = that.rowSums[i];
}
while (that.cN > 2) {
//if (that.cN % 100 == 0 ) console.log(that.cN);
({ minI, minJ } = search(that));
d1 =
0.5 * that.D[minI][minJ] +
(that.rowSums[minI] - that.rowSums[minJ]) / (2 * that.cN - 4);
d2 = that.D[minI][minJ] - d1;
l1 = that.currIndexToLabel[minI];
l2 = that.currIndexToLabel[minJ];
node1 = setUpNode(l1, d1);
node2 = setUpNode(l2, d2);
node3 = new Branch({ children: [node1, node2] });
recalculateDistanceMatrix(that, minI, minJ);
let sorted = sortWithIndices(that.D[minJ], minJ, true);
that.S[minJ] = sorted;
that.I[minJ] = sorted.sortIndices;
that.S[minI] = that.I[minI] = [];
that.cN--;
that.labelToNode[that.nextIndex] = node3;
that.currIndexToLabel[minI] = -1;
that.currIndexToLabel[minJ] = that.nextIndex++;
}
let left = that.indicesLeft.values();
minI = left.next().value;
minJ = left.next().value;
l1 = that.currIndexToLabel[minI];
l2 = that.currIndexToLabel[minJ];
d1 = d2 = that.D[minI][minJ] / 2;
node1 = setUpNode(l1, d1);
node2 = setUpNode(l2, d2);
let tree = new Branch({ children: [node1, node2] });
tree.fixParenthood();
return tree.fixDistances();
}
function search(t) {
let qMin = Infinity,
D = t.D,
cN = t.cN,
n2 = cN - 2,
S = t.S,
I = t.I,
rowSums = t.rowSums,
removedColumns = t.removedIndices,
uMax = t.rowSumMax,
q,
minI = -1,
minJ = -1,
c2;
// initial guess for qMin
for (let r = 0; r < t.N; r++) {
if (removedColumns.has(r)) continue;
c2 = I[r][0];
if (removedColumns.has(c2)) continue;
q = D[r][c2] * n2 - rowSums[r] - rowSums[c2];
if (q < qMin) {
qMin = q;
minI = r;
minJ = c2;
}
}
for (let r = 0; r < t.N; r++) {
if (removedColumns.has(r)) continue;
for (let c = 0; c < S[r].length; c++) {
c2 = I[r][c];
if (removedColumns.has(c2)) continue;
if (S[r][c] * n2 - rowSums[r] - uMax > qMin) break;
q = D[r][c2] * n2 - rowSums[r] - rowSums[c2];
if (q < qMin) {
qMin = q;
minI = r;
minJ = c2;
}
}
}
return { minI, minJ };
}
function recalculateDistanceMatrix(t, joinedIndex1, joinedIndex2) {
let D = t.D,
n = D.length,
sum = 0,
aux,
aux2,
removedIndices = t.removedIndices,
rowSums = t.rowSums,
newRow = t.newRow,
rowChange = t.rowChange,
newMax = 0;
removedIndices.add(joinedIndex1);
for (let i = 0; i < n; i++) {
if (removedIndices.has(i)) continue;
aux = D[joinedIndex1][i] + D[joinedIndex2][i];
aux2 = D[joinedIndex1][joinedIndex2];
newRow[i] = 0.5 * (aux - aux2);
sum += newRow[i];
rowChange[i] = -0.5 * (aux + aux2);
}
for (let i = 0; i < n; i++) {
D[joinedIndex1][i] = -1;
D[i][joinedIndex1] = -1;
if (removedIndices.has(i)) continue;
D[joinedIndex2][i] = newRow[i];
D[i][joinedIndex2] = newRow[i];
rowSums[i] += rowChange[i];
if (rowSums[i] > newMax) newMax = rowSums[i];
}
rowSums[joinedIndex1] = 0;
rowSums[joinedIndex2] = sum;
if (sum > newMax) newMax = sum;
t.rowSumMax = newMax;
t.indicesLeft.delete(joinedIndex1);
}
function sumRows(a) {
let n = a.length,
sums = new Array(n);
for (let i = 0; i < n; i++) {
let sum = 0;
for (let j = 0; j < n; j++) {
let v = parseFloat(a[i][j]);
if (typeof v !== "number") continue;
sum += a[i][j];
}
sums[i] = sum;
}
return sums;
}
function sortWithIndices(toSort, skip) {
if (typeof skip === "undefined") skip = -1;
let n = toSort.length;
let indexCopy = new Array(n);
let valueCopy = new Array(n);
let i2 = 0;
for (let i = 0; i < n; i++) {
if (toSort[i] === -1 || i === skip) continue;
indexCopy[i2] = i;
valueCopy[i2++] = toSort[i];
}
indexCopy.length = i2;
valueCopy.length = i2;
indexCopy.sort((a, b) => toSort[a] - toSort[b]);
valueCopy.sortIndices = indexCopy;
for (let j = 0; j < i2; j++) {
valueCopy[j] = toSort[indexCopy[j]];
}
return valueCopy;
}
/**
* Parses a Newick String and returns a Branch object representing the root
* of the output Tree.
* This is adapted Jason Davies' [newick.js](https://github.com/jasondavies/newick.js/blob/master/src/newick.js),
* which is released for modification under [the MIT License](https://opensource.org/licenses/MIT).
* @param {string} newick A Newick String
* @return {Branch} A Branch representing the root of the output tree
*/
export function parseNewick(newick) {
let ancestors = [],
tree = new Branch(),
tokens = newick.split(/\s*(;|\(|\)|,|:)\s*/),
n = tokens.length;
for (let t = 0; t < n; t++) {
let token = tokens[t];
let c;
switch (token) {
case "(": // new Branchset
c = tree.addChild();
ancestors.push(tree);
tree = c;
break;
case ",": // another Branch
c = ancestors[ancestors.length - 1].addChild();
tree = c;
break;
case ")": // optional name next
tree = ancestors.pop();
break;
case ":": // optional length next
break;
default:
let x = tokens[t - 1];
if (x == ")" || x == "(" || x == ",") {
tree.id = token;
} else if (x == ":") {
tree.length = parseFloat(token);
}
}
}
return tree.fixDistances();
}