Skip to content

Instantly share code, notes, and snippets.

@aroberts
Forked from kueda/d3.phylogram.js
Created September 16, 2011 18:42
Show Gist options
  • Save aroberts/1222777 to your computer and use it in GitHub Desktop.
Save aroberts/1222777 to your computer and use it in GitHub Desktop.
Right-angle phylograms and circular dendrograms with d3. Forked to add some options and minor formatting changes.

Changes:

  • Added stroke width and color options
  • Added option to suppress node styling (the circles)
  • Added option to suppress interior branch lengths
  • Added option to specify just the separator of the tree function

TODO: URL table (map name to link, clicking name takes you to link) TODO: evenly vspaced branches


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.
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}
}
}());
<!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.js" type="text/javascript"></script>
<script src="https://raw.github.com/mbostock/d3/master/d3.layout.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