Skip to content

Instantly share code, notes, and snippets.

@kueda
Last active February 23, 2023 11:53
Show Gist options
  • Star 17 You must be signed in to star a gist
  • Fork 18 You must be signed in to fork a gist
  • Save kueda/1036776 to your computer and use it in GitHub Desktop.
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

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.
Copyright (c) 2013, Ken-ichi Ueda
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer. Redistributions in binary
form must reproduce the above copyright notice, this list of conditions and
the following disclaimer in the documentation and/or other materials
provided with the distribution.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
POSSIBILITY OF SUCH DAMAGE.
DOCUEMENTATION
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.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.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.name + ' ('+d.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="http://d3js.org/d3.v3.min.js" type="text/javascript"></script>
<script 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>
/**
* Newick format parser in JavaScript.
*
* Copyright (c) Jason Davies 2010.
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*
* Example tree (from http://en.wikipedia.org/wiki/Newick_format):
*
* +--0.1--A
* F-----0.2-----B +-------0.3----C
* +------------------0.5-----E
* +---------0.4------D
*
* Newick format:
* (A:0.1,B:0.2,(C:0.3,D:0.4)E:0.5)F;
*
* Converted to JSON:
* {
* name: "F",
* branchset: [
* {name: "A", length: 0.1},
* {name: "B", length: 0.2},
* {
* name: "E",
* length: 0.5,
* branchset: [
* {name: "C", length: 0.3},
* {name: "D", length: 0.4}
* ]
* }
* ]
* }
*
* Converted to JSON, but with no names or lengths:
* {
* branchset: [
* {}, {}, {
* branchset: [{}, {}]
* }
* ]
* }
*/
(function(exports) {
exports.parse = function(s) {
var ancestors = [];
var tree = {};
var tokens = s.split(/\s*(;|\(|\)|,|:)\s*/);
for (var i=0; i<tokens.length; i++) {
var token = tokens[i];
switch (token) {
case '(': // new branchset
var subtree = {};
tree.branchset = [subtree];
ancestors.push(tree);
tree = subtree;
break;
case ',': // another branch
var subtree = {};
ancestors[ancestors.length-1].branchset.push(subtree);
tree = subtree;
break;
case ')': // optional name next
tree = ancestors.pop();
break;
case ':': // optional length next
break;
default:
var x = tokens[i-1];
if (x == ')' || x == '(' || x == ',') {
tree.name = token;
} else if (x == ':') {
tree.length = parseFloat(token);
}
}
}
return tree;
};
})(
// exports will be set in any commonjs platform; use it if it's available
typeof exports !== "undefined" ?
exports :
// otherwise construct a name space. outside the anonymous function,
// "this" will always be "window" in a browser, even in strict mode.
this.Newick = {}
);
@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