Public
Edited
Jul 29
Fork of Tree of Life
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));
setColor(root);

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

svg.append("g").call(legend);

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", "#000")
.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("stroke-width", "0.5")
.attr("d", linkExtensionConstant);

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

svg
.append("g")
.selectAll("text")
.data(root.leaves())
.join("text")
.attr("dy", ".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("font-style", "italic")
.attr("fill", "white")
.text((d) => d.data.name.replace(/_/g, " "))
.on("mouseover", mouseovered(true))
.on("mouseout", mouseovered(false));

// 1. Build a list of leaf nodes in order (same as your radial tree layout)
const rawGcPoints = root
.leaves()
.map((d) => {
const name = d.data.name?.trim().toLowerCase();
const gc = nodeCountMap.get(name);
if (gc == null) {
console.warn("No GC for", d.data.name);
return null;
}

return { name: d.data.name, gc, angleDeg: d.x };
})
.filter((d) => d !== null);

const gcExtent = d3.extent(gcMap.values());
const gcScale = d3
.scaleLinear()
.domain(gcExtent)
.range([0, outerRadius - innerRadius]); // bar length only

const barGroup = svg.append("g").attr("class", "gc-bars");

barGroup
.selectAll("line")
.data(root.leaves())
.join("line")
.attr("x1", (d) => {
const angle = ((d.x - 90) * Math.PI) / 180;
return innerRadius * Math.cos(angle);
})
.attr("y1", (d) => {
const angle = ((d.x - 90) * Math.PI) / 180;
return innerRadius * Math.sin(angle);
})
.attr("x2", (d) => {
const gc = gcMap.get(d.data.name?.trim().toLowerCase());
const barLength = gc != null ? gcScale(gc) : 0;
const angle = ((d.x - 90) * Math.PI) / 180;
return (innerRadius + barLength) * Math.cos(angle);
})
.attr("y2", (d) => {
const gc = gcMap.get(d.data.name?.trim().toLowerCase());
const barLength = gc != null ? gcScale(gc) : 0;
const angle = ((d.x - 90) * Math.PI) / 180;
return (innerRadius + barLength) * Math.sin(angle);
})
.attr("stroke", "green")
.attr("stroke-width", 4)
.attr("stroke-opacity", 0.5);

const tooltip = d3
.select("body")
.append("div")
.style("position", "absolute")
.style("pointer-events", "none")
.style("background", "white")
.style("border", "1px solid #ccc")
.style("padding", "5px 8px")
.style("font-size", "12px")
.style("border-radius", "4px")
.style("box-shadow", "0 2px 5px rgba(0,0,0,0.2)")
.style("display", "none");

barGroup
.selectAll("line")
.on("mouseover", function (event, d) {
const gc = gcMap.get(d.data.name?.trim().toLowerCase());
d3.select(this).attr("stroke", "black");
tooltip
.style("display", "block")
.html(`<b>${d.data.name}</b><br/>GC: ${gc?.toFixed(4) ?? "?"}`);
})
.on("mousemove", function (event) {
tooltip
.style("left", event.pageX + 10 + "px")
.style("top", event.pageY - 10 + "px");
})
.on("mouseout", function () {
d3.select(this).attr("stroke", "green").attr("stroke-opacity", 0.5);
tooltip.style("display", "none");
});

// GC content ticks (e.g. 0.40, 0.42, ..., 0.52)
const gcTicks = d3.ticks(0.38, 0.48, 6); // adjust range and number as needed

const gcScale2 = d3
.scaleLinear()
.domain(gcExtent)
.range([innerRadius, outerRadius]);

// Draw concentric GC rings
const gcScaleGroup = svg
.append("g")
.attr("stroke", "#ccc")
.attr("stroke-dasharray", "2,2")
.attr("fill", "none");

gcScaleGroup
.selectAll("circle")
.data(gcTicks)
.join("circle")
.attr("r", (d) => gcScale2(d));

// Add labels for each ring
gcScaleGroup
.selectAll("text")
.data(gcTicks)
.join("text")
.attr("y", (d) => -gcScale2(d)) // place along vertical (12 o'clock)
.attr("x", 0)
.attr("dy", "-0.3em")
.attr("text-anchor", "middle")
.attr("font-size", 8)
.attr("fill", "#555")
.text((d) => d.toFixed(3));

const gcLine = d3
.lineRadial()
.angle((d) => ((d.angleDeg - 90) * Math.PI) / 180)
.radius((d) => gcScale2(d.gc));

const gcPoints = root
.leaves()
.map((d) => {
const name = d.data.name?.trim().toLowerCase();
return {
name,
angleDeg: d.x,
gc: gcMap.get(name)
};
})
.filter((d) => d.gc != null);

console.log(gcPoints);

svg
.append("path")
.datum(gcPoints)
.attr("fill", "none")
.attr("stroke", "red")
.attr("stroke-opacity", 0.6)
.attr("stroke-width", 2)
.attr("d", gcLine);

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 (event, d) {
d3.select(this)
.classed("label--active", active)
.attr("fill", active ? "black" : "white");
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
color = d3
.scaleOrdinal()
.domain(["Non-sedge flowering plants", "Sedges", "Mosses"])
.range(d3.schemeCategory10)
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
// Set the color of each node by recursively inheriting.
function setColor(d) {
if (!d.children || d.children.length === 0) {
// Leaf node
const name = d.data.name;
d.group = nameToGroup[name] || null;
} else {
const childGroups = new Set();
d.children.forEach(child => {
setColor(child);
if (child.group) childGroups.add(child.group);
});

// If all children share the same group, assign it to the parent
if (childGroups.size === 1) {
d.group = [...childGroups][0];
} else {
d.group = null; // Mixed group — do not assign
}
}

d.color = d.group ? color(d.group) : (d.parent ? d.parent.color : null);
}

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
legend = (svg) => {
const g = svg
.selectAll("g")
.data(color.domain())
.join("g")
.attr(
"transform",
(d, i) => `translate(${-outerRadius},${-outerRadius + i * 30})`
);

g.append("rect").attr("width", 24).attr("height", 24).attr("fill", color);

g.append("text")
.attr("x", 30)
.attr("y", 9)
.attr("dy", "0.5em")
.attr("font-size", "20px")
.text((d) => d);
}
Insert cell
function forceUltrametricExtend(tree) {
// Step 1: Calculate distance from root to each tip
function tipDepths(node, depth = 0, result = {}) {
const currentLength = node.length ?? 0;
const newDepth = depth + currentLength;

if (!node.branchset || node.branchset.length === 0) {
result[node.name] = newDepth;
} else {
node.branchset.forEach((child) => tipDepths(child, newDepth, result));
}
return result;
}

const depths = tipDepths(tree);
const maxDepth = Math.max(...Object.values(depths));

// Step 2: Extend terminal branches
function extendTips(node, depth = 0) {
const currentLength = node.length ?? 0;
const newDepth = depth + currentLength;

if (!node.branchset || node.branchset.length === 0) {
// leaf node
const missing = maxDepth - newDepth;
node.length = (node.length ?? 0) + missing;
} else {
node.branchset.forEach((child) => extendTips(child, newDepth));
}
}

extendTips(tree, 0);
return tree;
}
Insert cell
data = forceUltrametricExtend(data2)
Insert cell
data2 = parseNewick(await FileAttachment("concatenated.dated.treefile").text())
Insert cell
width = 954
Insert cell
outerRadius = width / 2
Insert cell
innerRadius = outerRadius - 170
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@6")
Insert cell
Dicots = "Non-sedge flowering plants"
Insert cell
Mosses = "Mosses"
Insert cell
Sedges = "Sedges"
Insert cell
Insert cell
nodeCountMap = new Map(
d3.groups(pdata, d => d.sp).map(([sp, entries]) => {
const totalNodes = d3.sum(entries, d => d.node_count);
return [sp.trim().toLowerCase(), totalNodes];
})
);
Insert cell
gcMap = {
const grouped = d3.groups(pdata, (d) => d.sp);

return new Map(
grouped.map(([sp, entries]) => {
const totalLen = d3.sum(entries, (d) => d.seq_len);
const weightedGC = d3.sum(entries, (d) => d.gc * d.seq_len) / totalLen;
return [sp.trim().toLowerCase(), weightedGC];
})
);
}
Insert cell
pdata = d3.tsvParse(polarData, (d) => ({
si: +d.subgraph_index,
gc: +d.gc,
node_count: +d.node_count,
edge_count: +d.edge_count,
cov: +d.coverage,
segs: d.segments.split(","),
seq_len: +d.total_seq_len,
is_circular: d.is_circular,
sp: d.species
}))
Insert cell
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