UNPKG

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
(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