UNPKG

phylogician-ts

Version:

Module to read, manipulate and write phylogenetic trees. Written in TypeScript

420 lines 15.4 kB
"use strict"; var __importStar = (this && this.__importStar) || function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k in mod) if (Object.hasOwnProperty.call(mod, k)) result[k] = mod[k]; result["default"] = mod; return result; }; Object.defineProperty(exports, "__esModule", { value: true }); const d3 = __importStar(require("d3")); const Tree_1 = require("./Tree"); const CircularTreeLayout_1 = require("./CircularTreeLayout"); const VerticalTreeLayout_1 = require("./VerticalTreeLayout"); const loglevel_colored_prefix_1 = require("loglevel-colored-prefix"); const defaultOptions = { node: { hover: { fill: 'orange', opacity: 0.5, radiusMultiplier: 3, stroke: 'none', }, select: { fill: 'orange', opacity: 1, radiusMultiplier: 3, stroke: 'red', }, }, transitions: { duration: { branches: 1000, labels: 1000, nodes: 1000, }, }, }; class Phylogician { /** * Creates an instance of Phylogician. * @memberof Phylogician */ constructor(options = defaultOptions, loglevel = 'info') { this.nodeHoverIn = (nodeId) => { const node = this.tree.getNode(nodeId); d3.selectAll(`#phylogicianTS-node-${nodeId}`) .transition() .delay(0) .attr('r', () => { return node.getNodeSize() * this.options.node.hover.radiusMultiplier; }) .attr('fill', this.options.node.hover.fill) .attr('stroke', this.options.node.hover.stroke) .attr('opacity', 0.5); }; this.nodeHoverOut = (nodeId) => { const node = this.tree.getNode(nodeId); d3.select(`#phylogicianTS-node-${nodeId}`) .transition() .delay(500) .attr('r', node.getNodeSize()) .attr('fill', node.getNodeColor()) .attr('opacity', node.isLeaf() || node.isRoot() ? 1 : 1); }; this.options = options; this.logLevel = loglevel; this.tree = new Tree_1.Tree(loglevel); this.svgParams = { height: 500, margin: 20, width: 800, }; this.scale = true; this.layout = new VerticalTreeLayout_1.Vertical(this.tree, this.svgParams, this.logLevel); this.logger = new loglevel_colored_prefix_1.Logger(loglevel); this.nodeIndexOrder = []; this.tooltip = this.drawTooltip(); this.tooltipNodeId = 0; } /** * Builds a Tree from newick. * * @param {string} data * @returns {Phylogician} * @memberof Phylogician */ data(data) { const log = this.logger.getLogger('Phylogician::data'); log.debug('Building tree with newick data'); this.tree.buildTree(data); return this; } /** * Set positions of nodes and labels according to a layout type. * * @param {string} layoutType * @returns {Phylogician} * @memberof Phylogician */ setLayout(layoutType) { const log = this.logger.getLogger('Phylogician::setLayout'); log.debug(`Setting layout: ${layoutType}`); switch (layoutType) { case 'circular': { this.layout = new CircularTreeLayout_1.Circular(this.tree, this.svgParams, this.logLevel); this.layout.calcTree(this.scale); break; } default: { this.layout = new VerticalTreeLayout_1.Vertical(this.tree, this.svgParams, this.logLevel); this.layout.calcTree(this.scale); break; } } return this; } /** * Draws tree in browser * * @param {string} divId * @returns {HTMLDivElement} * @memberof Phylogician */ draw(divId) { const log = this.logger.getLogger('Phylogician::draw'); log.debug(`Drawing tree in ${divId}`); const svg = d3 .select(divId) .append('svg') .attr('width', this.svgParams.width) .attr('height', this.svgParams.height) .attr('id', 'phylogicianTS'); this.nodeIndexOrder = this.tree.nodes.map(node => node.id); const nodes = svg .selectAll('g') .data(this.nodeIndexOrder) .enter() .append('g') .attr('id', (d) => this.tree.getNode(d).id); nodes .append('path') .attr('class', 'branch') .attr('id', (id) => `phylogicianTS-branch-${id}`) .attr('d', (id) => this.tree.getNode(id).getBranchPath()) .attr('width', (id) => this.tree.getNode(id).getNodeSize()) .attr('stroke', (id) => this.tree.getNode(id).getBranchColor()) .attr('fill', 'none'); nodes .append('circle') .attr('class', 'node') .attr('id', (id) => `phylogicianTS-node-${id}`) .attr('r', (id) => this.tree.getNode(id).getNodeSize()) .attr('cx', (id) => this.tree.getNode(id).getPosX()) .attr('cy', (id) => this.tree.getNode(id).getPosY()) .attr('fill', (id) => this.tree.getNode(id).getNodeColor()) .attr('opacity', (id) => (this.tree.getNode(id).isLeaf() || this.tree.getNode(id).isRoot() ? 1 : 1)) .on('contextmenu', (id) => { d3.event.preventDefault(); this.tooltipNodeId = id; if (event) { const node = this.tree.getNode(id); log.info(`making tooltip for node ${node.name}`); const name = node.name || id; this.tooltip.select('#header').text(name); const pos = { x: d3.event.pageX, y: d3.event.pageY, }; log.debug(`click info: ${JSON.stringify(pos)}`); const tooltipElement = document.getElementById('phylogicianTS-tooltip'); const phylogician = document.getElementById('phylogicianTS'); const postAttrTooltip = tooltipElement.getBoundingClientRect(); const posPhylogician = phylogician.getBoundingClientRect(); log.debug(`phylogicianDiv: ${JSON.stringify(posPhylogician)}`); log.debug(`phylogicianTooltip: ${JSON.stringify(postAttrTooltip)}`); const clientHeight = window.innerHeight; const clientWidth = window.innerWidth; log.debug(`clientWindow height: ${clientHeight}`); log.debug(`clientWindow width: ${clientWidth}`); let offsetX = pos.x + 10; const maxX = Math.min(posPhylogician.left + posPhylogician.width, clientWidth); if (offsetX + postAttrTooltip.width > maxX) { offsetX -= postAttrTooltip.width; } let offsetY = pos.y + 10; const maxY = Math.min(posPhylogician.top + posPhylogician.height, clientHeight); if (offsetY + postAttrTooltip.height > maxY) { offsetY -= postAttrTooltip.height; } this.tooltip .style('top', `${offsetY}px`) .style('left', `${offsetX}px`) .style('visibility', 'visible'); } }) .on('mouseenter', this.nodeHoverIn) .on('mouseout', this.nodeHoverOut); const self = this; nodes.each(function (id) { const display = d3.select(this); const node = self.tree.getNode(id); const label = self.layout.renderNames(node, display); label .attr('id', `phylogicianTS-label-${id}`) .attr('transform', self.layout.labelPlace(id)) .attr('text-anchor', self.layout.labelAnchor(id)) .attr('x', node.getPosX()) .attr('y', node.getPosY() + self.calcLabelHeighOffSet(id)) .attr('display', node.getShowLabel() ? 'block' : 'none'); }); return this; } /** * Update Tree representation * * @returns {Phylogician} * @memberof Phylogician */ update() { this.layout.calcTree(this.scale); d3.selectAll('#phylogicianTS') .attr('width', this.svgParams.width) .attr('height', this.svgParams.height); d3.selectAll('.node') .data(this.nodeIndexOrder) .transition() .duration(this.options.transitions.duration.nodes) .attr('r', (id) => this.tree.getNode(id).getNodeSize()) .attr('cx', (id) => this.tree.getNode(id).getPosX()) .attr('cy', (id) => this.tree.getNode(id).getPosY()) .attr('fill', (id) => this.tree.getNode(id).getNodeColor()) .attr('opacity', (id) => (this.tree.getNode(id).isLeaf() || this.tree.getNode(id).isRoot() ? 1 : 1)); d3.selectAll('.branch') .data(this.nodeIndexOrder) .transition() .duration(this.options.transitions.duration.branches) .attr('stroke', (id) => this.tree.getNode(id).getBranchColor()) .attrTween('d', (id) => () => this.tree.getNode(id).getBranchPath()); const self = this; d3.selectAll('.label') .data(this.nodeIndexOrder) .each(function (id) { const label = d3.select(this); self.layout.updateNames(self.tree.getNode(id), label, self.options.transitions.duration.nodes); label .transition() .duration(self.options.transitions.duration.labels) .attr('id', `phylogicianTS-label-${id}`) .attr('transform', self.layout.labelPlace(id)) .attr('text-anchor', self.layout.labelAnchor(id)) .attr('x', self.tree.getNode(id).getPosX()) .attr('y', self.tree.getNode(id).getPosY() + self.calcLabelHeighOffSet(id)) .attr('display', self.tree.getNode(id).getShowLabel() ? 'block' : 'none'); }); return this; } /** * Set svg parameters and update tree". * * @param {ISvgParams} parameters * @returns * @memberof Phylogician */ setSvgParameters(parameters) { Object.assign(this.svgParams, parameters); this.layout.calcTree(this.scale); this.update(); return this; } /** * Scale or not the tree by branch lengths * * @returns {Phylogician} * @memberof Phylogician */ toggleScale() { const log = this.logger.getLogger('Phylogician::toggleScale'); log.debug(`toggleScale request`); this.scale = !this.scale; this.layout.calcTree(this.scale).calcBranchPath(); this.update(); return this; } /** * It sets the font size of all labels. It recalculates the tree and updates * * @param {string} fontSize * @returns * @memberof Phylogician */ setFontSizeAllLabels(fontSize) { this.tree.nodes.forEach(node => node.setDefaultFontSize(fontSize)); this.update(); return this; } /** * Changes to display or hide the labels of all nodes. * (not tested) * * @returns {Phylogician} * @memberof Phylogician */ showAllLabels(show) { const log = this.logger.getLogger('Phylogician::showLabels'); log.debug(`showLabels request`); this.tree.nodes.forEach(node => node.setShowLabel(show)); this.update(); return this; } /** * Changes to display or hide the labels of leafs only. * * @returns {Phylogician} * @memberof Phylogician */ showLeafLabels(show) { const log = this.logger.getLogger('Phylogician::showLeafLabels'); log.debug(`showLeafLabels request`); this.tree.nodes.forEach(node => (node.isLeaf() ? node.setShowLabel(show) : 0)); this.layout.calcTree(this.scale); this.update(); return this; } /** * Ladderize the tree or sub-tree. * * @param {number} nodeId * @returns * @memberof Phylogician */ ladderize(nodeId) { const log = this.logger.getLogger('Phylogician::ladderize'); log.debug('ladderize request'); this.tree.ladderize(nodeId); this.layout.calcTree(this.scale).calcTree(this.scale); this.update(); return this; } /** * Change the color of the node ids passed. * * @param {number[]} nodeIds * @param {string} color * @returns * @memberof Phylogician */ markNodes(nodeIds, color) { nodeIds.forEach(id => { const node = this.tree.getNode(id); node.setNodeColor(color); }); this.update(); return this; } calcLabelHeighOffSet(nodeId) { const node = this.tree.getNode(nodeId); return node.getLabelHeight() / 4; } /** * Draw tooltip () * * @returns {this} * @memberof Phylogician */ drawTooltip() { const log = this.logger.getLogger('Phylogician::drawTooltip'); log.debug(`Drawing tooltip`); const tooltip = d3 .select('body') .append('div') .attr('id', 'phylogicianTS-tooltip') .style('position', 'absolute') .style('z-index', '10') .style('visibility', 'hidden') .on('mouseover', () => { tooltip.transition().duration(0); }); const menu = tooltip .append('div') .style('display', 'flex') .style('background-color', 'black') .style('color', 'white') .style('opacity', 0.7) .style('padding', 10) .on('mouseover', () => { tooltip.transition().duration(0); tooltip.style('visibility', 'visible'); }) .on('mouseout', () => { tooltip .transition() .delay(500) .style('visibility', 'hidden'); }) .append('table') .attr('id', 'phylogicianTS-tooltip-menu'); menu .append('th') .attr('id', 'header') .attr('class', 'phylogicianTS-tooltip-menuEntry') .style('text-align', 'center') .style('color', 'white') .text('NodeName'); menu .append('tr') .attr('class', 'phylogicianTS-tooltip-item') .attr('id', 'phylogicianTS-tooltip-item-ladderize') .style('text-align', 'center') .style('color', 'white') .style('cursor', 'default') .text('Ladderize') .on('click', () => { log.debug(`ladderization clicked for node ${this.tooltipNodeId}`); this.ladderize(this.tooltipNodeId); }); return tooltip; } } exports.Phylogician = Phylogician; //# sourceMappingURL=Phylogician.js.map