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 });
}