Demonstration of two common tree visualizations using d3 and newick.js. I hadn't found any examples of these kinds of right-angle trees so I figured I'd share. Input data is a Newick-formatted guide tree from a clustalw multiple sequence alignment on some cytochrome b sequences from several North American snakes. And it turns out the creator of newick.js also has an implementation of this kind of radial tree: check out http://www.jasondavies.com/tree-of-life/
-
-
Save phillipalexander/6091349 to your computer and use it in GitHub Desktop.
Demonstration of two common tree visualizations using [d3](http://mbostock.github.com/d3) and [newick.js](https://github.com/jasondavies/newick.js). I hadn't found any examples of these kinds of right-angle trees so I figured I'd share. Input data is a Newick-formatted guide tree from a clustalw multiple sequence alignment on some cytochrome b s…
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* | |
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. | |
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 | |
}); | |
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); | |
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", "#aaa") | |
.attr("stroke-width", "4px"); | |
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 + ")"; }) | |
d3.phylogram.styleTreeNodes(vis) | |
if (!options.skipLabels) { | |
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; }); | |
vis.selectAll('g.leaf.node').append("svg:text") | |
.attr("dx", 8) | |
.attr("dy", 3) | |
.attr("text-anchor", "start") | |
.attr('font-family', 'Helvetica Neue, Helvetica, sans-serif') | |
.attr('font-size', '10px') | |
.attr('fill', 'black') | |
.text(function(d) { return d.data.name + ' ('+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} | |
} | |
}()); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!DOCTYPE html> | |
<html lang='en' xml:lang='en' xmlns='http://www.w3.org/1999/xhtml'> | |
<head> | |
<meta content='text/html;charset=UTF-8' http-equiv='content-type'> | |
<title>Right-angle phylograms and dendrograms with d3</title> | |
<script src="https://raw.github.com/mbostock/d3/master/d3.v2.js" type="text/javascript"></script> | |
<script src="https://raw.github.com/jasondavies/newick.js/master/src/newick.js" type="text/javascript"></script> | |
<script src="d3.phylogram.js" type="text/javascript"></script> | |
<script> | |
function load() { | |
var newick = Newick.parse("(((Crotalus_oreganus_oreganus_cytochrome_b:0.00800,Crotalus_horridus_cytochrome_b:0.05866):0.04732,(Thamnophis_elegans_terrestris_cytochrome_b:0.00366,Thamnophis_atratus_cytochrome_b:0.00172):0.06255):0.00555,(Pituophis_catenifer_vertebralis_cytochrome_b:0.00552,Lampropeltis_getula_cytochrome_b:0.02035):0.05762,((Diadophis_punctatus_cytochrome_b:0.06486,Contia_tenuis_cytochrome_b:0.05342):0.01037,Hypsiglena_torquata_cytochrome_b:0.05346):0.00779);") | |
var newickNodes = [] | |
function buildNewickNodes(node, callback) { | |
newickNodes.push(node) | |
if (node.branchset) { | |
for (var i=0; i < node.branchset.length; i++) { | |
buildNewickNodes(node.branchset[i]) | |
} | |
} | |
} | |
buildNewickNodes(newick) | |
d3.phylogram.buildRadial('#radialtree', newick, { | |
width: 400, | |
skipLabels: true | |
}) | |
d3.phylogram.build('#phylogram', newick, { | |
width: 300, | |
height: 400 | |
}); | |
} | |
</script> | |
<style type="text/css" media="screen"> | |
body { font-family: "Helvetica Neue", Helvetica, sans-serif; } | |
td { vertical-align: top; } | |
</style> | |
</head> | |
<body onload="load()"> | |
<table> | |
<tr> | |
<td> | |
<h2>Circular Dendrogram</h2> | |
<div id='radialtree'></div> | |
</td> | |
<td> | |
<h2>Phylogram</h2> | |
<div id='phylogram'></div> | |
</td> | |
</tr> | |
</table> | |
</body> | |
</html> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment