phylogician-ts
Version:
Module to read, manipulate and write phylogenetic trees. Written in TypeScript
420 lines • 15.4 kB
JavaScript
"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