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/

364 lines (302 loc) 9.08 kB
import * as d3 from "d3"; import { itemTagged, itemSelected } from "./helpers"; import { isLeafNode } from "../nodes"; import { css_classes } from "./options"; export 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]; } export function drawNode(container, node, transitions) { container = d3.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", (event, d) => { this.emit('nodeClick', node, event); this.handle_node_click(node, event); }) .on("mouseenter", (event) => { this.emit('nodeHover', node, event); }) .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", (event, d) => { this.emit('nodeClick', node, event); this.handle_node_click(node, event); }) .on("mouseenter", (event) => { this.emit('nodeHover', node, event); }); } else { circles.remove(); } } if (this.node_styler) { this.node_styler(container, node); } return node; } export 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; } export 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 the current ``phylotree`` if setting. */ export function nodeSpan(attr) { if (!arguments.length) return this.nodeSpan; if (typeof attr == "string" && attr == "equal") { this.nodeSpan = function(d) { return 1; }; } else { this.nodeSpan = attr; } return this; } export function reclassNode(node) { let class_var = css_classes[isLeafNode(node) ? "node" : "internal-node"]; if (itemTagged(node)) { class_var += " " + css_classes["tagged-node"]; } if (itemSelected(node, this.selection_attribute_name)) { class_var += " " + css_classes["selected-node"]; } if (!node["parent"]) { class_var += " " + css_classes["root-node"]; } if (isNodeCollapsed(node) || hasHiddenNodes(node)) { class_var += " " + css_classes["collapsed-node"]; } return class_var; } export function nodeVisible(node) { return !(node.hidden || node.notshown || false); } export function nodeNotshown(node) { return node.notshown; } export function hasHiddenNodes(node) { return node.hasHiddenNodes || false; } export function isNodeCollapsed(node) { return node.collapsed || false; } export function nodeCssSelectors(css_classes) { return [ css_classes["node"], css_classes["internal-node"], css_classes["collapsed-node"], css_classes["tagged-node"], css_classes["root-node"] ].reduce(function(p, c, i, a) { return (p += "g." + c + (i < a.length - 1 ? "," : "")); }, ""); } export function internalLabel(callback, respect_existing) { this.phylotree.clearInternalNodes(respect_existing); for (var i = this.phylotree.nodes.descendants().length - 1; i >= 0; i--) { var d = this.phylotree.nodes.descendants()[i]; if (!(isLeafNode(d) || itemSelected(d, this.selection_attribute_name))) { d[this.selection_attribute_name] = callback(d.children); } } this.modifySelection((d, callback) => { if (isLeafNode(d.target)) { return d.target[this.selection_attribute_name]; } return d.target[this.selection_attribute_name]; }); } export function defNodeLabel(_node) { _node = _node.data; if (isLeafNode(_node)) { return _node.name || ""; } if (this.showInternalName(_node)) { return _node.name; } return ""; } /** * Get or set nodeLabel 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. */ export function nodeLabel(attr) { if (!arguments.length) return this._nodeLabel; this._nodeLabel = attr ? attr : defNodeLabel; this.update(); return this; }