|
/* |
|
d3.phylogram.js |
|
Wrapper around a d3-based phylogram (tree where branch lengths are scaled) |
|
Also includes a radial dendrogram visualization (branch lengths not scaled) |
|
along with some helper methods for building angled-branch trees. |
|
|
|
d3.phylogram.build(selector, nodes, options) |
|
Creates a phylogram. |
|
Arguments: |
|
selector: selector of an element that will contain the SVG |
|
nodes: JS object of nodes |
|
Options: |
|
width |
|
Width of the vis, will attempt to set a default based on the width of |
|
the container. |
|
height |
|
Height of the vis, will attempt to set a default based on the height |
|
of the container. |
|
vis |
|
Pre-constructed d3 vis. |
|
tree |
|
Pre-constructed d3 tree layout. |
|
children |
|
Function for retrieving an array of children given a node. Default is |
|
to assume each node has an attribute called "branchset" |
|
diagonal |
|
Function that creates the d attribute for an svg:path. Defaults to a |
|
right-angle diagonal. |
|
skipTicks |
|
Skip the tick rule. |
|
skipBranchLengthScaling |
|
Make a dendrogram instead of a phylogram. |
|
skipTreeNodeStyle |
|
Don't draw circles at the root and leaf nodes |
|
skipInteriorBranchLengths |
|
Don't draw the interior branch lengths |
|
skipLabels |
|
Don't add labels at leaf nodes |
|
strokeWidth |
|
Size for stroke that draws the tree (e.g. "1px") |
|
strokeColor |
|
Color for stroke (e.g. "#dedede") |
|
separation |
|
override the separation function of the tree layout |
|
urlMap |
|
hash of leaf name => url, will provide clickable names for any leaf who |
|
provides an entry |
|
|
|
d3.phylogram.buildRadial(selector, nodes, options) |
|
Creates a radial dendrogram. |
|
Options: same as build, but without diagonal, skipTicks, and |
|
skipBranchLengthScaling |
|
|
|
d3.phylogram.rightAngleDiagonal() |
|
Similar to d3.diagonal except it create an orthogonal crook instead of a |
|
smooth Bezier curve. |
|
|
|
d3.phylogram.radialRightAngleDiagonal() |
|
d3.phylogram.rightAngleDiagonal for radial layouts. |
|
*/ |
|
|
|
if (!d3) { throw "d3 wasn't included!"}; |
|
(function() { |
|
d3.phylogram = {} |
|
d3.phylogram.rightAngleDiagonal = function() { |
|
var projection = function(d) { return [d.y, d.x]; } |
|
|
|
var path = function(pathData) { |
|
return "M" + pathData[0] + ' ' + pathData[1] + " " + pathData[2]; |
|
} |
|
|
|
function diagonal(diagonalPath, i) { |
|
var source = diagonalPath.source, |
|
target = diagonalPath.target, |
|
midpointX = (source.x + target.x) / 2, |
|
midpointY = (source.y + target.y) / 2, |
|
pathData = [source, {x: target.x, y: source.y}, target]; |
|
pathData = pathData.map(projection); |
|
return path(pathData) |
|
} |
|
|
|
diagonal.projection = function(x) { |
|
if (!arguments.length) return projection; |
|
projection = x; |
|
return diagonal; |
|
}; |
|
|
|
diagonal.path = function(x) { |
|
if (!arguments.length) return path; |
|
path = x; |
|
return diagonal; |
|
}; |
|
|
|
return diagonal; |
|
} |
|
|
|
d3.phylogram.radialRightAngleDiagonal = function() { |
|
return d3.phylogram.rightAngleDiagonal() |
|
.path(function(pathData) { |
|
var src = pathData[0], |
|
mid = pathData[1], |
|
dst = pathData[2], |
|
radius = Math.sqrt(src[0]*src[0] + src[1]*src[1]), |
|
srcAngle = d3.phylogram.coordinateToAngle(src, radius), |
|
midAngle = d3.phylogram.coordinateToAngle(mid, radius), |
|
clockwise = Math.abs(midAngle - srcAngle) > Math.PI ? midAngle <= srcAngle : midAngle > srcAngle, |
|
rotation = 0, |
|
largeArc = 0, |
|
sweep = clockwise ? 0 : 1; |
|
return 'M' + src + ' ' + |
|
"A" + [radius,radius] + ' ' + rotation + ' ' + largeArc+','+sweep + ' ' + mid + |
|
'L' + dst; |
|
}) |
|
.projection(function(d) { |
|
var r = d.y, a = (d.x - 90) / 180 * Math.PI; |
|
return [r * Math.cos(a), r * Math.sin(a)]; |
|
}) |
|
} |
|
|
|
// Convert XY and radius to angle of a circle centered at 0,0 |
|
d3.phylogram.coordinateToAngle = function(coord, radius) { |
|
var wholeAngle = 2 * Math.PI, |
|
quarterAngle = wholeAngle / 4 |
|
|
|
var coordQuad = coord[0] >= 0 ? (coord[1] >= 0 ? 1 : 2) : (coord[1] >= 0 ? 4 : 3), |
|
coordBaseAngle = Math.abs(Math.asin(coord[1] / radius)) |
|
|
|
// Since this is just based on the angle of the right triangle formed |
|
// by the coordinate and the origin, each quad will have different |
|
// offsets |
|
switch (coordQuad) { |
|
case 1: |
|
coordAngle = quarterAngle - coordBaseAngle |
|
break |
|
case 2: |
|
coordAngle = quarterAngle + coordBaseAngle |
|
break |
|
case 3: |
|
coordAngle = 2*quarterAngle + quarterAngle - coordBaseAngle |
|
break |
|
case 4: |
|
coordAngle = 3*quarterAngle + coordBaseAngle |
|
} |
|
return coordAngle |
|
} |
|
|
|
d3.phylogram.styleTreeNodes = function(vis) { |
|
vis.selectAll('g.leaf.node') |
|
.append("svg:circle") |
|
.attr("r", 4.5) |
|
.attr('stroke', 'yellowGreen') |
|
.attr('fill', 'greenYellow') |
|
.attr('stroke-width', '2px'); |
|
|
|
vis.selectAll('g.root.node') |
|
.append('svg:circle') |
|
.attr("r", 4.5) |
|
.attr('fill', 'steelblue') |
|
.attr('stroke', '#369') |
|
.attr('stroke-width', '2px'); |
|
} |
|
|
|
function scaleBranchLengths(nodes, w) { |
|
// Visit all nodes and adjust y pos width distance metric |
|
var visitPreOrder = function(root, callback) { |
|
callback(root) |
|
if (root.children) { |
|
for (var i = root.children.length - 1; i >= 0; i--){ |
|
visitPreOrder(root.children[i], callback) |
|
}; |
|
} |
|
} |
|
visitPreOrder(nodes[0], function(node) { |
|
node.rootDist = (node.parent ? node.parent.rootDist : 0) + (node.data.length || 0) |
|
}) |
|
var rootDists = nodes.map(function(n) { return n.rootDist; }); |
|
var yscale = d3.scale.linear() |
|
.domain([0, d3.max(rootDists)]) |
|
.range([0, w]); |
|
visitPreOrder(nodes[0], function(node) { |
|
node.y = yscale(node.rootDist) |
|
}) |
|
return yscale |
|
} |
|
|
|
|
|
d3.phylogram.build = function(selector, nodes, options) { |
|
options = options || {} |
|
var w = options.width || d3.select(selector).style('width') || d3.select(selector).attr('width'), |
|
h = options.height || d3.select(selector).style('height') || d3.select(selector).attr('height'), |
|
w = parseInt(w), |
|
h = parseInt(h); |
|
var tree = options.tree || d3.layout.cluster() |
|
.size([h, w]) |
|
.sort(function(node) { return node.children ? node.children.length : -1; }) |
|
.children(options.children || function(node) { |
|
return node.branchset |
|
}); |
|
|
|
if (options.separation) { |
|
tree = tree.separation(options.separation); |
|
} |
|
|
|
var diagonal = options.diagonal || d3.phylogram.rightAngleDiagonal(); |
|
var vis = options.vis || d3.select(selector).append("svg:svg") |
|
.attr("width", w + 300) |
|
.attr("height", h + 30) |
|
.append("svg:g") |
|
.attr("transform", "translate(20, 20)"); |
|
var nodes = tree(nodes); |
|
|
|
var strokeWidth = options.strokeWidth || "4px"; |
|
var strokeColor = options.strokeColor || "#aaa"; |
|
|
|
var urlMap = options.urlMap || {} |
|
|
|
if (options.skipBranchLengthScaling) { |
|
var yscale = d3.scale.linear() |
|
.domain([0, w]) |
|
.range([0, w]); |
|
} else { |
|
var yscale = scaleBranchLengths(nodes, w) |
|
} |
|
|
|
if (!options.skipTicks) { |
|
vis.selectAll('line') |
|
.data(yscale.ticks(10)) |
|
.enter().append('svg:line') |
|
.attr('y1', 0) |
|
.attr('y2', h) |
|
.attr('x1', yscale) |
|
.attr('x2', yscale) |
|
.attr("stroke", "#ddd"); |
|
|
|
vis.selectAll("text.rule") |
|
.data(yscale.ticks(10)) |
|
.enter().append("svg:text") |
|
.attr("class", "rule") |
|
.attr("x", yscale) |
|
.attr("y", 0) |
|
.attr("dy", -3) |
|
.attr("text-anchor", "middle") |
|
.attr('font-size', '8px') |
|
.attr('fill', '#ccc') |
|
.text(function(d) { return Math.round(d*100) / 100; }); |
|
} |
|
|
|
var link = vis.selectAll("path.link") |
|
.data(tree.links(nodes)) |
|
.enter().append("svg:path") |
|
.attr("class", "link") |
|
.attr("d", diagonal) |
|
.attr("fill", "none") |
|
.attr("stroke", strokeColor) |
|
.attr("stroke-width", strokeWidth); |
|
|
|
var node = vis.selectAll("g.node") |
|
.data(nodes) |
|
.enter().append("svg:g") |
|
.attr("class", function(n) { |
|
if (n.children) { |
|
if (n.depth == 0) { |
|
return "root node" |
|
} else { |
|
return "inner node" |
|
} |
|
} else { |
|
return "leaf node" |
|
} |
|
}) |
|
.attr("transform", function(d) { return "translate(" + d.y + "," + d.x + ")"; }) |
|
|
|
if (!options.skipTreeNodeStyle) { |
|
d3.phylogram.styleTreeNodes(vis) |
|
} |
|
|
|
if (!options.skipInteriorBranchLengths) { |
|
vis.selectAll('g.inner.node') |
|
.append("svg:text") |
|
.attr("dx", -6) |
|
.attr("dy", -6) |
|
.attr("text-anchor", 'end') |
|
.attr('font-size', '8px') |
|
.attr('fill', '#ccc') |
|
.text(function(d) { return d.data.length; }); |
|
} |
|
|
|
if (!options.skipLabels) { |
|
vis.selectAll('g.leaf.node').append("svg:text") |
|
.attr("dx", 10) |
|
.attr("dy", "0.4em") |
|
.attr("id", function(d) { return "text-" + d.data.name; }) |
|
.attr("text-anchor", "start") |
|
.attr('fill', 'black') |
|
.text(function(d) { return d.data.name; }) |
|
|
|
vis.selectAll('g.leaf.node').append("svg:text") |
|
.attr("dx", function(d) { |
|
var elem = vis.select("#text-"+d.data.name)[0][0]; |
|
console.log(elem.id + " " + elem.getBBox().width) |
|
return (elem.getBBox().width + 30) }) |
|
.attr("dy", "0.4em") |
|
.attr("text-anchor", "start") |
|
.attr('fill', 'black') |
|
.text(function(d) { return '('+d.data.length+')'; }); |
|
|
|
} |
|
|
|
return {tree: tree, vis: vis} |
|
} |
|
|
|
d3.phylogram.buildRadial = function(selector, nodes, options) { |
|
options = options || {} |
|
var w = options.width || d3.select(selector).style('width') || d3.select(selector).attr('width'), |
|
r = w / 2, |
|
labelWidth = options.skipLabels ? 10 : options.labelWidth || 120; |
|
|
|
var vis = d3.select(selector).append("svg:svg") |
|
.attr("width", r * 2) |
|
.attr("height", r * 2) |
|
.append("svg:g") |
|
.attr("transform", "translate(" + r + "," + r + ")"); |
|
|
|
var tree = d3.layout.tree() |
|
.size([360, r - labelWidth]) |
|
.sort(function(node) { return node.children ? node.children.length : -1; }) |
|
.children(options.children || function(node) { |
|
return node.branchset |
|
}) |
|
.separation(function(a, b) { return (a.parent == b.parent ? 1 : 2) / a.depth; }); |
|
|
|
var phylogram = d3.phylogram.build(selector, nodes, { |
|
vis: vis, |
|
tree: tree, |
|
skipBranchLengthScaling: true, |
|
skipTicks: true, |
|
skipLabels: options.skipLabels, |
|
diagonal: d3.phylogram.radialRightAngleDiagonal() |
|
}) |
|
vis.selectAll('g.node') |
|
.attr("transform", function(d) { return "rotate(" + (d.x - 90) + ")translate(" + d.y + ")"; }) |
|
|
|
if (!options.skipLabels) { |
|
vis.selectAll('g.leaf.node text') |
|
.attr("dx", function(d) { return d.x < 180 ? 8 : -8; }) |
|
.attr("dy", ".31em") |
|
.attr("text-anchor", function(d) { return d.x < 180 ? "start" : "end"; }) |
|
.attr("transform", function(d) { return d.x < 180 ? null : "rotate(180)"; }) |
|
.attr('font-family', 'Helvetica Neue, Helvetica, sans-serif') |
|
.attr('font-size', '10px') |
|
.attr('fill', 'black') |
|
.text(function(d) { return d.data.name; }); |
|
|
|
vis.selectAll('g.inner.node text') |
|
.attr("dx", function(d) { return d.x < 180 ? -6 : 6; }) |
|
.attr("text-anchor", function(d) { return d.x < 180 ? "end" : "start"; }) |
|
.attr("transform", function(d) { return d.x < 180 ? null : "rotate(180)"; }); |
|
} |
|
|
|
return {tree: tree, vis: vis} |
|
} |
|
}()); |