-
-
Save kueda/1036776 to your computer and use it in GitHub Desktop.
| /* | |
| 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> |
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.
FYI, your repo links are out of date again. Fixed it offline though and had a look. Great work here. Thanks for posting.
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.
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
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?
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