code / public /Figure_13_interactive.html
laura.wagner
added tag hieararchy
f8cb255
Raw
History Blame Contribute Delete
13.2 kB
<!DOCTYPE html>
<html lang="en" class="light-mode">
<head>
<meta charset="UTF-8">
<title>Danbooru Tag Taxonomy</title>
<script src="https://d3js.org/d3.v7.min.js"></script>
<link rel="stylesheet" href="styles.css">
<style>
/* Tree-specific styles (not in shared styles.css) */
html, body { overflow: auto; }
svg { position: static; margin-top: 0; z-index: auto; }
.tree-svg {
width: 100%;
overflow: visible;
display: block;
}
.link {
fill: none;
stroke: #ccc;
stroke-width: 1.5px;
}
.node circle {
stroke: #555;
stroke-width: 1.5px;
cursor: pointer;
}
.node.leaf circle {
cursor: default;
}
.node text {
font-size: 12px;
fill: #333;
pointer-events: none;
}
.node.leaf text {
font-size: 10px;
fill: #666;
}
.node:hover circle {
stroke: #000;
stroke-width: 2.5px;
}
.tree-controls {
margin: 0.5em 0 1em 0;
}
.tree-controls button {
margin-right: 6px;
padding: 6px 14px;
cursor: pointer;
background-color: rgba(138, 43, 226, 0.7);
color: white;
border: none;
border-radius: 4px;
transition: background-color 0.3s;
}
.tree-controls button:hover {
background-color: rgba(138, 43, 226, 0.9);
}
</style>
</head>
<body>
<!-- Header Overlay -->
<div class="header-overlay">
<div class="visualization-container">
<h1>Danbooru Tag Taxonomy</h1>
<h2 class="caption">Interactive hierarchical tree of Danbooru's tag classification system (~39,000 tags across 200 categories)</h2>
<div class="nav-buttons"></div>
<button class="nav-button" onclick="location.href='index.html'">Home</button>
<button class="nav-button" onclick="location.href='figure_15.html'">Next: Textual Training Data</button>
<div class="tree-controls">
<button id="expandAllBtn">Expand All Categories</button>
<button id="collapseAllBtn">Collapse All</button>
<button id="downloadBtn">Download SVG</button>
</div>
</div>
</div>
<svg class="tree-svg"></svg>
<script>
// Default to light mode
document.documentElement.classList.add('light-mode');
d3.json("json/danbooru_flat_full.json").then(function(data) {
// ---- CONFIG ----
const margin = { top: 20, right: 250, bottom: 20, left: 80 };
const nodeWidth = 220;
const duration = 400;
const svg = d3.select("svg.tree-svg");
const g = svg.append("g")
.attr("transform", `translate(${margin.left},${margin.top})`);
const linkGroup = g.append("g").attr("class", "links");
const nodeGroup = g.append("g").attr("class", "nodes");
// ---- STEP 0: Distinct color per top-level category ----
const categoryColors = {
"attire_and_body_accessories": "#e6194b",
"body": "#f58231",
"characters": "#3cb44b",
"copyrights_artists_projects_and_media": "#4363d8",
"creatures": "#42d4f4",
"drawing_software": "#911eb4",
"games": "#f032e6",
"metatags": "#bfef45",
"more": "#9A6324",
"objects": "#e6beff",
"plant": "#006400",
"real_world": "#ffd8b1",
"sex": "#ff69b4",
"visual_characteristics": "#000075",
"subject": "#ffe119",
"uncategorized": "#a9a9a9",
"actions_and_expressions": "#800000",
"objects_and_backgrounds": "#469990",
};
function assignColors(node, inheritedColor) {
if (inheritedColor) node.color = inheritedColor;
if (node.children) node.children.forEach(child => assignColors(child, node.color));
}
if (data.children) {
data.children.forEach(child => {
child.color = categoryColors[child.name] || "#888";
assignColors(child, child.color);
});
}
// ---- Pre-process: sort + convert tags to leaf nodes ----
function preprocess(node) {
if (node.children && node.children.length > 0) {
node.children.forEach(preprocess);
node.children.sort((a, b) => (b.tag_count || 0) - (a.tag_count || 0));
}
else if (node.tags && node.tags.length > 0) {
node.children = node.tags
.filter(t => t !== "...")
.map(tag => ({
name: tag,
_isTag: true,
color: node.color,
tag_count: 0,
category_count: 0
}));
node.children.sort((a, b) => a.name.localeCompare(b.name));
}
return node;
}
preprocess(data);
function getColor(d) {
return d.data.color || "#888";
}
// ---- Build hierarchy ----
const root = d3.hierarchy(data, d => d.children);
root.x0 = 0;
root.y0 = 0;
let idCounter = 0;
root.each(d => { d.id = ++idCounter; });
// ---- Size scale for tag_count → radius ----
const allTagCounts = root.descendants()
.filter(d => d.depth > 0 && !d.data._isTag)
.map(d => d.data.tag_count || 0);
const sizeScale = d3.scaleSqrt()
.domain([0, d3.max(allTagCounts)])
.range([4, 18]);
function nodeRadius(d) {
if (d.data._isTag) return 2.5;
if (d.depth === 0) return 8;
return sizeScale(d.data.tag_count || 0);
}
function collapse(d) {
if (d.children) {
d._children = d.children;
d._children.forEach(collapse);
d.children = null;
}
}
function expandCategories(d) {
if (d._children) {
if (!d._children[0].data._isTag) {
d.children = d._children;
d._children = null;
}
}
if (d.children) d.children.forEach(expandCategories);
}
function expandAll(d) {
if (d._children) {
d.children = d._children;
d._children = null;
}
if (d.children) d.children.forEach(expandAll);
}
// Collapse everything below root initially
if (root.children) {
root.children.forEach(collapse);
}
function computeTreeLayout() {
const leaves = root.descendants().filter(d =>
!d.children || d.children.length === 0
);
const visibleCount = leaves.length;
const rowHeight = 18;
const treeHeight = Math.max(300, visibleCount * rowHeight);
const treeLayout = d3.tree()
.size([treeHeight, 0])
.separation((a, b) => {
if (a.data._isTag && b.data._isTag) return 0.6;
return a.parent === b.parent ? 1 : 1.3;
});
treeLayout(root);
root.each(d => { d.y = d.depth * nodeWidth; });
const maxDepth = d3.max(root.descendants(), d => d.depth) || 0;
const totalWidth = margin.left + margin.right + (maxDepth + 1) * nodeWidth;
const totalHeight = margin.top + margin.bottom + treeHeight;
svg.attr("width", totalWidth)
.attr("height", totalHeight)
.attr("viewBox", `0 0 ${totalWidth} ${totalHeight}`);
}
// ---- MAIN UPDATE FUNCTION ----
function update(source) {
computeTreeLayout();
const nodes = root.descendants();
const links = root.links();
// Links
const link = linkGroup.selectAll("path.link")
.data(links, d => d.target.id);
const linkEnter = link.enter()
.append("path")
.attr("class", "link")
.attr("d", () => {
const o = { x: source.x0, y: source.y0 };
return diagonal({ source: o, target: o });
});
linkEnter.merge(link).transition().duration(duration)
.attr("d", d => diagonal(d))
.attr("stroke", d => d.target.data.color || "#ccc")
.attr("stroke-opacity", 0.5);
link.exit().transition().duration(duration)
.attr("d", () => {
const o = { x: source.x, y: source.y };
return diagonal({ source: o, target: o });
})
.remove();
// Nodes
const node = nodeGroup.selectAll("g.node")
.data(nodes, d => d.id);
const nodeEnter = node.enter()
.append("g")
.attr("class", d => d.data._isTag ? "node leaf" : "node")
.attr("transform", () => `translate(${source.y0},${source.x0})`)
.on("click", (event, d) => {
if (d.data._isTag) return;
if (d.children) {
d._children = d.children;
d.children = null;
} else if (d._children) {
d.children = d._children;
d._children = null;
}
update(d);
});
nodeEnter.append("circle").attr("r", 1e-6);
nodeEnter.append("text")
.attr("dy", "0.32em")
.attr("x", d => nodeRadius(d) + 4)
.text(d => {
if (d.data._isTag) return d.data.name;
const count = d.data.tag_count || 0;
return count > 0 ? `${d.data.name} (${count})` : d.data.name;
});
nodeEnter.append("title");
const nodeUpdate = nodeEnter.merge(node);
nodeUpdate.transition().duration(duration)
.attr("transform", d => `translate(${d.y},${d.x})`);
nodeUpdate.select("circle")
.attr("r", d => nodeRadius(d))
.style("fill", d => {
if (d.data._isTag) return getColor(d);
return (d._children && d._children.length) ? getColor(d) : "#fff";
})
.style("stroke", d => getColor(d));
nodeUpdate.select("text")
.style("fill-opacity", 1)
.style("font-weight", d => {
if (d.data._isTag) return "normal";
return d.depth <= 1 ? "bold" : "normal";
})
.style("font-size", d => d.data._isTag ? "10px" : "12px")
.style("fill", d => d.data._isTag ? "#666" : "#333");
nodeUpdate.select("title")
.text(d => {
if (d.data._isTag) return d.data.name;
const parts = [d.data.name];
if (d.data.tag_count) parts.push(`Tags: ${d.data.tag_count}`);
if (d.data.category_count) parts.push(`Sub-categories: ${d.data.category_count}`);
const hidden = d._children ? d._children.length : 0;
if (hidden) {
const type = d._children[0].data._isTag ? "tags" : "children";
parts.push(`Click to expand (${hidden} ${type})`);
} else if (d.children && d.children.length) {
parts.push("Click to collapse");
}
return parts.join("\n");
});
const nodeExit = node.exit().transition().duration(duration)
.attr("transform", () => `translate(${source.y},${source.x})`)
.remove();
nodeExit.select("circle").attr("r", 1e-6);
nodeExit.select("text").style("fill-opacity", 1e-6);
nodes.forEach(d => { d.x0 = d.x; d.y0 = d.y; });
}
function diagonal(d) {
return `M${d.source.y},${d.source.x}
C${(d.source.y + d.target.y) / 2},${d.source.x}
${(d.source.y + d.target.y) / 2},${d.target.x}
${d.target.y},${d.target.x}`;
}
// Initial render
update(root);
// Expand All (categories only)
document.getElementById("expandAllBtn").addEventListener("click", () => {
expandCategories(root);
update(root);
});
// Collapse All
document.getElementById("collapseAllBtn").addEventListener("click", () => {
if (root.children) root.children.forEach(collapse);
update(root);
});
// Download SVG
document.getElementById("downloadBtn").addEventListener("click", () => {
const svgNode = document.querySelector("svg");
const clonedSvg = svgNode.cloneNode(true);
clonedSvg.setAttribute("xmlns", "http://www.w3.org/2000/svg");
const styleEl = document.createElement("style");
styleEl.textContent = `
.link { fill: none; stroke: #ccc; stroke-width: 1.5px; }
.node circle { stroke-width: 1.5px; }
.node text { font-size: 12px; fill: #333; font-family: sans-serif; }
.node.leaf text { font-size: 10px; fill: #666; }
`;
clonedSvg.insertBefore(styleEl, clonedSvg.firstChild);
const svgData = new XMLSerializer().serializeToString(clonedSvg);
const svgBlob = new Blob([svgData], { type: "image/svg+xml;charset=utf-8" });
const url = URL.createObjectURL(svgBlob);
const a = document.createElement("a");
a.href = url;
a.download = "danbooru_tree_interactive.svg";
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
});
});
</script>
</body>
</html>