UNPKG

@lichong/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,663 lines (1,382 loc) 49.7 kB
import * as d3 from "d3"; import * as _ from "underscore"; import { drawArc, cartesianToPolar, arcSegmentPlacer } from "./radial"; import { default as draw_line, lineSegmentPlacer } from "./cartesian"; import { isLeafNode } from "../nodes"; import { xCoord, yCoord } from "./coordinates"; import * as clades from "./clades"; import * as render_nodes from "./nodes"; import * as render_edges from "./edges"; import * as events from "./events"; import { generatePhylotreeInstanceId } from "./events"; import { css_classes } from "./options"; import * as opt from "./options"; import * as menus from "./menus"; // replacement for d3.functor function constant(x) { return function() { return x; }; } class TreeRender { constructor(phylotree, options = {}) { this.css_classes = css_classes; this.phylotree = phylotree; this.container = options.container; // 为每个进化树实例生成唯一ID this.instanceId = generatePhylotreeInstanceId(); this.separation = function(_node, _previous) { return 0; }; this._nodeLabel = this.defNodeLabel; this.svg = null; this.selectionCallback = null; this.scales = [1, 1]; this.size = [1, 1]; this.fixed_width = [14, 30]; this.scale_bar_font_size = 12; this.draw_branch = draw_line; this.draw_scale_bar = null; this.edge_placer = lineSegmentPlacer; this.count_listener_handler = function() {}; this.layout_listener_handler = function() {}; this.node_styler = undefined; this.edge_styler = undefined; this.selection_attribute_name = "selected"; this.right_most_leaf = 0; this.label_width = 0; this.radial_center = 0; this.radius = 1; this.radius_pad_for_bubbles = 0; this.rescale_nodeSpan = 1; this.relative_nodeSpan = function(_node) { return this.nodeSpan(_node) / this.rescale_nodeSpan; }; let default_options = { layout: "left-to-right", logger: console, branches: "step", scaling: true, bootstrap: false, "color-fill": true, "font-size": 14, "internal-names": false, selectable: true, // restricted-selectable can take an array of predetermined // selecters that are defined in phylotree.predefined_selecters // only the defined functions will be allowed when selecting // branches "restricted-selectable": false, collapsible: true, "left-right-spacing": "fixed-step", //'fit-to-size', "top-bottom-spacing": "fixed-step", "left-offset": 0, "show-scale": "top", // currently not implemented to support any other positioning "draw-size-bubbles": false, "bubble-styler": this.radius_pad_for_bubbles, "binary-selectable": false, "is-radial": false, "attribute-list": [], "max-radius": 768, "annular-limit": 0.38196601125010515, compression: 0.2, "align-tips": false, "maximum-per-node-spacing": 100, "minimum-per-node-spacing": 2, "maximum-per-level-spacing": 100, "minimum-per-level-spacing": 10, node_circle_size: constant(3), transitions: null, brush: true, reroot: true, hide: true, "label-nodes-with-name": false, zoom: false, "show-menu": true, "show-labels": true, "node-styler": null, "edge-styler": null, "node-span": null, // 新增参数:控制拖拽功能是否启用 enableDrag: true, // 新增参数:控制缩放功能是否启用 enableZoom: true, // 新增参数:控制主分支是否加粗 "bold-main-branch": false, // 新增参数:控制是否严格遵守左下到右上的渲染顺序 "strict-bottom-left-to-top-right": false }; this.ensure_size_is_in_px = function(value) { return typeof value === "number" ? value + "px" : value; }; this.options = _.defaults(options, default_options); this.font_size = this.options["font-size"]; this.offsets = [0, this.font_size / 2]; this.shown_font_size = this.font_size; this.width = this.options.width || 800; this.height = this.options.height || 600; this.node_styler = this.options['node-styler']; this.edge_styler = this.options['edge-styler']; this.nodeSpan = this.options['node-span']; if(!this.nodeSpan) { this.nodeSpan = function(_node) { return 1; }; } this.rescale_nodeSpan = this.phylotree.nodes.children .map(d => { if (isLeafNode(d) || this.showInternalName(d)) return this.nodeSpan(d); }) .reduce(function(p, c) { return Math.min(c, p || 1e200); }, null) || 1; this.initialize_svg(this.container); this.links = this.phylotree.nodes.links(); this.initializeEdgeLabels(); // 初始化主分支标识 this.mainBranchNodes = new Set(); if (this.options["bold-main-branch"]) { this.identifyMainBranch(); } this.update(); // 为当前实例添加事件监听器 events.d3PhylotreeAddEventListener(this.instanceId); } /** * 销毁进化树实例,清理事件监听器 * 防止内存泄漏,特别是在单页应用中动态创建和销毁进化树时 */ destroy() { // 移除事件监听器 events.d3PhylotreeRemoveEventListener(this.instanceId); // 清理SVG元素 if (this.svg) { this.svg.remove(); this.svg = null; } // 清理其他引用 this.phylotree = null; this.container = null; this.links = null; } /** * 识别进化树的主分支 * 主分支判断逻辑:第一分支节点最多的为主分支,节点数相同时选择进化距离最长的 */ identifyMainBranch() { this.mainBranchNodes.clear(); if (!this.phylotree.nodes || !this.phylotree.nodes.children) { return; } // 从根节点开始识别主分支 let currentNode = this.phylotree.nodes; while (currentNode && currentNode.children && currentNode.children.length > 0) { // 将当前节点标记为主分支 this.mainBranchNodes.add(currentNode); let maxNodeCount = 0; let maxDistance = 0; let mainChild = null; // 遍历所有子节点,找到节点数最多的分支 for (let i = 0; i < currentNode.children.length; i++) { let child = currentNode.children[i]; let nodeCount = this.countDescendants(child); let branchDistance = this.calculateBranchDistance(child); // 如果节点数更多,或者节点数相同但距离更长,则选择这个分支 if (nodeCount > maxNodeCount || (nodeCount === maxNodeCount && branchDistance > maxDistance)) { maxNodeCount = nodeCount; maxDistance = branchDistance; mainChild = child; } } currentNode = mainChild; } // 将最后一个节点也标记为主分支(如果存在) if (currentNode) { this.mainBranchNodes.add(currentNode); } } /** * 检查节点是否属于主分支 * @param {Object} node - 要检查的节点 * @returns {boolean} 是否属于主分支 */ isMainBranchNode(node) { return this.mainBranchNodes.has(node); } /** * 获取边的线条粗细 * @param {Object} edge - 边对象 * @returns {Number} 返回线条粗细的像素值 */ getEdgeStrokeWidth(edge) { // 默认线条粗细 let defaultWidth = 1; // 检查是否为主分支边 if (this.options["bold-main-branch"] && this.isMainBranchNode && this.isMainBranchNode(edge.target)) { let boldValue = this.options["bold-main-branch"]; // 如果传入的是数字,则作为像素值使用 if (typeof boldValue === 'number') { return boldValue; } // 如果传入的是字符串数字,转换为数字 else if (typeof boldValue === 'string' && !isNaN(parseFloat(boldValue))) { return parseFloat(boldValue); } // 如果是布尔值true或其他值,使用默认的加粗粗细 else { return 3; } } return defaultWidth; } /** * 计算节点的后代数量(包括叶子节点和内部节点) * @param {Object} node - 要计算的节点 * @returns {number} 后代节点数量 */ countDescendants(node) { if (!node) return 0; if (isLeafNode(node)) { return 1; } let count = 1; // 包括当前节点 if (node.children) { for (let i = 0; i < node.children.length; i++) { count += this.countDescendants(node.children[i]); } } return count; } /** * 计算从当前节点到所有叶子节点的最大进化距离 * @param {Object} node - 要计算的节点 * @returns {number} 最大进化距离 */ calculateBranchDistance(node) { if (!node) return 0; if (isLeafNode(node)) { return this.phylotree.branch_length_accessor ? (this.phylotree.branch_length_accessor(node) || 0) : 0; } let maxDistance = 0; if (node.children) { for (let i = 0; i < node.children.length; i++) { let childDistance = this.calculateBranchDistance(node.children[i]); maxDistance = Math.max(maxDistance, childDistance); } } let currentBranchLength = this.phylotree.branch_length_accessor ? (this.phylotree.branch_length_accessor(node) || 0) : 0; return currentBranchLength + maxDistance; } pad_height() { if (this.draw_scale_bar) { return this.scale_bar_font_size + 25; } return 0; } pad_width() { // reset label_width this.label_width = this._label_width(this.shown_font_size); const _label_width = this.options["show-labels"] ? this.label_width : 0; return this.offsets[1] + this.options["left-offset"] + _label_width; } /** * Collapses a given node. * * @param {Node} node A node to be collapsed. */ collapse_node(n) { if (!render_nodes.isNodeCollapsed(n)) { n.collapsed = true; } } /** * Get or set the size of tree in pixels. * * @param {Array} attr (optional) An array of the form ``[height, width]``. * @returns {Phylotree} The current ``size`` array if getting, or the current ``phylotree`` * if setting. */ set_size(attr) { if (!arguments.length) { return this.size; } let phylo_attr = attr; if (this.options["top-bottom-spacing"] != "fixed-step") { this.size[0] = phylo_attr[0]; } if (this.options["left-right-spacing"] != "fixed-step") { this.size[1] = phylo_attr[1]; } return this; } /** * Getter/setter for the SVG element for the Phylotree to be rendered in. * * @param {d3-selection} svg_element (Optional) SVG element to render within, selected by D3. * @returns The selected SVG element if getting, or the current ``phylotree`` if setting.` */ initialize_svg(svg_element) { //if (!arguments.length) return this.svg; if (this.svg !== svg_element) { d3.select(svg_element) .select("svg") .remove(); this.svg = d3 .create("svg") .attr("width", this.width) .attr("height", this.height); this.set_size([this.height, this.width]); if (this.css_classes["tree-container"] == "phylotree-container") { this.svg.selectAll("*").remove(); this.svg.append("defs"); } d3.select(this.container).on( "click", d => { this.handle_node_click(null); }, true ); } return this; } update_layout(new_json, do_hierarchy) { if (do_hierarchy) { this.nodes = d3.hierarchy(new_json); this.nodes.each(function(d) { d.id = null; }); } this.update(); this.syncEdgeLabels(); } /** * Update the current phylotree, i.e., alter the svg * elements. * * @param {Boolean} transitions (Optional) Toggle whether transitions should be shown. * @returns The current ``phylotree``. */ update(transitions) { var self = this; //if (!this.svg) return this; this.placenodes(); transitions = this.transitions(transitions); let node_id = 0; let enclosure = this.svg .selectAll("." + css_classes["tree-container"]) .data([0]); enclosure = enclosure .enter() .append("g") .attr("class", css_classes["tree-container"]) .merge(enclosure) .attr("transform", d => { return this.d3PhylotreeSvgTranslate([ this.offsets[1] + this.options["left-offset"], this.pad_height() ]); }); if (this.draw_scale_bar) { let scale_bar = this.svg .selectAll("." + css_classes["tree-scale-bar"]) .data([0]); scale_bar .enter() .append("g") .attr("class", css_classes["tree-scale-bar"]) .style("font-size", this.ensure_size_is_in_px(this.scale_bar_font_size)) .merge(scale_bar) .attr("transform", d => { return this.d3PhylotreeSvgTranslate([ this.offsets[1] + this.options["left-offset"], this.pad_height() - 10 ]); }) .call(this.draw_scale_bar); scale_bar.selectAll("text").style("text-anchor", "end"); } else { this.svg.selectAll("." + css_classes["tree-scale-bar"]).remove(); } enclosure = this.svg .selectAll("." + css_classes["tree-container"]) .data([0]); this.updateCollapsedClades(transitions); let drawn_links = enclosure .selectAll(render_edges.edgeCssSelectors(css_classes)) .data(this.links.filter(render_edges.edgeVisible), d => { return d.target.id || (d.target.id = ++node_id); }); if (transitions) { drawn_links.exit().remove(); } else { drawn_links.exit().remove(); } drawn_links = drawn_links .enter() .insert("path", ":first-child") .merge(drawn_links) .each(function(d) { self.drawEdge(this, d, transitions); }); let drawn_nodes = enclosure .selectAll(render_nodes.nodeCssSelectors(css_classes)) .data( this.phylotree.nodes.descendants().filter(render_nodes.nodeVisible), d => { return d.id || (d.id = ++node_id); } ); drawn_nodes.exit().remove(); drawn_nodes = drawn_nodes .enter() .append("g") .attr("class", this.reclassNode) .merge(drawn_nodes) .attr("transform", d => { const should_shift = this.options["layout"] == "right-to-left" && isLeafNode(d); d.screen_x = xCoord(d); d.screen_y = yCoord(d); return this.d3PhylotreeSvgTranslate([ should_shift ? 0 : d.screen_x, d.screen_y ]); }) .each(function(d) { self.drawNode(this, d, transitions); }) .attr("transform", d => { if (!_.isUndefined(d.screen_x) && !_.isUndefined(d.screen_y)) { return "translate(" + d.screen_x + "," + d.screen_y + ")"; } }); if (this.options["label-nodes-with-name"]) { drawn_nodes = drawn_nodes.attr("id", d => { return "node-" + d.name; }); } this.resizeSvg(this.phylotree, this.svg, transitions); if (this.options["brush"]) { var brush = enclosure .selectAll("." + css_classes["tree-selection-brush"]) .data([0]) .enter() .insert("g", ":first-child") .attr("class", css_classes["tree-selection-brush"]); var brush_object = d3 .brush() .on("brush", (event, d) => { var extent = event.selection, shown_links = this.links.filter(render_edges.edgeVisible); var selected_links = shown_links .filter((d, i) => { return ( d.source.screen_x >= extent[0][0] && d.source.screen_x <= extent[1][0] && d.source.screen_y >= extent[0][1] && d.source.screen_y <= extent[1][1] && d.target.screen_x >= extent[0][0] && d.target.screen_x <= extent[1][0] && d.target.screen_y >= extent[0][1] && d.target.screen_y <= extent[1][1] ); }) .map(d => { return d.target; }); this.modifySelection( this.phylotree.links.map(d => { return d.target; }), "tag", false, selected_links.length > 0, "false" ); this.modifySelection(selected_links, "tag", false, false, "true"); }) .on("end", () => { //brush.call(d3.event.target.clear()); }); brush.call(brush_object); } this.syncEdgeLabels(); // 处理缩放和拖拽功能的参数配置 // 向后兼容性:如果没有指定enableZoom或enableDrag,使用原有的zoom参数作为默认值 let shouldEnableZoom = this.options["enableZoom"] !== undefined ? this.options["enableZoom"] : this.options["zoom"]; let shouldEnableDrag = this.options["enableDrag"] !== undefined ? this.options["enableDrag"] : this.options["zoom"]; // 如果启用了缩放或拖拽功能中的任意一个,则创建zoom_listener if (shouldEnableZoom || shouldEnableDrag) { // 创建缩放监听器并保存为实例属性,以便外部可以访问 this.zoom_listener = d3.zoom(); // 根据enableZoom参数控制缩放功能 if (shouldEnableZoom) { this.zoom_listener.scaleExtent([0.1, 10]); } else { // 禁用缩放功能:将缩放范围设置为固定值1 this.zoom_listener.scaleExtent([1, 1]); } // 根据enableDrag参数控制拖拽功能 if (!shouldEnableDrag) { // 禁用拖拽功能:过滤掉拖拽事件 this.zoom_listener.filter((event) => { // 只允许滚轮事件(用于缩放),禁止鼠标拖拽事件 return event.type === 'wheel'; }); } // 设置zoom事件处理函数 this.zoom_listener.on("zoom", (event) => { // 使用当前实例的SVG选择器,而不是全局选择器 this.svg.select("." + css_classes["tree-container"]).attr("transform", d => { let toTransform = event.transform; return toTransform; }); // Give some extra room this.svg.select("." + css_classes["tree-scale-bar"]).attr("transform", d => { let toTransform = event.transform; toTransform.y -= 10; return toTransform; }); }); this.svg.call(this.zoom_listener); } return this; } _handle_single_node_layout( a_node ) { let _nodeSpan = this.nodeSpan(a_node) / this.rescale_nodeSpan; // compute the relative size of nodes (0,1) // sum over all nodes is 1 this.x = a_node.x = this.x + this.separation(this.last_node, a_node) + (this.last_span + _nodeSpan) * 0.5; // separation is a user-settable callback to add additional spacing on nodes this._extents[1][1] = Math.max(this._extents[1][1], a_node.y); this._extents[1][0] = Math.min( this._extents[1][0], a_node.y - _nodeSpan * 0.5 ); if (this.is_under_collapsed_parent) { this._extents[0][1] = Math.max( this._extents[0][1], this.save_x + (a_node.x - this.save_x) * this.options["compression"] + this.save_span + (_nodeSpan * 0.5 + this.separation(this.last_node, a_node)) * this.options["compression"] ); } else { this._extents[0][1] = Math.max( this._extents[0][1], this.x + _nodeSpan * 0.5 + this.separation(this.last_node, a_node) ); } this.last_node = a_node; this.last_span = _nodeSpan; } tree_layout(a_node) { /** for each node: y: the y coordinate is root to tip (left to right in cladogram layout, radius is radial layout x : the x coordinate is top-most to bottom-most (top to bottom in cladogram layout, angle in radial layout) @return the x-coordinate of a_node or undefined in the node is not displayed (hidden or under a collapsed node) */ // do not layout hidden nodes if (render_nodes.nodeNotshown(a_node)) { return undefined; } let is_leaf = isLeafNode(a_node); // the next four members are radial layout options a_node.text_angle = null; // the angle at which text is being laid out a_node.text_align = null; // css alignment option for node labels a_node.radius = null; // radial layout radius a_node.angle = null; // radial layout angle (in radians) /** determine the root-to-tip location of this node; the root node receives the co-ordinate of 0 if the tree has branch lengths, then the placement of each node is simply the total branch length to the root if the tree has no branch lengths, all leaves get the same depth ("number of internal nodes on the deepest path") and all internal nodes get the depth in integer units of the # of internal nodes on the path to the root + 1 */ let undef_BL = false; /** _extents computes a bounding box for the tree (initially NOT in screen coordinates) all account for node sizes _extents [1][0] -- the minimum x coordinate (breadth) _extents [1][1] -- the maximum y coordinate (breadth) _extents [1][0] -- the minimum y coordinate (root-to-tip, or depthwise) _extents [1][1] -- the maximum y coordinate (root-to-tip, or depthwise) */ // last node laid out in the top bottom hierarchy if (a_node["parent"]) { if (this.do_scaling) { if (undef_BL) { return 0; } a_node.y = this.phylotree.branch_length_accessor(a_node); if (typeof a_node.y === "undefined") { undef_BL = true; return 0; } a_node.y += a_node.parent.y; } else { a_node.y = is_leaf ? this.max_depth : a_node.depth; } } else { this.x = 0.0; // the span of the last node laid out in the top to bottom hierarchy a_node.y = 0.0; this.last_node = null; this.last_span = 0.0; this._extents = [[0, 0], [0, 0]]; } /** the next block has to do with top-to-bottom spacing of nodes **/ if (is_leaf) { // displayed internal nodes are handled in `process_internal_node` this._handle_single_node_layout( a_node ); } if (!is_leaf) { // for internal nodes if ( render_nodes.isNodeCollapsed(a_node) && !this.is_under_collapsed_parent ) { // collapsed node this.save_x = this.x; this.save_span = this.last_span * 0.5; this.is_under_collapsed_parent = true; this.process_internal_node(a_node); this.is_under_collapsed_parent = false; if (typeof a_node.x === "number") { a_node.x = this.save_x + (a_node.x -this.save_x) * this.options["compression"] + this.save_span; a_node.collapsed = [[a_node.x, a_node.y]]; var map_me = n => { n.hidden = true; if (isLeafNode(n)) { this.x = n.x = this.save_x + (n.x - this.save_x) * this.options["compression"] + this.save_span; a_node.collapsed.push([n.x, n.y]); } else { n.children.map(map_me); } }; this.x = this.save_x; map_me(a_node); a_node.collapsed.splice(1, 0, [this.save_x, a_node.y]); a_node.collapsed.push([this.x, a_node.y]); a_node.collapsed.push([a_node.x, a_node.y]); a_node.hidden = false; } } else { // normal node, or under a collapsed parent this.process_internal_node(a_node); } } return a_node.x; } process_internal_node(a_node) { /** decide if the node will be shown, and compute its top-to-bottom (breadthwise) placement */ let count_undefined = 0; // 如果启用了严格左下到右上渲染,对子节点进行排序 if (this.options["strict-bottom-left-to-top-right"] && a_node.children) { this.sortChildrenForBottomLeftToTopRight(a_node); } if (this.showInternalName(a_node)) { // do in-order traversal to allow for proper internal node spacing // (x/2) >> 0 is integer division let half_way = (a_node.children.length / 2) >> 0; let displayed_children = 0; let managed_to_display = false; for (let child_id = 0; child_id < a_node.children.length; child_id++) { let child_x = this.tree_layout(a_node.children[child_id]);//.bind(this); if (typeof child_x == "number") { displayed_children++; } if (displayed_children >= half_way && !managed_to_display) { this._handle_single_node_layout(a_node); managed_to_display = true; } } if (displayed_children == 0) { a_node.notshown = true; a_node.x = undefined; } else { if (!managed_to_display) { this._handle_single_node_layout(a_node); } } } else { // postorder layout a_node.x = a_node.children .map(this.tree_layout.bind(this)) .reduce((a, b) => { if (typeof b == "number") return a + b; count_undefined += 1; return a; }, 0.0); if (count_undefined == a_node.children.length) { a_node.notshown = true; a_node.x = undefined; } else { a_node.x /= a_node.children.length - count_undefined; } } } /** * 对子节点进行排序以实现严格的左下到右上渲染 * 排序规则:按照子树的叶子节点数量和进化距离进行排序 * @param {Object} node - 要排序子节点的父节点 */ sortChildrenForBottomLeftToTopRight(node) { if (!node.children || node.children.length <= 1) { return; } // 为每个子节点计算排序权重 let childrenWithWeights = []; for (let i = 0; i < node.children.length; i++) { let child = node.children[i]; let leafCount = this.countLeafNodes(child); let maxDepth = this.getMaxDepthFromNode(child); let branchLength = this.phylotree.branch_length_accessor ? (this.phylotree.branch_length_accessor(child) || 0) : 0; // 计算排序权重:叶子节点数量 * 1000 + 最大深度 * 100 + 分支长度 // 这样可以确保叶子节点多的分支排在前面(左下),深度大的排在后面(右上) let weight = leafCount * 1000 + maxDepth * 100 + branchLength; childrenWithWeights.push({ node: child, weight: weight, leafCount: leafCount, maxDepth: maxDepth, branchLength: branchLength }); } // 按权重降序排序(权重大的在前,实现左下到右上) childrenWithWeights.sort((a, b) => { // 首先按叶子节点数量降序排序 if (a.leafCount !== b.leafCount) { return b.leafCount - a.leafCount; } // 如果叶子节点数量相同,按最大深度升序排序 if (a.maxDepth !== b.maxDepth) { return a.maxDepth - b.maxDepth; } // 如果深度也相同,按分支长度降序排序 return b.branchLength - a.branchLength; }); // 更新子节点数组 node.children = childrenWithWeights.map(item => item.node); } /** * 计算节点下的叶子节点数量 * @param {Object} node - 要计算的节点 * @returns {number} 叶子节点数量 */ countLeafNodes(node) { if (!node) return 0; if (isLeafNode(node)) { return 1; } let count = 0; if (node.children) { for (let i = 0; i < node.children.length; i++) { count += this.countLeafNodes(node.children[i]); } } return count; } /** * 获取从当前节点到叶子节点的最大深度 * @param {Object} node - 要计算的节点 * @returns {number} 最大深度 */ getMaxDepthFromNode(node) { if (!node) return 0; if (isLeafNode(node)) { return 1; } let maxDepth = 0; if (node.children) { for (let i = 0; i < node.children.length; i++) { let childDepth = this.getMaxDepthFromNode(node.children[i]); maxDepth = Math.max(maxDepth, childDepth); } } return maxDepth + 1; } do_lr(at_least_one_dimension_fixed) { if (this.radial() && at_least_one_dimension_fixed) { this.offsets[1] = 0; } if (this.options["left-right-spacing"] == "fixed-step") { this.size[1] = this.max_depth * this.fixed_width[1]; this.scales[1] = (this.size[1] - this.offsets[1] - this.options["left-offset"]) / this._extents[1][1]; this.label_width = this._label_width(this.shown_font_size); if (this.radial()) { this.label_width *= 2; } } else { this.label_width = this._label_width(this.shown_font_size); at_least_one_dimension_fixed = true; // 当使用fit-to-size模式时,使用全部容器宽度,因为g标签已经从x=0开始 let available_width = this.size[1]; if (available_width * 0.5 < this.label_width) { this.shown_font_size *= (available_width * 0.5) / this.label_width; this.label_width = available_width * 0.5; } // 使用全部容器宽度减去标签宽度进行缩放计算 this.scales[1] = (this.size[1] - this.label_width) / this._extents[1][1]; } } /** * Place the current nodes, i.e., determine their coordinates based * on current settings. * * @returns The current ``phylotree``. */ placenodes() { this._extents = [ [0, 0], [0, 0] ]; this.x = 0.0; this.last_span = 0.0; //let x = 0.0, // last_span = 0; this.last_node = null; this.last_span = 0.0; (this.save_x = this.x), (this.save_span = this.last_span * 0.5); this.do_scaling = this.options["scaling"]; let undef_BL = false; this.is_under_collapsed_parent = false; this.max_depth = 1; // Set initial x this.phylotree.nodes.x = this.tree_layout( this.phylotree.nodes, this.do_scaling ); this.max_depth = d3.max(this.phylotree.nodes.descendants(), n => { return n.depth; }); if (this.do_scaling && undef_BL) { // requested scaling, but some branches had no branch lengths // redo layout without branch lengths this.do_scaling = false; this.phylotree.nodes.x = this.tree_layout(this.phylotree.nodes); } let at_least_one_dimension_fixed = false; this.draw_scale_bar = this.options["show-scale"] && this.do_scaling; // this is a hack so that phylotree.pad_height would return ruler spacing this.offsets[1] = Math.max( this.font_size, -this._extents[1][0] * this.fixed_width[0] ); if (this.options["top-bottom-spacing"] == "fixed-step") { this.size[0] = this._extents[0][1] * this.fixed_width[0]; this.scales[0] = this.fixed_width[0]; } else { this.scales[0] = (this.size[0] - this.pad_height()) / this._extents[0][1]; at_least_one_dimension_fixed = true; } this.shown_font_size = Math.min(this.font_size, this.scales[0]); if (this.radial()) { // map the nodes to polar coordinates this.draw_branch = _.partial(drawArc, this.radial_center); this.edge_placer = arcSegmentPlacer; let last_child_angle = null, last_circ_position = null, last_child_radius = null, min_radius = 0, effective_span = this._extents[0][1] * this.scales[0]; let compute_distance = function(r1, r2, a1, a2, annular_shift) { annular_shift = annular_shift || 0; return Math.sqrt( (r2 - r1) * (r2 - r1) + 2 * (r1 + annular_shift) * (r2 + annular_shift) * (1 - Math.cos(a1 - a2)) ); }; let max_r = 0; this.phylotree.nodes.each(d => { let my_circ_position = d.x * this.scales[0]; d.angle = (2 * Math.PI * my_circ_position) / effective_span; d.text_angle = d.angle - Math.PI / 2; d.text_angle = d.text_angle > 0 && d.text_angle < Math.PI; d.text_align = d.text_angle ? "end" : "start"; d.text_angle = (d.text_angle ? 180 : 0) + (d.angle * 180) / Math.PI; }); this.do_lr(at_least_one_dimension_fixed); this.phylotree.nodes.each(d => { d.radius = (d.y * this.scales[1]) / this.size[1]; max_r = Math.max(d.radius, max_r); }); let annular_shift = 0; this.phylotree.nodes.each(d => { if (!d.children) { let my_circ_position = d.x * this.scales[0]; if (last_child_angle !== null) { let required_spacing = my_circ_position - last_circ_position, radial_dist = compute_distance( d.radius, last_child_radius, d.angle, last_child_angle, annular_shift ); let local_mr = radial_dist > 0 ? required_spacing / radial_dist : 10 * this.options["max-radius"]; if (local_mr > this.options["max-radius"]) { // adjust the annular shift let dd = required_spacing / this.options["max-radius"], b = d.radius + last_child_radius, c = d.radius * last_child_radius - (dd * dd - (last_child_radius - d.radius) * (last_child_radius - d.radius)) / 2 / (1 - Math.cos(last_child_angle - d.angle)), st = Math.sqrt(b * b - 4 * c); annular_shift = Math.min( this.options["annular-limit"] * max_r, (-b + st) / 2 ); min_radius = this.options["max-radius"]; } else { min_radius = Math.max(min_radius, local_mr); } } last_child_angle = d.angle; last_circ_position = my_circ_position; last_child_radius = d.radius; } }); this.radius = Math.min( this.options["max-radius"], Math.max(effective_span / 2 / Math.PI, min_radius) ); if (at_least_one_dimension_fixed) { this.radius = Math.min( this.radius, (Math.min(effective_span, this._extents[1][1] * this.scales[1]) - this.label_width) * 0.5 - this.radius * annular_shift ); } this.radial_center = this.radius_pad_for_bubbles = this.radius; this.draw_branch = _.partial(drawArc, this.radial_center); let scaler = 1; if (annular_shift) { scaler = max_r / (max_r + annular_shift); this.radius *= scaler; } this.phylotree.nodes.each(d => { cartesianToPolar( d, this.radius, annular_shift, this.radial_center, this.scales, this.size ); max_r = Math.max(max_r, d.radius); if (this.options["draw-size-bubbles"]) { this.radius_pad_for_bubbles = Math.max( this.radius_pad_for_bubbles, d.radius + this.nodeBubbleSize(d) ); } else { this.radius_pad_for_bubbles = Math.max( this.radius_pad_for_bubbles, d.radius ); } if (d.collapsed) { d.collapsed = d.collapsed.map(p => { let z = {}; z.x = p[0]; z.y = p[1]; z = cartesianToPolar( z, this.radius, annular_shift, this.radial_center, this.scales, this.size ); return [z.x, z.y]; }); let last_point = d.collapsed[1]; d.collapsed = d.collapsed.filter(function(p, i) { if (i < 3 || i > d.collapsed.length - 4) return true; if ( Math.sqrt( Math.pow(p[0] - last_point[0], 2) + Math.pow(p[1] - last_point[1], 2) ) > 3 ) { last_point = p; return true; } return false; }); } }); this.size[0] = this.radial_center + this.radius / scaler; this.size[1] = this.radial_center + this.radius / scaler; } else { this.do_lr(); this.draw_branch = draw_line; this.edge_placer = lineSegmentPlacer; this.right_most_leaf = 0; this.phylotree.nodes.each(d => { d.x *= this.scales[0]; d.y *= this.scales[1]; if (this.options["layout"] == "right-to-left") { d.y = this._extents[1][1] * this.scales[1] - d.y; } if (isLeafNode(d)) { this.right_most_leaf = Math.max( this.right_most_leaf, d.y + this.nodeBubbleSize(d) ); } if (d.collapsed) { d.collapsed.forEach(p => { p[0] *= this.scales[0]; p[1] *= this.scales[1]; }); let last_x = d.collapsed[1][0]; d.collapsed = d.collapsed.filter(function(p, i) { if (i < 3 || i > d.collapsed.length - 4) return true; if (p[0] - last_x > 3) { last_x = p[0]; return true; } return false; }); } }); } if (this.draw_scale_bar) { let domain_limit, range_limit; if (this.radial()) { range_limit = Math.min(this.radius / 5, 50); domain_limit = Math.pow( 10, Math.ceil( Math.log((this._extents[1][1] * range_limit) / this.radius) / Math.log(10) ) ); range_limit = domain_limit * (this.radius / this._extents[1][1]); if (range_limit < 30) { let stretch = Math.ceil(30 / range_limit); range_limit *= stretch; domain_limit *= stretch; } } else { domain_limit = this._extents[1][1]; range_limit = this.size[1] - this.offsets[1] - this.options["left-offset"] - this.shown_font_size; } let scale = d3 .scaleLinear() .domain([0, domain_limit]) .range([0, range_limit]), scaleTickFormatter = d3.format(".2f"); this.draw_scale_bar = d3 .axisTop() .scale(scale) .tickFormat(function(d) { if (d === 0) { return ""; } return scaleTickFormatter(d); }); if (this.radial()) { this.draw_scale_bar.tickValues([domain_limit]); } else { let round = function(x, n) { return n ? Math.round(x * (n = Math.pow(10, n))) / n : Math.round(x); }; let my_ticks = scale.ticks(); my_ticks = my_ticks.length > 1 ? my_ticks[1] : my_ticks[0]; this.draw_scale_bar.ticks( Math.min( 10, round( range_limit / (this.shown_font_size * scaleTickFormatter(my_ticks).length * 2), 0 ) ) ); } } else { this.draw_scale_bar = null; } return this; } /** * Get or set spacing in the x-direction. * * @param {Number} attr (Optional), the new spacing value if setting. * @param {Boolean} skip_render (Optional), whether or not a refresh should be performed. * @returns The current ``spacing_x`` value if getting, or the current ``phylotree`` if setting. */ spacing_x(attr, skip_render) { if (!arguments.length) return this.fixed_width[0]; if ( this.fixed_width[0] != attr && attr >= this.options["minimum-per-node-spacing"] && attr <= this.options["maximum-per-node-spacing"] ) { this.fixed_width[0] = attr; if (!skip_render) { this.placenodes(); } } return this; } /** * Get or set spacing in the y-direction. * * @param {Number} attr (Optional), the new spacing value if setting. * @param {Boolean} skip_render (Optional), whether or not a refresh should be performed. * @returns The current ``spacing_y`` value if getting, or the current ``phylotree`` if setting. */ spacing_y(attr, skip_render) { if (!arguments.length) return this.fixed_width[1]; if ( this.fixed_width[1] != attr && attr >= this.options["minimum-per-level-spacing"] && attr <= this.options["maximum-per-level-spacing"] ) { this.fixed_width[1] = attr; if (!skip_render) { this.placenodes(); } } return this; } _label_width(_font_size) { _font_size = _font_size || this.shown_font_size; let width = 0; this.phylotree.nodes .descendants() .filter(render_nodes.nodeVisible) .forEach(node => { let node_width = 12 + this._nodeLabel(node).length * _font_size * 0.8; if (node.angle !== null) { node_width *= Math.max( Math.abs(Math.cos(node.angle)), Math.abs(Math.sin(node.angle)) ); } width = Math.max(node_width, width); }); return width; } /** * Get or set font size. * * @param {Function} attr Empty if getting, or new font size if setting. * @returns The current ``font_size`` accessor if getting, or the current ``phylotree`` if setting. */ font_size(attr) { if (!arguments.length) return this.font_size; this.font_size = attr === undefined ? 12 : attr; return this; } scale_bar_font_size(attr) { if (!arguments.length) return this.scale_bar_font_size; this.scale_bar_font_size = attr === undefined ? 12 : attr; return this; } node_circle_size(attr, attr2) { if (!arguments.length) return this.options["node_circle_size"]; this.options["node_circle_size"] = constant(attr === undefined ? 3 : attr); return this; } css(opt) { if (arguments.length === 0) return this.css_classes; if (arguments.length > 2) { var arg = {}; arg[opt[0]] = opt[1]; return this.css(arg); } for (var key in css_classes) { if (key in opt && opt[key] != css_classes[key]) { css_classes[key] = opt[key]; } } return this; } transitions(arg) { if (arg !== undefined) { return arg; } if (this.options["transitions"] !== null) { return this.options["transitions"]; } return this.phylotree.nodes.descendants().length <= 300; } /** * Get or set CSS classes. * * @param {Object} opt Keys are the CSS class to toggle and values are * the parameters for that CSS class. * @param {Boolean} run_update (optional) Whether or not the tree should update. * @returns The current ``phylotree``. */ css_classes(opt, run_update) { if (!arguments.length) return this.css_classes; let do_update = false; for (var key in css_classes) { if (key in opt && opt[key] != this.css_classes[key]) { do_update = true; this.css_classes[key] = opt[key]; } } if (run_update && do_update) { this.layout(); } return this; } /** * Lay out the tree within the SVG. * * @param {Boolean} transitions Specify whether or not transitions should occur. * @returns The current ``phylotree``. */ layout(transitions) { if (this.svg) { this.svg.selectAll( "." + this.css_classes["tree-container"] + ",." + this.css_classes["tree-scale-bar"] + ",." + this.css_classes["tree-selection-brush"] ); //.remove(); events.d3PhylotreeTriggerLayout(this, this.instanceId); return this.update(); } events.d3PhylotreeTriggerLayout(this, this.instanceId); return this; } handle_node_click(node, event) { this.nodeDropdownMenu(node, this.container, this, this.options, event); } refresh() { if (this.svg) { // for re-entrancy let enclosure = this.svg.selectAll( "." + this.css_classes["tree-container"] ); let edges = enclosure .selectAll(render_edges.edgeCssSelectors(this.css_classes)) .attr("class", this.reclassEdge.bind(this)); if (this.edge_styler) { edges.each(d => { this.edge_styler(d3.select(this), d); }); } //let nodes = this.enclosure // .selectAll(inspector.nodeCssSelectors(this.css_classes)) // .attr("class", this.phylotree.reclassNode); //if (this.node_styler) { // nodes.each(function(d) { // this.node_styler(d3.select(this), d); // }); //} } return this; } countHandler(attr) { if (!arguments.length) return this.count_listener_handler; this.count_listener_handler = attr; return this; } /** * Get or set node styler. If setting, pass a function of two arguments, * ``element`` and ``data``. ``data`` exposes the underlying node so that * its attributes can be referenced. These can be used to apply styles to * ``element``, which will be a D3 selection corresponding to the SVG element * that makes up the current node. * ``transition`` is the third argument which indicates that there is an ongoing * d3 transition in progress * * @param {Function} attr - Optional; if setting, the node styler function to be set. * @returns The ``node_styler`` function if getting, or the current ``phylotree`` if setting. */ style_nodes(attr) { if (!arguments.length) return this.node_styler; this.node_styler = attr; return this; } /** * Get or set edge styler. If setting, pass a function of two arguments, * ``element`` and ``data``. ``data`` exposes the underlying edge so that * its attributes can be referenced. These can be used to apply styles to * ``element``, which will be a D3 selection corresponding to the SVG element * that makes up the current edge. * * Note that, in accordance with the D3 hierarchy layout, edges will have * a ``source`` and ``target`` field, corresponding to the nodes that make up * up the associated branch. * * @param {Function} attr - Optional; if setting, the node styler function to be set. * @returns The ``edge_styler`` function if getting, or the current ``phylotree`` if setting. */ style_edges(attr) { if (!arguments.length) return this.edge_styler; this.edge_styler = attr.bind(this); return this; } itemSelected(item, tag) { return item[tag] || false; } show() { return this.svg.node() } } _.extend(TreeRender.prototype, clades); _.extend(TreeRender.prototype, render_nodes); _.extend(TreeRender.prototype, render_edges); _.extend(TreeRender.prototype, events); _.extend(TreeRender.prototype, menus); _.extend(TreeRender.prototype, opt); export default TreeRender;