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/
1,888 lines (1,551 loc) • 159 kB
JavaScript
(function (global, factory) {
typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports, require('d3'), require('underscore'), require('lodash')) :
typeof define === 'function' && define.amd ? define(['exports', 'd3', 'underscore', 'lodash'], factory) :
(global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory(global.phylotree = global.phylotree || {}, global.d3, global._, global._$1));
}(this, (function (exports, d3, _, _$1) { 'use strict';
function _interopNamespace(e) {
if (e && e.__esModule) return e;
var n = Object.create(null);
if (e) {
Object.keys(e).forEach(function (k) {
if (k !== 'default') {
var d = Object.getOwnPropertyDescriptor(e, k);
Object.defineProperty(n, k, d.get ? d : {
enumerable: true,
get: function () {
return e[k];
}
});
}
});
}
n['default'] = e;
return Object.freeze(n);
}
var d3__namespace = /*#__PURE__*/_interopNamespace(d3);
var ___namespace = /*#__PURE__*/_interopNamespace(_);
var ___namespace$1 = /*#__PURE__*/_interopNamespace(_$1);
//import { parseString } from "xml2js";
var nexml_parser = function(xml_string, options) {
var trees;
parseString(xml_string, function(error, xml) {
trees = xml["nex:nexml"].trees[0].tree.map(function(nexml_tree) {
var node_list = nexml_tree.node.map(d => d.$),
node_hash = node_list.reduce(function(a, b) {
b.edges = [];
b.name = b.id;
a[b.id] = b;
return a;
}, {}),
roots = node_list.filter(d => d.root),
root_id = roots > 0 ? roots[0].id : node_list[0].id;
node_hash[root_id].name = "root";
nexml_tree.edge.map(d => d.$).forEach(function(edge) {
node_hash[edge.source].edges.push(edge);
});
function parseNexml(node, index) {
if (node.edges) {
var targets = ___namespace.pluck(node.edges, "target");
node.children = ___namespace.values(___namespace.pick(node_hash, targets));
node.children.forEach(function(child, i) {
child.attribute = node.edges[i].length || "";
});
node.children.forEach(parseNexml);
node.annotation = "";
}
}
parseNexml(node_hash[root_id]);
return node_hash[root_id];
});
});
return trees;
};
// These methods are part of the Phylotree object
function graftANode(graftAt, newChild, newParent, lengths) {
let nodes = this.nodes.descendants();
if (graftAt.parent) {
let nodeIndex = nodes.indexOf(graftAt);
if (nodeIndex >= 0) {
let parentIndex = graftAt.parent.children.indexOf(graftAt);
let newSplit = {
name: newParent,
parent: graftAt.parent,
attribute: lengths ? lengths[2] : null,
original_child_order: graftAt["original_child_order"]
},
newNode = {
name: newChild,
parent: newSplit,
attribute: lengths ? lengths[1] : null,
original_child_order: 2
};
newSplit["children"] = [graftAt, newNode];
graftAt["parent"].children[parentIndex] = newSplit;
graftAt.parent = newSplit;
graftAt["attribute"] = lengths ? lengths[0] : null;
graftAt["original_child_order"] = 1;
}
}
return this;
}
function addChild(parent, child) {
if(parent.children) {
parent.children.push(child);
} else {
parent["children"] = [child];
}
return parent;
}
function createNode(name, lengths) {
return {
data: {
name: name,
attribute: lengths ? lengths[1] : null
},
parent: '',
};
}
/**
* Delete a given node.
*
* @param {Node} The node to be deleted, or the index of such a node.
* @returns The current ``phylotree``.
*/
function deleteANode(index) {
let nodes = this.nodes.descendants();
if (typeof index != "number") {
return this.deleteANode(nodes.indexOf(index));
}
if (index > 0 && index < nodes.length) {
let node = nodes[index];
if (node.parent) {
// can only delete nodes that are not the root
let delete_me_idx = node.parent.children.indexOf(node);
if (delete_me_idx >= 0) {
nodes.splice(index, 1);
if (node.children) {
node.children.forEach(function(d) {
d["original_child_order"] = node.parent.children.length;
node.parent.children.push(d);
d.parent = node.parent;
});
}
if (node.parent.children.length > 2) {
node.parent.children.splice(delete_me_idx, 1);
} else {
if (node.parent.parent) {
node.parent.parent.children[
node.parent.parent.children.indexOf(node.parent)
] =
node.parent.children[1 - delete_me_idx];
node.parent.children[1 - delete_me_idx].parent = node.parent.parent;
nodes.splice(nodes.indexOf(node.parent), 1);
} else {
nodes.splice(0, 1);
nodes.parent = null;
delete nodes.data["attribute"];
delete nodes.data["annotation"];
delete nodes.data["original_child_order"];
nodes.name = "root";
nodes.data.name = "root";
}
}
}
}
}
return this;
}
/**
* Get the tips of the tree
* @returns {Array} Nodes in the current ``phylotree``.
*/
function getTips() {
// get all nodes that have no nodes
return ___namespace.filter(this.nodes.descendants(), n => {
return !___namespace.has(n, "children");
});
}
/**
* Get the internal nodes of the tree
* @returns {Array} Nodes in the current ``phylotree``.
*/
function getInternals() {
// get all nodes that have no nodes
return ___namespace.filter(this.nodes.descendants(), n => {
return ___namespace.has(n, "children");
});
}
/**
* Get the root node.
*
* @returns the current root node of the ``phylotree``.
*/
function getRootNode() {
return this.nodes;
}
/**
* Get an array of all nodes.
* @returns {Array} Nodes in the current ``phylotree``.
*/
function getNodes() {
return this.nodes;
}
/**
* Get a node by name.
*
* @param {String} name Name of the desired node.
* @returns {Node} Desired node.
*/
function getNodeByName(name) {
return ___namespace.filter(this.nodes.descendants(), d => {
return d.data.name == name;
})[0];
}
/**
* Add attributes to nodes. New attributes will be placed in the
* ``annotations`` key of any nodes that are matched.
*
* @param {Object} attributes An object whose keys are the names of nodes
* to modify, and whose values are the new attributes to add.
*/
function assignAttributes(attributes) {
//return nodes;
// add annotations to each matching node
___namespace.each(this.nodes.descendants(), function(d) {
if (d.data && (d.data.name in attributes)) {
d["annotations"] = attributes[d.data.name];
}
});
}
/**
* Determine if a given node is a leaf node.
*
* @param {Node} A node in a tree.
* @returns {Bool} Whether or not the node is a leaf node.
*/
function isLeafNode(node) {
return !___namespace.has(node, "children")
}
/**
* Update a given key name in each node.
*
* @param {String} old_key The old key name.
* @param {String} new_key The new key name.
* @returns The current ``this``.
*/
function updateKeyName(old_key, new_key) {
this.nodes.each(function(n) {
if (old_key in n) {
if (new_key) {
n[new_key] = n[old_key];
}
delete n[old_key];
}
});
return this;
}
function clearInternalNodes(respect) {
if (!respect) {
this.nodes.each(d => {
if (!isLeafNode(d)) {
// TODO: Move away from storing attribute data as root (BREAKS occasionally with d3>3)
d[this.selection_attribute_name] = false;
if(!d.data.traits) {
d.data.traits = {};
}
d.data.traits[this.selection_attribute_name] = d[this.selection_attribute_name];
}
});
}
}
/**
* Select all descendents of a given node, with options for selecting
* terminal/internal nodes.
*
* @param {Node} node The node whose descendents should be selected.
* @param {Boolean} terminal Whether to include terminal nodes.
* @param {Boolean} internal Whther to include internal nodes.
* @returns {Array} An array of selected nodes.
*/
function selectAllDescendants$1(node, terminal, internal) {
let selection = [];
function sel(d) {
if (isLeafNode(d)) {
if (terminal) {
if (d != node) selection.push(d);
}
} else {
if (internal) {
if (d != node) selection.push(d);
}
d.children.forEach(sel);
}
}
sel(node);
return selection;
}
var node_operations = /*#__PURE__*/Object.freeze({
__proto__: null,
graftANode: graftANode,
addChild: addChild,
createNode: createNode,
deleteANode: deleteANode,
getTips: getTips,
getInternals: getInternals,
getRootNode: getRootNode,
getNodes: getNodes,
getNodeByName: getNodeByName,
assignAttributes: assignAttributes,
isLeafNode: isLeafNode,
updateKeyName: updateKeyName,
clearInternalNodes: clearInternalNodes,
selectAllDescendants: selectAllDescendants$1
});
/**
* Parses a Newick string into an equivalent JSON representation that is
* suitable for consumption by ``hierarchy``.
*
* Optionally accepts bootstrap values. Currently supports Newick strings with or without branch lengths,
* as well as tagged trees such as
* ``(a,(b{TAG},(c{TAG},d{ANOTHERTAG})))``
*
* @param {String} nwk_str - A string representing a phylogenetic tree in Newick format.
* @param {Object} bootstrap_values.
* @returns {Object} An object with keys ``json`` and ``error``.
*/
function newickParser(nwk_str, options={}) {
const int_or_float = /^-?\d+(\.\d+)?$/;
let left_delimiter = options.left_delimiter || '{',
right_delimiter = options.right_delimiter || '}';
let clade_stack = [];
function addNewTreeLevel() {
let new_level = {
name: null
};
let the_parent = clade_stack[clade_stack.length - 1];
if (!("children" in the_parent)) {
the_parent["children"] = [];
}
clade_stack.push(new_level);
the_parent["children"].push(clade_stack[clade_stack.length - 1]);
clade_stack[clade_stack.length - 1]["original_child_order"] =
the_parent["children"].length;
}
function finishNodeDefinition() {
let this_node = clade_stack.pop();
this_node["name"] = current_node_name;
if ("children" in this_node) {
this_node["bootstrap_values"] = current_node_name;
} else {
this_node["name"] = current_node_name;
}
this_node["attribute"] = current_node_attribute;
if(left_delimiter == "[" && current_node_annotation.includes("&&NHX")) {
current_node_annotation
.split(':')
.slice(1)
.forEach(annotation => {
const [key, value] = annotation.split('=');
this_node[key] = int_or_float.test(value) ? +value : value;
});
} else {
this_node["annotation"] = current_node_annotation;
}
current_node_name = "";
current_node_attribute = "";
current_node_annotation = "";
}
function generateError(location) {
return {
json: null,
error:
"Unexpected '" +
nwk_str[location] +
"' in '" +
nwk_str.substring(location - 20, location + 1) +
"[ERROR HERE]" +
nwk_str.substring(location + 1, location + 20) +
"'"
};
}
let automaton_state = 0;
let current_node_name = "";
let current_node_attribute = "";
let current_node_annotation = "";
let quote_delimiter = null;
let name_quotes = {
"'": 1,
'"': 1
};
let tree_json = {
name: "root"
};
clade_stack.push(tree_json);
var space = /\s/;
for (var char_index = 0; char_index < nwk_str.length; char_index++) {
try {
var current_char = nwk_str[char_index];
switch (automaton_state) {
case 0: {
// look for the first opening parenthesis
if (current_char == "(") {
addNewTreeLevel();
automaton_state = 1; // expecting node name
}
break;
}
case 1: // name
case 3: {
// branch length
// reading name
if (current_char == ":") {
automaton_state = 3;
} else if (current_char == "," || current_char == ")") {
try {
finishNodeDefinition();
automaton_state = 1;
if (current_char == ",") {
addNewTreeLevel();
}
} catch (e) {
return generateError(char_index);
}
} else if (current_char == "(") {
if (current_node_name.length > 0) {
return generateError(char_index);
} else {
addNewTreeLevel();
}
} else if (current_char in name_quotes) {
if (
automaton_state == 1 &&
current_node_name.length === 0 &&
current_node_attribute.length === 0 &&
current_node_annotation.length === 0
) {
automaton_state = 2;
quote_delimiter = current_char;
continue;
}
return generateError(char_index);
} else {
if (current_char == left_delimiter) {
if (current_node_annotation.length) {
return generateError(char_index);
} else {
automaton_state = 4;
}
} else {
if (automaton_state == 3) {
current_node_attribute += current_char;
} else {
if (space.test(current_char)) {
continue;
}
if (current_char == ";") {
// semicolon terminates tree definition
char_index = nwk_str.length;
break;
}
current_node_name += current_char;
}
}
}
break;
}
case 2: {
// inside a quoted expression
if (current_char == quote_delimiter) {
if (char_index < nwk_str.length - 1) {
if (nwk_str[char_index + 1] == quote_delimiter) {
char_index++;
current_node_name += quote_delimiter;
continue;
}
}
quote_delimiter = 0;
automaton_state = 1;
continue;
} else {
current_node_name += current_char;
}
break;
}
case 4: {
// inside a comment / attribute
if (current_char == right_delimiter) {
automaton_state = 3;
} else {
if (current_char == left_delimiter) {
return generateError(char_index);
}
current_node_annotation += current_char;
}
break;
}
}
} catch (e) {
return generateError(char_index);
}
}
if (clade_stack.length != 1) {
return generateError(nwk_str.length - 1);
}
return {
json: tree_json,
error: null
};
}
/**
* Return Newick string representation of a phylotree.
*
* @param {Function} annotator - Function to apply to each node, determining
* what label is written (optional).
* @param {Node} node - start at this node (default == root)
* @returns {String} newick - Phylogenetic tree serialized as a Newick string.
*/
function getNewick(annotator, root) {
let self = this;
if (!annotator) annotator = d => '';
function nodeDisplay(n) {
// Skip the node if it is hidden
if (n.notshown) return;
if (!isLeafNode(n)) {
element_array.push("(");
n.children.forEach(function(d, i) {
if (i) {
element_array.push(",");
}
nodeDisplay(d);
});
element_array.push(")");
}
if(n.data.name !== 'root') {
const node_label = n.data.name.replaceAll("'", "''");
// Surround the entire string with single quotes if it contains any
// non-alphanumeric characters.
if (/\W/.test(node_label)) {
element_array.push("'" + node_label + "'");
} else {
element_array.push(node_label);
}
}
element_array.push(annotator(n));
let bl = self.branch_length_accessor(n);
if (bl !== undefined) {
element_array.push(":" + bl);
}
}
let element_array = [];
annotator = annotator || "";
nodeDisplay(root || this.nodes);
return element_array.join("")+";";
}
function parseAnnotations (buf) {
let str = buf;
let index = str.toUpperCase().indexOf('BEGIN DATA;');
let data = str.slice(index);
if(data.length < 2) {
return '';
}
index = data.toUpperCase().indexOf('END;');
let data_str = data.slice(0, index);
// split on semicolon
data = ___namespace.map(data_str.split(';'), d => { return d.trim() } );
// get dimensions
let dimensions = ___namespace.filter(data, d => {return d.toUpperCase().startsWith('DIMENSION')});
dimensions = dimensions[0].split(' ');
dimensions = ___namespace.object(___namespace.map(___namespace.rest(dimensions), d => { return d.split('=') }));
// get formats
let format = ___namespace.filter(data, d => {return d.toUpperCase().startsWith('FORMAT')});
format = format[0].split(' ');
format = ___namespace.object(___namespace.map(___namespace.rest(format), d => { return d.split('=') }));
format.symbols = ___namespace.reject(format.symbols.split(""), d => d=='"');
// get character matrix
let matrix = ___namespace.filter(data, d => {return d.toUpperCase().startsWith('MATRIX')});
matrix = matrix[0].split('\n');
matrix = ___namespace.object(___namespace.map(___namespace.rest(matrix), d=> { return ___namespace.compact(d.split(' ')) }));
// create all possible states for matrix
matrix = ___namespace.mapObject(matrix, (v,k) => {
if(v == '?') {
return format.symbols
}
else {
return Array(v)
}
});
return { 'dimensions' : dimensions, 'format' : format, 'matrix' : matrix }
}
/**
* Loads annotations from a nexus-formatted buffer and annotates existing tree
* nodes appropriately.
*
* @param {Object} tree - Instatiated phylotree
* @param {String} NEXUS string
* @returns {Object} Annotations
*/
function loadAnnotations(tree, label, annotations) {
// if filename, then load from filesystem
___namespace.each(tree.getTips(), d => { d.data["test"] = annotations.matrix[d.data.name]; });
// decorate nodes with annotations
}
function loadTree(buf) {
// if filename, then load from filesystem
// Parse first tree from NEXUS file and send to newickParser
// Make all upper case
let str = buf;
// Get TREE block
let index = str.toUpperCase().indexOf('BEGIN TREES;');
let split = str.slice(index);
if(split.length < 2) {
return '';
}
index = split.toUpperCase().indexOf('END;');
let tree_str = split.slice(0, index);
// Filter lines that start with TREE
let trees = tree_str.split('\n');
trees = ___namespace.filter(trees, d => { return d.trim().toUpperCase().startsWith('TREE') });
// Identify start of newick string
return newickParser(trees[0]);
}
var nexus = /*#__PURE__*/Object.freeze({
__proto__: null,
parseAnnotations: parseAnnotations,
loadAnnotations: loadAnnotations,
'default': loadTree
});
// Changes XML to JSON
// Modified version from here: http://davidwalsh.name/convert-xml-json
function xmlToJson(xml) {
// Create the return object
var obj = {};
if (xml.nodeType == 1) { // element
// do attributes
if (xml.attributes.length > 0) {
obj["@attributes"] = {};
for (var j = 0; j < xml.attributes.length; j++) {
var attribute = xml.attributes.item(j);
obj["@attributes"][attribute.nodeName] = attribute.nodeValue;
}
}
} else if (xml.nodeType == 3) { // text
obj = xml.nodeValue;
}
// do children
// If just one text node inside
if (xml.hasChildNodes() && xml.childNodes.length === 1 && xml.childNodes[0].nodeType === 3) {
obj = xml.childNodes[0].nodeValue;
}
else if (xml.hasChildNodes()) {
for(var i = 0; i < xml.childNodes.length; i++) {
var item = xml.childNodes.item(i);
var nodeName = item.nodeName;
if (typeof(obj[nodeName]) == "undefined") {
obj[nodeName] = xmlToJson(item);
} else {
if (typeof(obj[nodeName].push) == "undefined") {
var old = obj[nodeName];
obj[nodeName] = [];
obj[nodeName].push(old);
}
obj[nodeName].push(xmlToJson(item));
}
}
}
return obj;
}
var phyloxml_parser = function(xml, options) {
function parsePhyloxml(node, index) {
if (node.clade) {
node.clade.forEach(parsePhyloxml);
node.children = node.clade;
delete node.clade;
}
node.annotation = 1;
node.attribute = "0.01";
if (node.branch_length) {
node.attribute = node.branch_length;
}
if (node.taxonomy) {
node.name = node.taxonomy.scientific_name;
}
node.annotation = "";
}
var tree_json;
xml = xmlToJson(xml);
tree_json = xml.phyloxml.phylogeny.clade;
tree_json.name = "root";
parsePhyloxml(tree_json);
return {
json: tree_json,
error: null
};
};
function beast_parser(newick, options) {
options.left_delimiter = '[';
options.right_delimiter = ']';
const parsed_newick = newickParser(newick, options);
function parseBeastNode(node) {
if(node.annotation) {
node.beast = {};
const tokens = node.annotation.split(/=|,|{|}/)
.filter(token => token);
for(var i = 0; i < tokens.length; i+=2) {
let key = tokens[i].replace(/&|%/g, '');
if(/[a-df-zA-DF-Z]+/.test(tokens[i+2])) {
node.beast[key] = +tokens[i+1];
} else {
node.beast[key] = [+tokens[i+1], +tokens[i+2]];
i++;
}
}
}
node.annotation = undefined;
if(node.children) {
node.children.forEach(parseBeastNode);
}
}
parseBeastNode(parsed_newick.json);
return parsed_newick;
}
/*
* A parser must have two fields, the object and
* options
*/
var format_registry = {
nexml: nexml_parser,
phyloxml: phyloxml_parser,
nexus : loadTree,
nwk: newickParser,
nhx: newickParser,
beast: beast_parser
};
/**
* Return CSV of nodes sorted by longest branches.
*
* @returns {Array} An array of all tips and associated lengths of the form :
* [{
* name : <tip_name>,
* length: <tip_length>
* }, ...]
*/
function getTipLengths() {
// Get nodes and branch lengths
let self = this;
let tips = self.getTips();
// Transform to name, attribute key-pair and sort by attribute length, descending
let toExport = ___namespace.map(tips, d => { return {'name' : d.data.name, 'length' : parseFloat(d.data.attribute) } });
toExport = ___namespace.sortBy(toExport, d=> -d.length);
return toExport;
}
function maxParsimony(respect_existing, attr_name) {
function populateMpMatrix(attr_name, d) {
d.mp = [
[0, 0], // score for parent selected / not selected
[false, false]
]; // selected or not
if (isLeafNode(d)) {
d.mp[1][0] = d.mp[1][1] = d[attr_name] || false;
d.mp[0][0] = d.mp[1][0] ? 1 : 0;
d.mp[0][1] = 1 - d.mp[0][0];
} else {
d.children.forEach(pop_mp_mat);
var s0 = d.children.reduce(function(p, n) {
return n.mp[0][0] + p;
}, 0);
// cumulative children score if this node is 0
var s1 = d.children.reduce(function(p, n) {
return n.mp[0][1] + p;
}, 0);
// cumulative children score if this node is 1
// parent = 0
if (d[attr_name]) {
// respect selected
d.mp[0][0] = s1 + 1;
d.mp[1][0] = true;
d.mp[0][1] = s1;
d.mp[1][1] = true;
} else {
if (s0 < s1 + 1) {
d.mp[0][0] = s0;
d.mp[1][0] = false;
} else {
d.mp[0][0] = s1 + 1;
d.mp[1][0] = true;
}
// parent = 1
if (s1 < s0 + 1) {
d.mp[0][1] = s1;
d.mp[1][1] = true;
} else {
d.mp[0][1] = s0 + 1;
d.mp[1][1] = false;
}
}
}
}
const pop_mp_mat = ___namespace.partial(populateMpMatrix, attr_name);
pop_mp_mat(this.nodes);
this.nodes.each(d => {
if (d.parent) {
d.mp = d.mp[1][d.parent.mp ? 1 : 0];
} else {
d.mp = d.mp[1][d.mp[0][0] < d.mp[0][1] ? 0 : 1];
}
});
this.display.modifySelection((d, callback) => {
if (isLeafNode(d.target)) {
return d.target[attr_name];
}
return d.target.mp;
});
}
function postOrder(node, callback, backtrack) {
let nodes = [node],
next = [],
children,
i,
n;
while ((node = nodes.pop())) {
if (!(backtrack && backtrack(node))) {
next.push(node), (children = node.children);
if (children)
for (i = 0, n = children.length; i < n; ++i) {
nodes.push(children[i]);
}
}
}
while ((node = next.pop())) {
callback(node);
}
return node;
}
function preOrder(node, callback, backtrack) {
let nodes = [node],
children,
i;
while ((node = nodes.pop())) {
if (!(backtrack && backtrack(node))) {
callback(node), (children = node.children);
if (children)
for (i = children.length - 1; i >= 0; --i) {
nodes.push(children[i]);
}
}
}
return node;
}
function inOrder(node, callback, backtrack) {
let current,
next = [node],
children,
i,
n;
do {
(current = next.reverse()), (next = []);
while ((node = current.pop())) {
if (!(backtrack && backtrack(node))) {
callback(node), (children = node.children);
if (children)
for (i = 0, n = children.length; i < n; ++i) {
next.push(children[i]);
}
}
}
} while (next.length);
return node;
}
/**
* Traverses a tree that represents left-child right-sibling
* @param {Object} tree -- the phylotree.js tree object
* @return {Object} An edge list that represents the original multi-way tree
*
*/
function leftChildRightSibling(root) {
let declareTrueParent = function(n) {
if(n.children) {
// left child is the child
n.children[0].data.multiway_parent = n;
// right child is the sibling
n.children[1].data.multiway_parent = n.parent;
}
};
// First decorate each node with its true parent node
postOrder(root, declareTrueParent);
// return edge list
let edge_list = ___namespace$1.map(root.descendants(), n => {
let source = n.data.multiway_parent;
let name = "unknown";
if(source) {
name = source.data.name;
}
// In order to get the true name of the infector/infectee, we must traverse
// the tree from the multiway_parents node.
return {"source" : n.data.multiway_parent, "target" : n, "name": name } });
// Construct edge list by new parent-child listing
return edge_list;
}
/**
* Returns T/F whether every branch in the tree has a branch length
*
* @returns {Object} true if every branch in the tree has a branch length
*/
function hasBranchLengths() {
let bl = this.branch_length;
if (bl) {
return ___namespace.every(this.nodes.descendants(), function(node) {
return !node.parent || !___namespace.isUndefined(bl(node));
});
}
return false;
}
/**
* Returns branch lengths
*
* @returns {Array} array of branch lengths
*/
function getBranchLengths() {
let bl = this.branch_length;
return ___namespace.map(this.nodes.descendants(), node => { return bl(node)});
}
function defBranchLengthAccessor(_node, new_length) {
let _node_data = _node.data;
if (
"attribute" in _node_data &&
_node_data["attribute"] &&
_node_data["attribute"].length
) {
if(new_length > 0) {
_node_data["attribute"] = String(new_length);
}
let bl = parseFloat(_node_data["attribute"]);
if (!isNaN(bl)) {
return Math.max(0, bl);
}
}
// Allow for empty branch length at root
if(_node_data.name == "root") {
return 0;
}
console.warn('Undefined branch length at ' + _node_data.name + '!');
return undefined;
}
/**
* Get or set branch length accessor.
*
* @param {Function} attr Empty if getting, or new branch length accessor if setting.
* @returns {Object} The branch length accessor if getting, or the current this if setting.
*/
function setBranchLength(attr) {
if (!arguments.length) return this.branch_length_accessor;
this.branch_length_accessor = attr ? attr : defBranchLengthAccessor;
return this;
}
/**
* Normalizes branch lengths
*/
function normalize(attr) {
let bl = this.branch_length;
let branch_lengths = ___namespace.map(this.nodes.descendants(), function(node) {
if(bl(node)) {
return bl(node);
} else {
return null;
}
});
const max_bl = ___namespace.max(branch_lengths);
const min_bl = ___namespace.min(branch_lengths);
let scaler = function (x) {
return (x - min_bl)/(max_bl - min_bl);
};
___namespace.each(this.nodes.descendants(), (node) => {
let len = bl(node);
if(len) {
bl(node, scaler(len));
}
});
return this;
}
/**
* Scales branch lengths
*
* @param {Function} function that scales the branches
*/
function scale(scale_by) {
let bl = this.branch_length;
___namespace.each(this.nodes.descendants(), (node) => {
let len = bl(node);
if(len) {
bl(node, scale_by(len));
}
});
return this;
}
/**
* Get or set branch name accessor.
*
* @param {Function} attr (Optional) If setting, a function that accesses a branch name
* from a node.
* @returns The ``nodeLabel`` accessor if getting, or the current ``this`` if setting.
*/
function branchName(attr) {
if (!arguments.length) return this.nodeLabel;
this.nodeLabel = attr;
return this;
}
/**
* Reroot the tree on the given node.
*
* @param {Node} node Node to reroot on.
* @param {fraction} if specified, partition the branch not into 0.5 : 0.5, but according to
the specified fraction
* @returns {Phylotree} The current ``phylotree``.
*/
function reroot(node, fraction) {
/** TODO add the option to root in the middle of a branch */
if(!(node instanceof d3__namespace.hierarchy)) {
throw new Error('node needs to be an instance of a d3.hierarchy node!');
}
let nodes = this.nodes.copy();
fraction = fraction !== undefined ? fraction : 0.5;
if (node.parent) {
var new_json = d3__namespace.hierarchy({
name: "new_root"
});
new_json.children = [node.copy()];
new_json.data.__mapped_bl = undefined;
nodes.each(n => {
n.data.__mapped_bl = this.branch_length_accessor(n);
});
this.setBranchLength(n => {
return n.data.__mapped_bl;
});
let remove_me = node,
current_node = node.parent,
stashed_bl = ___namespace.noop();
let apportioned_bl =
node.data.__mapped_bl === undefined ? undefined : node.data.__mapped_bl * fraction;
stashed_bl = current_node.data.__mapped_bl;
current_node.data.__mapped_bl =
node.data.__mapped_bl === undefined
? undefined
: node.data.__mapped_bl - apportioned_bl;
node.data.__mapped_bl = apportioned_bl;
var remove_idx;
if (current_node.parent) {
new_json.children.push(current_node);
while (current_node.parent) {
remove_idx = current_node.children.indexOf(remove_me);
if (current_node.parent.parent) {
current_node.children.splice(remove_idx, 1, current_node.parent);
} else {
current_node.children.splice(remove_idx, 1);
}
let t = current_node.parent.data.__mapped_bl;
if (t !== undefined) {
current_node.parent.data.__mapped_bl = stashed_bl;
stashed_bl = t;
}
remove_me = current_node;
current_node = current_node.parent;
}
remove_idx = current_node.children.indexOf(remove_me);
current_node.children.splice(remove_idx, 1);
} else {
remove_idx = current_node.children.indexOf(remove_me);
current_node.children.splice(remove_idx, 1);
stashed_bl = current_node.data.__mapped_bl;
remove_me = new_json;
}
// current_node is now old root, and remove_me is the root child we came up
// the tree through
if (current_node.children.length == 1) {
if (stashed_bl) {
current_node.children[0].data.__mapped_bl += stashed_bl;
}
remove_me.children = remove_me.children.concat(current_node.children);
} else {
let new_node = new d3__namespace.hierarchy({ name: "__reroot_top_clade", __mapped_bl: stashed_bl });
___namespace.extendOwn (new_json.children[0], node);
new_node.data.__mapped_bl = stashed_bl;
new_node.children = current_node.children.map(function(n) {
n.parent = new_node;
return n;
});
new_node.parent = remove_me;
remove_me.children.push(new_node);
}
}
// need to traverse the nodes and update parents
this.update(new_json);
this.traverse_and_compute(n => {
___namespace.each (n.children, (c) => {c.parent = n;});
}, "pre-order");
if(!___namespace.isUndefined(this.display)) {
// get options
let options = this.display.options;
// get container
d3__namespace.select(this.display.container).select('svg').remove();
// retain selection
let selectionName = this.display.selection_attribute_name;
delete this.display;
let rendered_tree = this.render(options);
rendered_tree.selectionLabel(selectionName);
rendered_tree.update();
d3__namespace.select(rendered_tree.container).node().appendChild(rendered_tree.show());
d3__namespace.select(this.display.container).dispatch('reroot');
}
return this;
}
function rootpath(attr_name, store_name) {
attr_name = attr_name || "attribute";
store_name = store_name || "y_scaled";
if ("parent" in this) {
let my_value = parseFloat(this[attr_name]);
this[store_name] =
this.parent[store_name] + (isNaN(my_value) ? 0.1 : my_value);
} else {
this[store_name] = 0.0;
}
return this[store_name];
}
function pathToRoot(node) {
let selection = [];
while (node) {
selection.push(node);
node = node.parent;
}
return selection;
}
var rooting = /*#__PURE__*/Object.freeze({
__proto__: null,
reroot: reroot,
rootpath: rootpath,
pathToRoot: pathToRoot
});
function xCoord(d) {
return d.y;
}
function yCoord(d) {
return d.x;
}
function radialMapper(r, a, radial_center) {
return {
x: radial_center + r * Math.sin(a),
y: radial_center + r * Math.cos(a)
};
}
function cartesianToPolar(
node,
radius,
radial_root_offset,
radial_center,
scales,
size
) {
node.radius = radius * (node.radius + radial_root_offset);
//if (!node.angle) {
node.angle = 2 * Math.PI * node.x * scales[0] / size[0];
//}
let radial = radialMapper(node.radius, node.angle, radial_center);
node.x = radial.x;
node.y = radial.y;
return node;
}
function drawArc(radial_center, points) {
var start = radialMapper(points[0].radius, points[0].angle, radial_center),
end = radialMapper(points[0].radius, points[1].angle, radial_center);
return (
"M " +
xCoord(start) +
"," +
yCoord(start) +
" A " +
points[0].radius +
"," +
points[0].radius +
" 0,0, " +
(points[1].angle > points[0].angle ? 1 : 0) +
" " +
xCoord(end) +
"," +
yCoord(end) +
" L " +
xCoord(points[1]) +
"," +
yCoord(points[1])
);
}
function arcSegmentPlacer(edge, where, radial_center) {
var r = radialMapper(
edge.target.radius + (edge.source.radius - edge.target.radius) * where,
edge.target.angle,
radial_center
);
return { x: xCoord(r), y: yCoord(r) };
}
var draw_line = d3__namespace
.line()
.x(function(d) {
return xCoord(d);
})
.y(function(d) {
return yCoord(d);
})
.curve(d3__namespace.curveStepBefore);
function lineSegmentPlacer(edge, where) {
return {
x:
xCoord(edge.target) +
(xCoord(edge.source) - xCoord(edge.target)) * where,
y: yCoord(edge.target)
};
}
function itemTagged(item) {
return item.tag || false;
}
function itemSelected(item, tag) {
return item[tag] || false;
}
const css_classes = {
"tree-container": "phylotree-container",
"tree-scale-bar": "tree-scale-bar",
node: "node",
"internal-node": "internal-node",
"tagged-node": "node-tagged",
"selected-node": "node-selected",
"collapsed-node": "node-collapsed",
"root-node": "root-node",
branch: "branch",
"selected-branch": "branch-selected",
"tagged-branch": "branch-tagged",
"tree-selection-brush": "tree-selection-brush",
"branch-tracer": "branch-tracer",
clade: "clade",
node_text: "phylotree-node-text"
};
function internalNames(attr) {
if (!arguments.length) return this.options["internal-names"];
this.options["internal-names"] = attr;
return this;
}
function radial(attr) {
if (!arguments.length) return this.options["is-radial"];
this.options["is-radial"] = attr;
return this;
}
function alignTips(attr) {
if (!arguments.length) return this.options["align-tips"];
this.options["align-tips"] = attr;
return this;
}
/**
* Return the bubble size of the current node.
*
* @param {Node} A node in the phylotree.
* @returns {Float} The size of the bubble associated to this node.
*/
function nodeBubbleSize(node) {
// if a custom bubble styler, use that instead
if(this.options["draw-size-bubbles"] && this.options["bubble-styler"]) {
return this.options["bubble-styler"](node);
} else {
return this.options["draw-size-bubbles"]
? this.relative_nodeSpan(node) * this.scales[0] * 0.25
: 0;
}
}
function shiftTip$1(d) {
if (this.options["is-radial"]) {
return [
(d.text_align == "end" ? -1 : 1) *
(this.radius_pad_for_bubbles - d.radius),
0
];
}
if (this.options["right-to-left"]) {
return [this.right_most_leaf - d.screen_x, 0];
}
return [this.right_most_leaf - d.screen_x, 0];
}
function layoutHandler(attr) {
if (!arguments.length) return this.layout_listener_handler;
this.layout_listener_handler = attr;
return this;
}
/**
* Getter/setter for the selection label. Useful when allowing
* users to make multiple selections.
*
* @param {String} attr (Optional) If setting, the new selection label.
* @returns The current selection label if getting, or the current ``phylotree`` if setting.
*/
function selectionLabel(attr) {
if (!arguments.length) return this.selection_attribute_name;
this.selection_attribute_name = attr;
this.syncEdgeLabels();
return this;
}
/**
* Get or set the current node span. If setting, the argument should
* be a function of a node which returns a number, so that node spans
* can be determined dynamically. Alternatively, the argument can be the
* string ``"equal"``, to give all nodes an equal span.
*
* @param {Function} attr Optional; if setting, the nodeSpan function.
* @returns The ``nodeSpan`` if getting, or the current ``phylotree`` if setting.
*/
function nodeSpan$1(attr) {
if (!arguments.length) return nodeSpan$1;
if (typeof attr == "string" && attr == "equal") {
nodeSpan$1 = function(d) { // eslint-disable-line
return 1;
};
} else {
nodeSpan$1 = attr; // eslint-disable-line
}
return this;
}
// List of all selecters that can be used with the restricted-selectable option
var predefined_selecters = {
all: d => {
return true;
},
none: d => {
return false;
},
"all-leaf-nodes": d => {
return isLeafNode(d.target);
},
"all-internal-nodes": d => {
return !isLeafNode(d.target);
}
};
/**
* Getter/setter for the selection callback. This function is called
* every time the current selection is modified, and its argument is
* an array of nodes that make up the current selection.
*
* @param {Function} callback (Optional) The selection callback function.
* @returns The current ``selectionCallback`` if getting, or the current ``this`` if setting.
*/
function selectionCallback$1(callback) {
if (!callback) return this.selectionCallback;
this.selectionCallback = callback;
return this;
}
var opt = /*#__PURE__*/Object.freeze({
__proto__: null,
css_classes: css_classes,
internalNames: internalNames,
radial: radial,
alignTips: alignTips,
nodeBubbleSize: nodeBubbleSize,
shiftTip: shiftTip$1,
layoutHandler: layoutHandler,
selectionLabel: selectionLabel,
get nodeSpan () { return nodeSpan$1; },
predefined_selecters: predefined_selecters,
selectionCallback: selectionCallback$1
});
function shiftTip(d) {
if (this.radial()) {
return [
(d.text_align == "end" ? -1 : 1) *
(this.radius_pad_for_bubbles - d.radius),
0
];
}
if (this.options["right-to-left"]) {
return [this.right_most_leaf - d.screen_x, 0];
}
return [this.right_most_leaf - d.screen_x, 0];
}
function drawNode(container, node, transitions) {
container = d3__namespace.select(container);
var is_leaf = isLeafNode(node);
if (is_leaf) {
container = container.attr("data-node-name", node.data.name);
}
var labels = container.selectAll("text").data([node]),
tracers = container.selectAll("line");
if (is_leaf || (this.showInternalName(node) && !isNodeCollapsed(node))) {
labels = labels
.enter()
.append("text")
.classed(this.css_classes["node_text"], true)
.merge(labels)
.on("click", d=> {
this.handle_node_click(node, d);
})
.attr("dy", d => {
return this.shown_font_size * 0.33;
})
.text(d => {
return this.options["show-labels"] ? this._nodeLabel(d) : "";
})
.style("font-size", d => {
return this.ensure_size_is_in_px(this.shown_font_size);
});
if (this.radial()) {
labels = labels
.attr("transform", d => {
return (
this.d3PhylotreeSvgRotate(d.text_angle) +
this.d3PhylotreeSvgTranslate(
this.alignTips() ? this.shiftTip(d) : null
)
);
})
.attr("text-anchor", d => {
return d.text_align;
});
} else {
labels = labels.attr("text-anchor", "start").attr("transform", d => {
if (this.options["layout"] == "right-to-left") {
return this.d3PhylotreeSvgTranslate([-20, 0]);
}
return this.d3PhylotreeSvgTranslate(
this.alignTips() ? this.shiftTip(d) : null
);
});
}
if (this.alignTips()) {
tracers = tracers.data([node]);
if (transitions) {
tracers = tracers
.enter()
.append("line")
.classed(this.css_classes["branch-tracer"], true)
.merge(tracers)
.attr("x1", d => {
return (
(d.text_align == "end" ? -1 : 1) * this.nodeBubbleSize(node)
);
})
.attr("x2", 0)
.attr("y1", 0)
.attr("y2", 0)
.attr("x2", d => {
if (this.options["layout"] == "right-to-left") {
return d.screen_x;
}
return this.shiftTip(d)[0];
})
.attr("transform", d => {
return this.d3PhylotreeSvgRotate(d.text_angle);
})
.attr("x2", d => {
if (this.options["layout"] == "right-to-left") {
return d.screen_x;
}
return this.shiftTip(d)[0];
})
.attr("transform", d => {
return this.d3PhylotreeSvgRotate(d.text_angle);
});
} else {
tracers = tracers
.enter()
.append("line")
.classed(this.css_classes["branch-tracer"], true)
.merge(tracers)
.attr("x1", d => {
return (
(d.text_align == "end" ? -1 : 1) * this.nodeBubbleSize(node)
);
})
.attr("y2", 0)
.attr("y1", 0)
.attr("x2", d => {
return this.shiftTip(d)[0];
});
tracers.attr("transform", d => {
return this.d3PhylotreeSvgRotate(d.text_angle);
});
}
} else {
tracers.remove();
}
if (this.options["draw-size-bubbles"]) {
var shift = this.nodeBubbleSize(node);
let circles = container
.selectAll("circle")
.data([shift])
.enter()
.append("circle");
circles.attr("r", function(d) {
return d;
});
if (this.shown_font_size >= 5) {
labels = labels.attr("dx", d => {
return (
(d.text_align == "end" ? -1 : 1) *
((this.alignTips() ? 0 : shift) + this.shown_font_size * 0.33)
);
});
}
} else {
if (this.shown_font_size >= 5) {
labels = labels.attr("dx", d => { // eslint-disable-line
return (d.text_align == "end" ? -1 : 1) * this.shown_font_size * 0.33;
});
}
}
}
if (!is_leaf) {
let circles = container
.selectAll("circle")
.data([node])
.enter()
.append("circle"),
radius = this.node_circle_size()(node);
if (radius > 0) {
circles
.merge(circles)
.attr("r", d => {
return Math.min(this.shown_font_size * 0.75, radius);
})
.on("click", d => {
this.handle_node_click(node, d);
});
} else {
circles.remove();
}
}
if (this.node_styler) {
this.node_styler(container, node);
}
return node;
}
function updateHasHiddenNodes() {
let nodes = this.phylotree.nodes.descendants();
for (let k = nodes.length - 1; k >= 0; k -= 1) {
if (isLeafNode(nodes[k])) {
nodes[k].hasHiddenNodes = nodes[k].notshown;
} else {
nodes[k].hasHiddenNodes = nodes[k].children.reduce(function(p, c) {
return c.notshown || p;
}, false);
}
}
return this;
}
function showInternalName(node) {
const i_names = this.internalNames();
if (i_names) {
if (typeof i_names === "function") {
return i_names(node);
}
return i_names;
}
return false;
}
/**
* Get or set the current node span. If setting, the argument should
* be a function of a node which returns a number, so that node spans
* can be determined dynamically. Alternatively, the argument can be the
* string ``"equal"``, to give all nodes an equal span.
*
* @param {Function} attr Optional; if setting, the nodeSpan function.
* @returns The ``nodeSpan`` if getting, or