phylotree
Version:
A JavaScript library for developing applications and interactive visualizations involving [phylogenetic trees](https://en.wikipedia.org/wiki/Phylogenetic_tree), written as an extension of the [D3](http://d3js.org) [hierarchy layout](https://github.com/d3/
385 lines (337 loc) • 10.5 kB
JavaScript
import * as d3 from "d3";
import * as _ from "underscore";
import { default as parser_registry } from "./formats/registry";
import { default as newickParser, getNewick, getTaggedNewick } from "./formats/newick";
import { default as getTipLengths } from "./export";
import * as nexus from "./formats/nexus";
import { default as phyloxml_parser } from "./formats/phyloxml";
import { default as maxParsimony } from "./max-parsimony";
import {
leftChildRightSibling,
postOrder,
preOrder,
default as inOrder,
} from "./traversal";
import {
default as hasBranchLengths,
getBranchLengths,
defBranchLengthAccessor,
setBranchLength,
branchName,
normalize,
scale,
} from "./branches";
import * as node_operations from "./nodes";
import * as rooting from "./rooting";
import { default as TreeRender } from "./render/draw";
function resortChildren(comparator, start_node, filter) {
// ascending
this.nodes
.sum(function (d) {
return d.value;
})
.sort(comparator);
// if a tree is rendered in the DOM
if (this.display) {
this.display.update_layout(this.nodes);
this.display.update();
}
return this;
}
/**
* Return most recent common ancestor of a pair of nodes.
* @returns An array of strings, comprising each tag that was read.
*/
function mrca(mrca_nodes) {
var mrca;
mrca_nodes = mrca_nodes.map(function (mrca_node) {
return typeof mrca_node == "string" ? mrca_node : mrca_node.data.name;
});
this.traverse_and_compute(function (node) {
if (!node.children) {
node.data.mrca = _.intersection([node.data.name], mrca_nodes);
} else if (!node.parent) {
if (!mrca) {
mrca = node;
}
} else {
node.data.mrca = _.union(
...node.descendants().map((child) => child.data.mrca)
);
if (!mrca && node.data.mrca.length == mrca_nodes.length) {
mrca = node;
}
}
});
return mrca;
}
/**
* An instance of a phylotree. Sets event listeners, parses tags, and creates links
* that represent branches.
*
* @param {Object} nwk - A Newick string, PhyloXML string, or hierarchical JSON representation of a phylogenetic tree.
* @param {Object} options
* - boostrap_values
* - type - format type
* @returns {Phylotree} phylotree - itself, following the builder pattern.
* @example
* // Create a simple phylotree from a Newick string
* const newick = "((A:0.1,B:0.2):0.05,C:0.3)";
* const tree = new Phylotree(newick);
*
* @example
* // Create a phylotree with options
* const tree = new Phylotree(newick, {
* bootstrap_values: true,
* type: "newick"
* });
*
* @example
* // Create from hierarchical JSON
* const jsonTree = {
* name: "root",
* children: [
* { name: "A", length: 0.1 },
* { name: "B", length: 0.2 }
* ]
* };
* const tree = new Phylotree(jsonTree);
*/
let Phylotree = class {
constructor(nwk, options = {}) {
this.newick_string = "";
this.nodes = [];
this.links = [];
this.parsed_tags = [];
this.partitions = [];
this.branch_length_accessor = defBranchLengthAccessor;
this.branch_length = defBranchLengthAccessor;
this.logger = options.logger || console;
this.selection_attribute_name = "selected";
// initialization
var type = options.type || undefined,
_node_data = [],
self = this;
// If the type is a string, check the parser_registry
if (_.isString(type)) {
if (type in parser_registry) {
_node_data = parser_registry[type](nwk, options);
} else {
// Hard failure
self.logger.error(
"type " +
type +
" not in registry! Available types are " +
_.keys(parser_registry)
);
}
} else if (_.isFunction(type)) {
// If the type is a function, try executing the function
try {
_node_data = type(nwk, options);
} catch (e) {
// Hard failure
self.logger.error("Could not parse custom format!");
}
} else {
// this builds children and links;
if (nwk.name == "root") {
// already parsed by phylotree.js
_node_data = { json: nwk, error: null };
} else if (typeof nwk != "string") {
// old default
_node_data = nwk;
} else if (nwk.contentType == "application/xml") {
// xml
_node_data = phyloxml_parser(nwk);
} else {
// newick string
this.newick_string = nwk;
_node_data = newickParser(nwk, options);
}
}
if (!_node_data["json"]) {
self.nodes = [];
} else {
self.nodes = d3.hierarchy(_node_data.json);
// Parse tags
let _parsed_tags = {};
self.nodes.each((node) => {
if (node.data.annotation) {
_parsed_tags[node.data.annotation] = true;
}
});
self.parsed_tags = Object.keys(_parsed_tags);
}
self.links = self.nodes.links();
// If no branch lengths are supplied, set all to 1
if (!this.hasBranchLengths()) {
console.warn(
"Phylotree User Warning : NO BRANCH LENGTHS DETECTED, SETTING ALL LENGTHS TO 1"
);
this.setBranchLength((x) => 1);
}
return self;
}
/*
Export the nodes of the tree with all local keys to JSON
The return will be an array of nodes in the specified traversal_type
('post-order' : default, 'pre-order', or 'in-order')
with parents and children referring to indices in that array
*/
json(traversal_type) {
var index = 0;
this.traverse_and_compute(function (n) {
n.json_export_index = index++;
}, traversal_type);
var node_array = new Array(index);
index = 0;
this.traverse_and_compute(function (n) {
let node_copy = _.clone(n);
delete node_copy.json_export_index;
if (n.parent) {
node_copy.parent = n.parent.json_export_index;
}
if (n.children) {
node_copy.children = _.map(n.children, function (c) {
return c.json_export_index;
});
}
node_array[index++] = node_copy;
}, traversal_type);
this.traverse_and_compute(function (n) {
delete n.json_export_index;
}, traversal_type);
return JSON.stringify(node_array);
}
/**
* Traverse the tree in a prescribed order, and compute a value at each node.
*
* @param {Function} callback A function to be called on each node.
* @param {String} traversal_type Either ``"pre-order"`` or ``"post-order"`` or ``"in-order"``.
* @param {Node} root_node start traversal here, if provided, otherwise start at root
* @param {Function} backtrack ; if provided, then at each node n, backtrack (n) will be called,
and if it returns TRUE, traversal will NOT continue past into this
node and its children
* @example
* // Count all nodes in the tree
* let nodeCount = 0;
* tree.traverse_and_compute(function(node) {
* nodeCount++;
* });
* console.log(`Tree has ${nodeCount} nodes`);
*
* @example
* // Find maximum depth in post-order traversal
* tree.traverse_and_compute(function(node) {
* if (!node.children) {
* node.depth = 0;
* } else {
* node.depth = Math.max(...node.children.map(c => c.depth)) + 1;
* }
* }, "post-order");
*
* @example
* // Add custom attributes to leaf nodes only
* tree.traverse_and_compute(function(node) {
* if (!node.children) {
* node.data.isLeaf = true;
* }
* });
*/
traverse_and_compute(callback, traversal_type, root_node, backtrack) {
traversal_type = traversal_type || "post-order";
function post_order(node) {
if (_.isUndefined(node)) {
return;
}
postOrder(node, callback, backtrack);
}
function pre_order(node) {
preOrder(node, callback, backtrack);
}
function in_order(node) {
inOrder(node, callback, backtrack);
}
if (traversal_type == "pre-order") {
traversal_type = pre_order;
} else {
if (traversal_type == "in-order") {
traversal_type = in_order;
} else {
traversal_type = post_order;
}
}
traversal_type(root_node ? root_node : this.nodes);
return this;
}
get_parsed_tags() {
return this.parsed_tags;
}
update(json) {
// update with new hiearchy layout
this.nodes = json;
}
/**
* Render the phylotree to a DOM element. Warning: Requires DOM!
*
* @param {Object} options - Rendering options including container, dimensions, and styling
* @returns {TreeRender} The display object for further customization
* @example
* // Basic rendering to a div element
* const tree = new Phylotree(newick);
* tree.render({
* container: "#tree-container",
* width: 800,
* height: 600
* });
*
* @example
* // Render with custom styling options
* const display = tree.render({
* container: d3.select("#my-tree"),
* width: 1000,
* height: 800,
* 'left-right-spacing': 'fit-to-size',
* 'top-bottom-spacing': 'fit-to-size',
* 'show-scale': true,
* selectable: true,
* collapsible: true
* });
*
* @example
* // Render radial tree layout
* tree.render({
* container: "#radial-tree",
* layout: "radial",
* width: 600,
* height: 600
* });
*/
render(options) {
this.display = new TreeRender(this, options);
return this.display;
}
};
Phylotree.prototype.isLeafNode = node_operations.isLeafNode;
Phylotree.prototype.selectAllDescendants = node_operations.selectAllDescendants;
Phylotree.prototype.mrca = mrca;
Phylotree.prototype.hasBranchLengths = hasBranchLengths;
Phylotree.prototype.getBranchLengths = getBranchLengths;
Phylotree.prototype.branchName = branchName;
Phylotree.prototype.normalizeBranchLengths = normalize;
Phylotree.prototype.scaleBranchLengths = scale;
Phylotree.prototype.getNewick = getNewick;
Phylotree.prototype.getTaggedNewick = getTaggedNewick;
Phylotree.prototype.resortChildren = resortChildren;
Phylotree.prototype.setBranchLength = setBranchLength;
Phylotree.prototype.maxParsimony = maxParsimony;
Phylotree.prototype.getTipLengths = getTipLengths;
Phylotree.prototype.leftChildRightSibling = leftChildRightSibling;
_.extend(Phylotree.prototype, node_operations);
_.extend(Phylotree.prototype, rooting);
_.extend(Phylotree.prototype, nexus);
export function itemTagged(item) {
return item.tag || false;
}
export default Phylotree;