radio_dendrogram_2 = {
const radius = 550;
const padding = 200;
const cluster = d3.cluster().size([2 * Math.PI, radius - 100]);
const tree = d3.hierarchy(groupedData, ([, value]) =>
value instanceof Map ? Array.from(value.entries()) : null
);
tree.sum((d) => (d.value instanceof Map ? 0 : d.value));
cluster(tree);
function toTitleCase(str) {
return str
?.toLowerCase()
.split(/[\s/_-]+/)
.map((w) => w.charAt(0).toUpperCase() + w.slice(1))
.join(" ");
}
const imageViewer = html`<div style="
width: 240px;
height: 780px;
padding: 30px;
overflow: hidden;
display: flex;
flex-direction: column;
justify-content: flex-start;
gap: 10px;
"></div>`;
const svg = d3
.create("svg")
.attr("viewBox", [
-radius - padding,
-radius - padding,
(radius + padding) * 2,
(radius + padding) * 2
])
.style("font", "14px sans-serif")
.style("user-select", "none");
const allLinks = svg
.append("g")
.attr("fill", "none")
.attr("stroke", "#ccc")
.attr("stroke-opacity", 0.6)
.attr("stroke-width", 1)
.selectAll("path")
.data(tree.links())
.join("path")
.attr(
"d",
d3
.linkRadial()
.angle((d) => d.x)
.radius((d) => d.y)
);
const allNodes = svg
.append("g")
.selectAll("g")
.data(tree.descendants())
.join("g")
.attr(
"transform",
(d) => `rotate(${(d.x * 180) / Math.PI - 90}) translate(${d.y},0)`
)
.on("mouseover", function (event, d) {
const pathNodes = new Set([...d.ancestors(), ...d.descendants()]);
allNodes
.selectAll("circle")
.transition()
.duration(200)
.attr("r", (n) => (pathNodes.has(n) ? 5 : 2.5))
.attr("fill", (n) =>
pathNodes.has(n) ? "#02a4fb" : n.children ? "#555" : "#999"
);
allNodes
.selectAll("text")
.transition()
.duration(200)
.style("font-weight", (n) => (pathNodes.has(n) ? "bold" : "normal"))
.style("fill", (n) => (pathNodes.has(n) ? "#02a4fb" : "#333"))
.style("font-size", (n) => (pathNodes.has(n) ? "14px" : "14px"));
allLinks
.transition()
.duration(200)
.attr("stroke", (l) =>
pathNodes.has(l.source) && pathNodes.has(l.target)
? "#02a4fb"
: "#ccc"
)
.attr("stroke-width", (l) =>
pathNodes.has(l.source) && pathNodes.has(l.target) ? 2.5 : 1
);
// Show images if available
if (!d.children) {
const task = d.parent?.data[0]?.trim();
const category = d.data[0]?.trim();
const key = `${task}|||${category}`;
if (imageMap.has(key)) {
const urls = imageMap.get(key);
imageViewer.innerHTML = "";
const title = document.createElement("div");
title.textContent = category;
title.style.fontWeight = "bold";
title.style.fontSize = "16px";
title.style.marginBottom = "10px";
title.style.color = "#000406";
title.style.textAlign = "center";
imageViewer.appendChild(title);
// Limit to first 2 images for Ship Classification
const limitedUrls =
task === "Ship Classification" ? urls.slice(0, 2) : urls;
limitedUrls.forEach((url) => {
const img = document.createElement("img");
img.src = url;
img.style.width = "100%";
img.style.borderRadius = "6px";
img.style.objectFit = "cover";
img.style.maxHeight = "150px";
imageViewer.appendChild(img);
});
}
}
})
.on("mouseout", function () {
allNodes
.selectAll("circle")
.transition()
.duration(200)
.attr("r", 2.5)
.attr("fill", (d) => (d.children ? "#555" : "#999"));
allNodes
.selectAll("text")
.transition()
.duration(200)
.style("font-weight", "normal")
.style("fill", "#333")
.style("font-size", "14px");
allLinks
.transition()
.duration(200)
.attr("stroke", "#ccc")
.attr("stroke-width", 1);
imageViewer.innerHTML = "";
});
allNodes
.append("circle")
.attr("r", 2.5)
.attr("fill", (d) => (d.children ? "#555" : "#999"));
allNodes
.append("text")
.attr("dy", "0.31em")
.attr("x", (d) => (d.x < Math.PI === !d.children ? 6 : -6))
.attr("text-anchor", (d) =>
d.x < Math.PI === !d.children ? "start" : "end"
)
.attr("transform", (d) => (d.x >= Math.PI ? "rotate(180)" : null))
.text((d) => toTitleCase(d.data[0]))
.style("fill", "#333")
.style("font-size", "14px");
return html`<div style="
display: flex;
gap: 10px;
align-items: center;
justify-content: center;
">
<div style="
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
width: 280px;
gap: 10px;
padding-top: 30px;
">
${imageViewer}
</div>
${svg.node()}
</div>`;
}