Public
Edited
Jun 29
Insert cell
Insert cell
Insert cell
tableData = FileAttachment("task_catogeries.csv").csv()
Insert cell
d3 = require("d3@7")
Insert cell
import {navio} from "@john-guerra/navio"
Insert cell
viewof selectedTable = navio(tableData)
Insert cell
groupedData = d3.rollup(
selectedTable,
(v) => v.length,
(d) => d.task,
(d) => d.category
)
Insert cell
treeData = d3.hierarchy(groupedData)
Insert cell
treeData.leaves()
Insert cell
height = 400
Insert cell
layTree = d3.tree().size([width, height])
Insert cell
layedOutTreeData = layTree(treeData)
Insert cell
layedOutTreeData.links()
Insert cell
tableWithUrls = FileAttachment("task_catogeries_urls@1.csv").csv()
Insert cell
imageMap = new Map(
tableWithUrls.map((d) => [
`${d.task.trim()}|||${d.category.trim()}`, // Composite key
[d.url_1, d.url_2, d.url_3, d.url_4, d.url_5]
])
)
Insert cell
{
const radius = 550;
const padding = 120;

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

const svg = d3
.create("svg")
.attr("viewBox", [
-radius - padding,
-radius - padding,
(radius + padding) * 2,
(radius + padding) * 2
])
.style("font", "14px sans-serif") // default larger text
.style("user-select", "none");

// Draw links
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)
);

// Create node groups
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()]);

// Highlight circles
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"
);

// Highlight text
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) ? "20px" : "14px"));

// Highlight links
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
);
})
.on("mouseout", function () {
// Reset circles
allNodes
.selectAll("circle")
.transition()
.duration(200)
.attr("r", 2.5)
.attr("fill", (d) => (d.children ? "#555" : "#999"));

// Reset text
allNodes
.selectAll("text")
.transition()
.duration(200)
.style("font-weight", "normal")
.style("fill", "#333")
.style("font-size", "14px");

// Reset links
allLinks
.transition()
.duration(200)
.attr("stroke", "#ccc")
.attr("stroke-width", 1);
});

// Add node visuals
allNodes
.append("circle")
.attr("r", 2.5)
.attr("fill", (d) => (d.children ? "#555" : "#999"));

// Add labels
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) => d.data[0])
.style("fill", "#333")
.style("font-size", "14px");

return svg.node();
}
Insert cell
// html`<div id="image-viewer" style="
// position: absolute;
// left: 0;
// top: 0;
// width: 400px;
// padding: 10px;
// display: flex;
// flex-direction: column;
// gap: 6px;
// background: #000;
// "></div>`
Insert cell
radio_dendrogram_1 = {
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);

// Capitalize helper
function toTitleCase(str) {
return str
?.toLowerCase()
.split(/[\s/_-]+/)
.map((w) => w.charAt(0).toUpperCase() + w.slice(1))
.join(" ");
}

// Image Viewer
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>`;

// Create SVG
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);

urls.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>`;
}
Insert cell
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);

// Capitalize helper
function toTitleCase(str) {
return str
?.toLowerCase()
.split(/[\s/_-]+/)
.map((w) => w.charAt(0).toUpperCase() + w.slice(1))
.join(" ");
}

// Image Viewer
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>`;

// Create SVG
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>`;
}
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