A re-implementation of Jason Davies’ Phylogenetic Tree of Life, with faded gray lines to connect the leaf nodes of the tree to their corresponding labels inspired by a figure from Nature.
This implementation modifies the depth of interior nodes in a cluster layout to show branch lengths. Toggle the checkbox in the top-left corner to show or hide branch lengths, and mouseover a label to highlight its path to the root.
forked from git-ashish's block: Tree of Life
xxxxxxxxxx
<meta charset="utf-8">
<style>
body {
font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
margin: 0;
}
#show-length {
position: absolute;
top: 10px;
left: 10px;
}
.links {
fill: none;
stroke: #000;
stroke-width: 2.5px;
}
.link-extensions {
fill: none;
stroke: #000;
stroke-opacity: .25;
}
.labels {
font: 10px sans-serif;
}
.link--active {
stroke: #000 !important;
stroke-width: 3.5px;
}
.link-extension--active {
stroke-opacity: .6;
}
.label--active {
font-weight: bold;
}
</style>
<script>
var txt="life.txt";
</script>
<label id="show-length">
<input type="radio" name="phase" id="group" class="group" value="group" onclick="reload2();" checked/> Group Phase
<input type="radio" name="phase" id="knockout" class="knockout" value="knockout" onclick="reload();" /> Round of 16
<input type="radio" name="phase" id="quarter" class="quarter" value="quarter" onclick="reload3();" /> Quarter-finals
<input type="radio" name="phase" id="semi" class="semi" value="semi" onclick="reload4();" /> Semi-finals
<input type="radio" name="phase" id="final" class="final" value="final" onclick="reload5();" /> Final
</label>
<!-- Copyright 2011 Jason Davies https://github.com/jasondavies/newick.js -->
<script>function parseNewick(a){for(var e=[],r={},s=a.split(/\s*(;|\(|\)|,|:)\s*/),t=0;t<s.length;t++){var n=s[t];switch(n){case"(":var c={};r.branchset=[c],e.push(r),r=c;break;case",":var c={};e[e.length-1].branchset.push(c),r=c;break;case")":r=e.pop();break;case":":break;default:var h=s[t-1];")"==h||"("==h||","==h?r.name=n:":"==h&&(r.length=parseFloat(n))}}return r}</script>
<!-- Copyright 2016 Mike Bostock https://d3js.org -->
<script src="https://d3js.org/d3.v4.min.js"></script>
<script>
var outerRadius = 960 / 2,
innerRadius = outerRadius - 170;
var color = d3.scaleOrdinal()
.domain(["A", "B", "C", "D", "E", "F", "G", "H"])
.range(d3.schemeCategory10);
var cluster = d3.cluster()
.size([360, innerRadius])
.separation(function(a, b) { return 1; });
var svg2,svg3,svg4,svg5 = null;
var svg = d3.select("body").append("svg")
.attr("class", "groupsvg")
.attr("width", outerRadius * 2)
.attr("height", outerRadius * 2);
createLegend(svg);
var chart = svg.append("g")
.attr("transform", "translate(" + outerRadius + "," + outerRadius + ")");
d3.text(txt, function(error, life) {
if (error) throw error;
var root = d3.hierarchy(parseNewick(life), function(d) { return d.branchset; })
.sum(function(d) { return d.branchset ? 0 : 1; })
.sort(function(a, b) { return (a.value - b.value) || d3.ascending(a.data.length, b.data.length); });
cluster(root);
setChart(root, chart, "group");
});
// Compute the maximum cumulative length of any node in the tree.
function maxLength(d) {
return d.data.length + (d.children ? d3.max(d.children, maxLength) : 0);
}
// Set the radius of each node by recursively summing and scaling the distance from the root.
function setRadius(d, y0, k) {
d.radius = (y0 += d.data.length) * k;
if (d.children) d.children.forEach(function(d) { setRadius(d, y0, k); });
}
// Set the color of each node by recursively inheriting.
function setColor(d) {
var name = d.data.name;
d.color = color.domain().indexOf(name[1]) >= 0 ? color(name[1]) : d.parent ? d.parent.color : null;
if (d.children) d.children.forEach(setColor);
}
function linkVariable(d) {
return linkStep(d.source.x, d.source.radius, d.target.x, d.target.radius);
}
function linkConstant(d) {
return linkStep(d.source.x, d.source.y, d.target.x, d.target.y);
}
function linkExtensionVariable(d) {
return linkStep(d.target.x, d.target.radius, d.target.x, innerRadius);
}
function linkExtensionConstant(d) {
return linkStep(d.target.x, d.target.y, d.target.x, innerRadius);
}
// Like d3.svg.diagonal.radial, but with square corners.
function linkStep(startAngle, startRadius, endAngle, endRadius) {
var c0 = Math.cos(startAngle = (startAngle - 90) / 180 * Math.PI),
s0 = Math.sin(startAngle),
c1 = Math.cos(endAngle = (endAngle - 90) / 180 * Math.PI),
s1 = Math.sin(endAngle);
return "M" + startRadius * c0 + "," + startRadius * s0
+ (endAngle === startAngle ? "" : "A" + startRadius + "," + startRadius + " 0 0 " + (endAngle > startAngle ? 1 : 0) + " " + startRadius * c1 + "," + startRadius * s1)
+ "L" + endRadius * c1 + "," + endRadius * s1;
}
function createLegend(svg){
var legend = svg.append("g")
.attr("class", "legend")
.selectAll("g")
.data(color.domain())
.enter().append("g")
.attr("transform", function(d, i) { return "translate(" + (outerRadius * 2 - 10) + "," + (i * 20 + 10) + ")"; });
legend.append("rect")
.attr("x", -18)
.attr("width", 18)
.attr("height", 18)
.attr("fill", color);
legend.append("text")
.attr("x", -24)
.attr("y", 9)
.attr("dy", ".35em")
.attr("text-anchor", "end")
.text(function(d) { return d; });
}
function setChart(root, chart, type) {
var input2 = d3.select("#show-length input."+type).on("change", changed),
timeout = setTimeout(function() { input2.property("checked", true).each(changed); }, 2000);
setRadius(root, root.data.length = 0, innerRadius / maxLength(root));
setColor(root);
var linkExtension = chart.append("g")
.attr("class", "link-extensions")
.selectAll("path")
.data(root.links().filter(function(d) { return !d.target.children; }))
.enter().append("path")
.each(function(d) { d.target.linkExtensionNode = this; })
.attr("d", linkExtensionConstant);
var link = chart.append("g")
.attr("class", "links")
.selectAll("path")
.data(root.links())
.enter().append("path")
.each(function(d) { d.target.linkNode = this; })
.attr("d", linkConstant)
.attr("stroke", function(d) { return d.target.color; });
chart.append("g")
.attr("class", "labels")
.selectAll("text")
.data(root.leaves())
.enter().append("text")
.attr("dy", ".31em")
.attr("transform", function(d) { return "rotate(" + (d.x - 90) + ")translate(" + (innerRadius + 4) + ",0)" + (d.x < 180 ? "" : "rotate(180)"); })
.attr("text-anchor", function(d) { return d.x < 180 ? "start" : "end"; })
.text(function(d) { return (d.data.name.indexOf("_")>0)?d.data.name.replace(/_/g, " ").substr(3,d.data.name.length):d.data.name; })
.on("mouseover", mouseovered(true))
.on("mouseout", mouseovered(false));
function changed() {
clearTimeout(timeout);
var t = d3.transition().duration(750);
linkExtension.transition(t).attr("d", this.checked ? linkExtensionVariable : linkExtensionConstant);
link.transition(t).attr("d", this.checked ? linkVariable : linkConstant);
}
function mouseovered(active) {
return function(d) {
d3.select(this).classed("label--active", active);
d3.select(d.linkExtensionNode).classed("link-extension--active", active).each(moveToFront);
do d3.select(d.linkNode).classed("link--active", active).each(moveToFront); while (d = d.parent);
};
}
function moveToFront() {
this.parentNode.appendChild(this);
}
}
function reload2(){
d3.select("svg.groupsvg")
.attr("style","display:block;");
d3.select("svg.quartersvg")
.attr("style","display:none;");
d3.select("svg.knockoutsvg")
.attr("style","display:none;");
d3.select("svg.semisvg")
.attr("style","display:none;");
d3.select("svg.finalsvg")
.attr("style","display:none;");
}
function reload(){
txt="life2.txt";
d3.select("svg.groupsvg").attr("style","display:none;");
d3.select("svg.quartersvg").attr("style","display:none;");
d3.select("svg.semisvg").attr("style","display:none;");
d3.select("svg.finalsvg").attr("style","display:none;");
if(svg2){
svg2.attr("style","display:block;");
}else{
svg2 = d3.select("body").append("svg")
.attr("class", "knockoutsvg")
.attr("width", outerRadius * 2)
.attr("height", outerRadius * 2);
createLegend(svg2);
var chart = svg2.append("g")
.attr("transform", "translate(" + outerRadius + "," + outerRadius + ")");
d3.text(txt, function(error, life) {
if (error) throw error;
var root = d3.hierarchy(parseNewick(life), function(d) { return d.branchset; })
.sum(function(d) { return d.branchset ? 0 : 1; })
.sort(function(a, b) { return (a.value - b.value) || d3.ascending(a.data.length, b.data.length); });
cluster(root);
setChart(root, chart, "knockout");
});
}
}
function reload3(){
txt="life3.txt";
d3.select("svg.groupsvg")
.attr("style","display:none;");
d3.select("svg.knockoutsvg")
.attr("style","display:none;");
d3.select("svg.semisvg")
.attr("style","display:none;");
d3.select("svg.finalsvg")
.attr("style","display:none;");
if(svg3){
svg3.attr("style","display:block;");
}else{
svg3 = d3.select("body").append("svg")
.attr("class", "quartersvg")
.attr("width", outerRadius * 2)
.attr("height", outerRadius * 2);
createLegend(svg3);
var chart = svg3.append("g")
.attr("transform", "translate(" + outerRadius + "," + outerRadius + ")");
d3.text(txt, function(error, life) {
if (error) throw error;
var root = d3.hierarchy(parseNewick(life), function(d) { return d.branchset; })
.sum(function(d) { return d.branchset ? 0 : 1; })
.sort(function(a, b) { return (a.value - b.value) || d3.ascending(a.data.length, b.data.length); });
cluster(root);
setChart(root, chart, "quarter");
});
}
}
function reload4(){
txt="life4.txt";
d3.select("svg.groupsvg")
.attr("style","display:none;");
d3.select("svg.knockoutsvg")
.attr("style","display:none;");
d3.select("svg.quartersvg")
.attr("style","display:none;");
d3.select("svg.finalsvg")
.attr("style","display:none;");
if(svg4){
svg4.attr("style","display:block;");
}else{
svg4 = d3.select("body").append("svg")
.attr("class", "semisvg")
.attr("width", outerRadius * 2)
.attr("height", outerRadius * 2);
createLegend(svg4);
var chart = svg4.append("g")
.attr("transform", "translate(" + outerRadius + "," + outerRadius + ")");
d3.text(txt, function(error, life) {
if (error) throw error;
var root = d3.hierarchy(parseNewick(life), function(d) { return d.branchset; })
.sum(function(d) { return d.branchset ? 0 : 1; })
.sort(function(a, b) { return (a.value - b.value) || d3.ascending(a.data.length, b.data.length); });
cluster(root);
setChart(root, chart, "semi");
});
}
}
function reload5(){
txt="life5.txt";
d3.select("svg.groupsvg")
.attr("style","display:none;");
d3.select("svg.knockoutsvg")
.attr("style","display:none;");
d3.select("svg.quartersvg")
.attr("style","display:none;");
d3.select("svg.semisvg")
.attr("style","display:none;");
if(svg5){
svg5.attr("style","display:block;");
}else{
svg5 = d3.select("body").append("svg")
.attr("class", "finalsvg")
.attr("width", outerRadius * 2)
.attr("height", outerRadius * 2);
createLegend(svg5);
var chart = svg5.append("g")
.attr("transform", "translate(" + outerRadius + "," + outerRadius + ")");
d3.text(txt, function(error, life) {
if (error) throw error;
var root = d3.hierarchy(parseNewick(life), function(d) { return d.branchset; })
.sum(function(d) { return d.branchset ? 0 : 1; })
.sort(function(a, b) { return (a.value - b.value) || d3.ascending(a.data.length, b.data.length); });
cluster(root);
setChart(root, chart, "final");
});
}
}
</script>
https://d3js.org/d3.v4.min.js