Skip to content

Instantly share code, notes, and snippets.

@kueda
Last active May 8, 2024 17:33
Show Gist options
  • Select an option

  • Save kueda/1036776 to your computer and use it in GitHub Desktop.

Select an option

Save kueda/1036776 to your computer and use it in GitHub Desktop.
Right-angle phylograms and circular dendrograms with d3. To preview see http://bl.ocks.org/kueda/1036776
/*
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)
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;
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 - 120])
.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,
diagonal: d3.phylogram.radialRightAngleDiagonal()
})
vis.selectAll('g.node')
.attr("transform", function(d) { return "rotate(" + (d.x - 90) + ")translate(" + d.y + ")"; })
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: parseInt(d3.select('#radialtree').style('width')) * 0.75,
})
d3.phylogram.build('#phylogram', newick, {
width: parseInt(d3.select('#phylogram').style('width')) * 0.75,
height: newickNodes.length * 20
});
}
</script>
</head>
<body onload="load()">
<h2>Circular Dendrogram</h2>
<div id='radialtree'></div>
<h2>Phylogram</h2>
<div id='phylogram'></div>
</body>
</html>
@jedifran
Copy link

Hi,

I've been unable to get this code running on both safari and chrome and the source links are not working anymore. Update please!

Cheers,
Jed

@kueda
Copy link
Author

kueda commented Mar 14, 2012

Thanks for the heads-up, should be fixed (they just changed the layout of the d3 repo I was linking to, see it running at http://bl.ocks.org/1036776). You should also check out that implementation by Jason Davies I linked to above (http://www.jasondavies.com/tree-of-life/), and the examples in the official repo. You might find better solutions there.

@jedifran
Copy link

jedifran commented Mar 14, 2012 via email

@briantjacobs
Copy link

FYI, your repo links are out of date again. Fixed it offline though and had a look. Great work here. Thanks for posting.

@rec3141
Copy link

rec3141 commented Sep 20, 2013

Your example is cool but doesn't seem to work with d3 v3 (throws an error about node.data.length being undefined), have you looked into that by chance? I haven't been able to figure it out.

@kueda
Copy link
Author

kueda commented Nov 27, 2013

Hm, I do't recall getting emails about these comments. Bummer. I fixed the errors, so it should work now. Check out http://bl.ocks.org/kueda/1036776

@zorji
Copy link

zorji commented Jun 13, 2015

Hi, I have a project that need to does the same thing but the input format is NHX instead of Newick. Can I create a GitRepo base on your code and reference back to this page?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment