Skip to content

Instantly share code, notes, and snippets.

@phillipalexander
Forked from kueda/d3.phylogram.js
Created July 26, 2013 18:55
Show Gist options
  • Save phillipalexander/6091349 to your computer and use it in GitHub Desktop.
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…

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/

/*
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}
}
}());
<!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