Published
Edited
Mar 14, 2020
Fork of Tree of Life
2 forks
33 stars
Insert cell
Insert cell
Insert cell
Insert cell
chart = {
const root = d3.hierarchy(data, d => d.branchset)
.sum(d => d.branchset ? 0 : 1)
.sort((a, b) => (a.value - b.value) || d3.ascending(a.data.length, b.data.length));

cluster(root);
setRadius(root, root.data.length = 0, innerRadius / maxLength(root));

const svg = d3.create("svg")
.attr("viewBox", [-outerRadius, -outerRadius, width, width])
.attr("font-family", "sans-serif")
.attr("font-size", 10);

svg.append("style").text(`

.link--active {
stroke: #000 !important;
stroke-width: 1.5px;
}

.link-extension--active {
stroke-opacity: .6;
}

.label--active {
font-weight: bold;
}

`);

const linkExtension = svg.append("g")
.attr("fill", "none")
.attr("stroke", "currentColor")
.attr("stroke-opacity", 0.25)
.selectAll("path")
.data(root.links().filter(d => !d.target.children))
.join("path")
.each(function(d) { d.target.linkExtensionNode = this; })
.attr("d", linkExtensionConstant);

const link = svg.append("g")
.attr("fill", "none")
.attr("stroke", "currentColor")
.selectAll("path")
.data(root.links())
.join("path")
.each(function(d) { d.target.linkNode = this; })
.attr("d", linkConstant)
.attr("stroke", d => color(name(d.target)));

svg.append("g")
.selectAll("text")
.data(root.leaves())
.join("text")
.attr("dy", "0.31em")
.attr("transform", d => `rotate(${d.x - 90}) translate(${innerRadius + 4},0)${d.x < 180 ? "" : " rotate(180)"}`)
.attr("text-anchor", d => d.x < 180 ? "start" : "end")
.attr("fill", d => color(name(d)))
.text(d => name(d) || "N/A")
.on("mouseover", mouseovered(true))
.on("mouseout", mouseovered(false))
.append("title")
.text(d => d.data.name.replace(/_/g, " "));

function update(checked) {
const t = d3.transition().duration(750);
linkExtension.transition(t).attr("d", checked ? linkExtensionVariable : linkExtensionConstant);
link.transition(t).attr("d", 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).raise();
do d3.select(d.linkNode).classed("link--active", active).raise();
while (d = d.parent);
};
}

return Object.assign(svg.node(), {update});
}
Insert cell
chart.update(showLength)
Insert cell
cluster = d3.cluster()
.size([360, innerRadius])
.separation((a, b) => 1)
Insert cell
function name(d) {
if (d.height === 0) {
const parts = d.data.name.split("_");
for (let i = 0; i < parts.length; ++i) {
if (/^\d{4}(?:-\d{2}(?:-\d{2})?)?$/.test(parts[i])) {
return parts[i + 1]
.replace(/-.*/, "")
.replace(/([a-z])([A-Z])/, (_, a, b) => [a, b].join(" "));
}
}
}
}
Insert cell
color = d3.scaleOrdinal()
.domain(["United States", "China"])
.range(d3.schemeTableau10)
.unknown(null)
Insert cell
// 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);
}
Insert cell
// 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(d => setRadius(d, y0, k));
}
Insert cell
function linkVariable(d) {
return linkStep(d.source.x, d.source.radius, d.target.x, d.target.radius);
}
Insert cell
function linkConstant(d) {
return linkStep(d.source.x, d.source.y, d.target.x, d.target.y);
}
Insert cell
function linkExtensionVariable(d) {
return linkStep(d.target.x, d.target.radius, d.target.x, innerRadius);
}
Insert cell
function linkExtensionConstant(d) {
return linkStep(d.target.x, d.target.y, d.target.x, innerRadius);
}
Insert cell
function linkStep(startAngle, startRadius, endAngle, endRadius) {
const c0 = Math.cos(startAngle = (startAngle - 90) / 180 * Math.PI);
const s0 = Math.sin(startAngle);
const c1 = Math.cos(endAngle = (endAngle - 90) / 180 * Math.PI);
const 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;
}
Insert cell
data = parseNewick(await FileAttachment("ncov-2019.txt").text())
Insert cell
width = 954
Insert cell
outerRadius = width / 2
Insert cell
innerRadius = outerRadius - 80
Insert cell
// https://github.com/jasondavies/newick.js
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}
Insert cell
d3 = require("d3@5")
Insert cell
import {swatches} from "@d3/color-legend"
Insert cell

Purpose-built for displays of data

Observable is your go-to platform for exploring data and creating expressive data visualizations. Use reactive JavaScript notebooks for prototyping and a collaborative canvas for visual data exploration and dashboard creation.
Learn more