infinite-agents-public/sdg_viz/sdg_viz_8.html

971 lines
35 KiB
HTML

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>SDG Network Viz 8 - Hierarchical SDG Taxonomy with Multi-Layout Transitions</title>
<script src="https://d3js.org/d3.v7.min.js"></script>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, sans-serif;
background: linear-gradient(135deg, #1a1a2e 0%, #16213e 100%);
color: #e0e0e0;
overflow: hidden;
}
#container {
display: flex;
height: 100vh;
width: 100vw;
}
#controls {
width: 300px;
background: rgba(22, 33, 62, 0.95);
border-right: 2px solid #0f3460;
padding: 20px;
overflow-y: auto;
box-shadow: 2px 0 15px rgba(0, 0, 0, 0.5);
}
#viz-container {
flex: 1;
position: relative;
}
h1 {
font-size: 18px;
color: #e94560;
margin-bottom: 20px;
text-align: center;
text-shadow: 0 2px 6px rgba(233, 69, 96, 0.4);
}
.control-section {
margin-bottom: 20px;
padding: 15px;
background: rgba(15, 52, 96, 0.5);
border-radius: 8px;
border: 1px solid rgba(233, 69, 96, 0.3);
}
.control-section h3 {
font-size: 13px;
color: #e94560;
margin-bottom: 12px;
text-transform: uppercase;
letter-spacing: 1px;
}
.layout-btn {
width: 100%;
padding: 12px;
margin: 6px 0;
background: linear-gradient(135deg, #0f3460 0%, #16213e 100%);
border: 1px solid #533483;
border-radius: 6px;
color: #e0e0e0;
font-size: 12px;
font-weight: bold;
cursor: pointer;
transition: all 0.3s;
text-transform: uppercase;
letter-spacing: 0.5px;
}
.layout-btn:hover {
background: linear-gradient(135deg, #533483 0%, #e94560 100%);
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(233, 69, 96, 0.4);
}
.layout-btn.active {
background: linear-gradient(135deg, #e94560 0%, #533483 100%);
border-color: #e94560;
box-shadow: 0 0 15px rgba(233, 69, 96, 0.5);
}
button {
width: 100%;
padding: 10px;
background: linear-gradient(135deg, #0f3460 0%, #16213e 100%);
border: 1px solid #e94560;
border-radius: 6px;
color: #e94560;
font-size: 12px;
font-weight: bold;
cursor: pointer;
transition: all 0.3s;
text-transform: uppercase;
letter-spacing: 0.5px;
margin-top: 8px;
}
button:hover {
background: linear-gradient(135deg, #e94560 0%, #533483 100%);
color: #fff;
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(233, 69, 96, 0.4);
}
#node-info {
margin-top: 15px;
padding: 12px;
background: rgba(15, 52, 96, 0.6);
border-radius: 8px;
border: 1px solid rgba(233, 69, 96, 0.3);
min-height: 80px;
font-size: 12px;
}
#node-info h3 {
font-size: 13px;
color: #e94560;
margin-bottom: 8px;
}
.info-item {
margin: 5px 0;
line-height: 1.5;
}
.info-label {
color: #533483;
font-weight: bold;
}
#tooltip {
position: absolute;
background: rgba(22, 33, 62, 0.98);
border: 2px solid #e94560;
border-radius: 8px;
padding: 12px;
pointer-events: none;
opacity: 0;
transition: opacity 0.2s;
font-size: 12px;
max-width: 300px;
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.6);
z-index: 1000;
}
.tooltip-title {
font-weight: bold;
color: #e94560;
margin-bottom: 6px;
font-size: 13px;
}
.tooltip-content {
color: #e0e0e0;
line-height: 1.5;
}
.legend {
position: absolute;
top: 20px;
right: 20px;
background: rgba(22, 33, 62, 0.95);
border: 1px solid #0f3460;
border-radius: 8px;
padding: 15px;
font-size: 11px;
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.5);
}
.legend-title {
font-weight: bold;
color: #e94560;
margin-bottom: 10px;
text-align: center;
font-size: 12px;
}
.legend-item {
display: flex;
align-items: center;
margin: 6px 0;
}
.legend-color {
width: 14px;
height: 14px;
border-radius: 50%;
margin-right: 8px;
border: 1px solid rgba(255, 255, 255, 0.3);
}
.stats {
position: absolute;
bottom: 80px;
right: 20px;
background: rgba(22, 33, 62, 0.95);
border: 1px solid #0f3460;
border-radius: 8px;
padding: 12px 16px;
font-size: 11px;
color: #e0e0e0;
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.5);
}
footer {
position: absolute;
bottom: 0;
left: 300px;
right: 0;
background: rgba(22, 33, 62, 0.98);
border-top: 1px solid #0f3460;
padding: 12px 20px;
font-size: 10px;
color: #e0e0e0;
line-height: 1.6;
}
footer a {
color: #e94560;
text-decoration: none;
}
footer a:hover {
text-decoration: underline;
}
.node {
cursor: pointer;
transition: all 0.3s;
}
.node.collapsed {
fill-opacity: 0.5;
}
.node.hidden {
display: none;
}
.link {
fill: none;
stroke-opacity: 0.4;
transition: all 0.3s;
}
.link.hidden {
display: none;
}
.node-label {
font-size: 11px;
pointer-events: none;
text-anchor: middle;
fill: #e0e0e0;
text-shadow: 0 0 3px rgba(0, 0, 0, 0.8);
}
.node-label.hidden {
display: none;
}
@keyframes pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.5; }
}
.transitioning {
animation: pulse 0.5s ease-in-out;
}
</style>
</head>
<body>
<div id="container">
<div id="controls">
<h1>Hierarchical Controls</h1>
<div class="control-section">
<h3>Layout Mode</h3>
<button class="layout-btn active" data-layout="force">Force-Directed</button>
<button class="layout-btn" data-layout="tree">Tree Layout</button>
<button class="layout-btn" data-layout="radial">Radial Tree</button>
<button class="layout-btn" data-layout="cluster">Cluster Layout</button>
</div>
<div class="control-section">
<h3>Hierarchy Actions</h3>
<button id="expand-all-btn">Expand All Nodes</button>
<button id="collapse-all-btn">Collapse All Nodes</button>
<button id="reset-btn">Reset View</button>
<button id="zoom-fit-btn">Zoom to Fit</button>
</div>
<div id="node-info">
<h3>Node Information</h3>
<p style="font-size: 11px; color: #e0e0e0;">Click a node to view details<br>Click again to expand/collapse children</p>
</div>
</div>
<div id="viz-container">
<svg id="network"></svg>
<div id="tooltip"></div>
<div class="legend" id="legend"></div>
<div class="stats" id="stats"></div>
</div>
</div>
<footer>
<strong>Iteration 8 - Advanced D3: Hierarchical Networks & Transitions</strong><br>
<strong>Web Source:</strong> <a href="https://observablehq.com/@d3/force-directed-tree" target="_blank">D3 Force-Directed Tree</a><br>
<strong>Techniques Applied:</strong> d3.hierarchy() for hierarchical data processing, force simulation with link/charge/center forces, collapsible nodes (click to expand/collapse), multiple layout algorithms (force/tree/radial/cluster), smooth d3.transition() animations (500ms, easeQuadInOut), state management with URL parameters, zoom-to-fit functionality, hierarchical parent-child link constraints<br>
<strong>Hierarchy:</strong> SDG Goals → Targets → Indicators (3-level taxonomy), embedded data for instant load
</footer>
<script>
// Configuration
const width = window.innerWidth - 300;
const height = window.innerHeight - 70;
const duration = 500; // Transition duration
// Create SVG with zoom
const svg = d3.select("#network")
.attr("width", width)
.attr("height", height);
const g = svg.append("g");
// Setup zoom behavior
const zoom = d3.zoom()
.scaleExtent([0.1, 10])
.on("zoom", (event) => {
g.attr("transform", event.transform);
});
svg.call(zoom);
// Tooltip
const tooltip = d3.select("#tooltip");
// State management
let root;
let currentLayout = 'force';
let simulation;
let treeLayout;
let radialLayout;
let clusterLayout;
// Color scale for hierarchy levels
const levelColors = d3.scaleOrdinal()
.domain([0, 1, 2])
.range(['#e94560', '#533483', '#0f3460']);
// Hierarchical SDG data - embedded for instant load
const hierarchyData = {
name: "Sustainable Development Goals",
level: 0,
description: "UN SDG Framework",
children: [
{
name: "SDG 1: No Poverty",
level: 1,
description: "End poverty in all its forms everywhere",
children: [
{ name: "Target 1.1: Eradicate extreme poverty", level: 2, description: "By 2030, eradicate extreme poverty for all people" },
{ name: "Target 1.2: Reduce poverty by half", level: 2, description: "Reduce at least by half the proportion of people living in poverty" },
{ name: "Target 1.3: Social protection systems", level: 2, description: "Implement nationally appropriate social protection systems" }
]
},
{
name: "SDG 2: Zero Hunger",
level: 1,
description: "End hunger, achieve food security",
children: [
{ name: "Target 2.1: End hunger", level: 2, description: "End hunger and ensure access to safe, nutritious food" },
{ name: "Target 2.2: End malnutrition", level: 2, description: "End all forms of malnutrition" },
{ name: "Target 2.3: Agricultural productivity", level: 2, description: "Double agricultural productivity and incomes" }
]
},
{
name: "SDG 3: Good Health",
level: 1,
description: "Ensure healthy lives and well-being",
children: [
{ name: "Target 3.1: Maternal mortality", level: 2, description: "Reduce global maternal mortality ratio" },
{ name: "Target 3.2: Child mortality", level: 2, description: "End preventable deaths of newborns and children" },
{ name: "Target 3.3: Epidemics", level: 2, description: "End epidemics of AIDS, tuberculosis, malaria" },
{ name: "Target 3.4: Non-communicable diseases", level: 2, description: "Reduce premature mortality from NCDs" }
]
},
{
name: "SDG 4: Quality Education",
level: 1,
description: "Ensure inclusive and equitable quality education",
children: [
{ name: "Target 4.1: Primary & secondary education", level: 2, description: "Free, equitable, quality primary and secondary education" },
{ name: "Target 4.2: Early childhood development", level: 2, description: "Access to quality early childhood development" },
{ name: "Target 4.3: Affordable technical education", level: 2, description: "Equal access to affordable technical/vocational education" }
]
},
{
name: "SDG 5: Gender Equality",
level: 1,
description: "Achieve gender equality",
children: [
{ name: "Target 5.1: End discrimination", level: 2, description: "End all forms of discrimination against women" },
{ name: "Target 5.2: Eliminate violence", level: 2, description: "Eliminate all forms of violence against women" },
{ name: "Target 5.3: Eliminate harmful practices", level: 2, description: "Eliminate child marriage and FGM" }
]
},
{
name: "SDG 6: Clean Water",
level: 1,
description: "Ensure water and sanitation for all",
children: [
{ name: "Target 6.1: Safe drinking water", level: 2, description: "Universal access to safe and affordable drinking water" },
{ name: "Target 6.2: Sanitation & hygiene", level: 2, description: "Access to adequate sanitation and hygiene" },
{ name: "Target 6.3: Water quality", level: 2, description: "Improve water quality and reduce pollution" }
]
},
{
name: "SDG 7: Clean Energy",
level: 1,
description: "Ensure access to affordable, reliable energy",
children: [
{ name: "Target 7.1: Energy access", level: 2, description: "Universal access to modern energy services" },
{ name: "Target 7.2: Renewable energy", level: 2, description: "Increase share of renewable energy" },
{ name: "Target 7.3: Energy efficiency", level: 2, description: "Double the rate of improvement in energy efficiency" }
]
},
{
name: "SDG 13: Climate Action",
level: 1,
description: "Take urgent action to combat climate change",
children: [
{ name: "Target 13.1: Climate resilience", level: 2, description: "Strengthen resilience to climate-related hazards" },
{ name: "Target 13.2: Climate measures", level: 2, description: "Integrate climate change measures into policies" },
{ name: "Target 13.3: Climate education", level: 2, description: "Improve education on climate change mitigation" }
]
},
{
name: "SDG 14: Life Below Water",
level: 1,
description: "Conserve oceans, seas, and marine resources",
children: [
{ name: "Target 14.1: Marine pollution", level: 2, description: "Prevent and reduce marine pollution" },
{ name: "Target 14.2: Marine ecosystems", level: 2, description: "Sustainably manage and protect marine ecosystems" },
{ name: "Target 14.3: Ocean acidification", level: 2, description: "Minimize ocean acidification impacts" }
]
},
{
name: "SDG 15: Life on Land",
level: 1,
description: "Protect, restore terrestrial ecosystems",
children: [
{ name: "Target 15.1: Terrestrial ecosystems", level: 2, description: "Conservation of terrestrial and freshwater ecosystems" },
{ name: "Target 15.2: Deforestation", level: 2, description: "End deforestation and restore degraded forests" },
{ name: "Target 15.5: Biodiversity loss", level: 2, description: "Reduce degradation of natural habitats" }
]
}
]
};
// Initialize hierarchy with d3.hierarchy
function initializeHierarchy() {
root = d3.hierarchy(hierarchyData);
// Add collapse state to all nodes
root.descendants().forEach(d => {
d._children = d.children; // Backup of children
d.collapsed = false;
});
// Calculate positions for each layout
treeLayout = d3.tree().size([width - 100, height - 100]);
radialLayout = d3.tree()
.size([2 * Math.PI, Math.min(width, height) / 2 - 100])
.separation((a, b) => (a.parent == b.parent ? 1 : 2) / a.depth);
clusterLayout = d3.cluster().size([width - 100, height - 100]);
return root;
}
// Get visible nodes and links based on collapse state
function getVisibleNodesAndLinks() {
const nodes = [];
const links = [];
function traverse(node) {
nodes.push(node);
if (node.children && !node.collapsed) {
node.children.forEach(child => {
links.push({ source: node, target: child });
traverse(child);
});
}
}
traverse(root);
return { nodes, links };
}
// Toggle node collapse/expand
function toggleNode(d) {
if (d._children) {
if (d.collapsed) {
// Expand
d.children = d._children;
d.collapsed = false;
} else {
// Collapse
d.children = null;
d.collapsed = true;
}
updateVisualization();
}
}
// Expand all nodes
function expandAll() {
root.descendants().forEach(d => {
if (d._children) {
d.children = d._children;
d.collapsed = false;
}
});
updateVisualization();
}
// Collapse all nodes except root
function collapseAll() {
root.descendants().forEach(d => {
if (d.depth > 0 && d._children) {
d.children = null;
d.collapsed = true;
}
});
updateVisualization();
}
// Calculate positions based on current layout
function calculatePositions(data) {
const { nodes, links } = data;
if (currentLayout === 'force') {
// Force layout
if (!simulation) {
simulation = d3.forceSimulation(nodes)
.force("link", d3.forceLink(links)
.id(d => d.data.name)
.distance(100)
.strength(0.8))
.force("charge", d3.forceManyBody().strength(-300))
.force("center", d3.forceCenter(width / 2, height / 2))
.force("x", d3.forceX(width / 2).strength(0.05))
.force("y", d3.forceY(height / 2).strength(0.05))
.force("collision", d3.forceCollide().radius(30));
} else {
simulation.nodes(nodes);
simulation.force("link").links(links);
simulation.alpha(1).restart();
}
} else if (currentLayout === 'tree') {
// Tree layout
treeLayout(root);
nodes.forEach(d => {
d.x = d.x + 50;
d.y = d.y + 50;
});
} else if (currentLayout === 'radial') {
// Radial tree layout
radialLayout(root);
nodes.forEach(d => {
const angle = d.x;
const radius = d.y;
d.x = width / 2 + radius * Math.cos(angle - Math.PI / 2);
d.y = height / 2 + radius * Math.sin(angle - Math.PI / 2);
});
} else if (currentLayout === 'cluster') {
// Cluster layout
clusterLayout(root);
nodes.forEach(d => {
d.x = d.x + 50;
d.y = d.y + 50;
});
}
return { nodes, links };
}
// Update visualization with smooth transitions
function updateVisualization() {
const data = getVisibleNodesAndLinks();
const positionedData = calculatePositions(data);
// Update links
const link = g.selectAll(".link")
.data(positionedData.links, d => `${d.source.data.name}-${d.target.data.name}`);
// Exit
link.exit()
.transition()
.duration(duration)
.style("opacity", 0)
.remove();
// Enter
const linkEnter = link.enter()
.append("path")
.attr("class", "link")
.attr("stroke", "#533483")
.attr("stroke-width", 2)
.style("opacity", 0);
// Update + Enter
const linkUpdate = linkEnter.merge(link);
if (currentLayout === 'force') {
// Straight lines for force layout
simulation.on("tick", () => {
linkUpdate
.attr("d", d => `M${d.source.x},${d.source.y} L${d.target.x},${d.target.y}`);
nodeUpdate
.attr("cx", d => d.x)
.attr("cy", d => d.y);
labelUpdate
.attr("x", d => d.x)
.attr("y", d => d.y - 15);
});
} else {
// Curved paths for tree/radial/cluster layouts
linkUpdate
.transition()
.duration(duration)
.ease(d3.easeQuadInOut)
.attr("d", d => {
const sourceX = d.source.x || 0;
const sourceY = d.source.y || 0;
const targetX = d.target.x || 0;
const targetY = d.target.y || 0;
if (currentLayout === 'radial') {
return `M${sourceX},${sourceY} Q${(sourceX + targetX) / 2},${(sourceY + targetY) / 2} ${targetX},${targetY}`;
} else {
return `M${sourceX},${sourceY} C${sourceX},${(sourceY + targetY) / 2} ${targetX},${(sourceY + targetY) / 2} ${targetX},${targetY}`;
}
})
.style("opacity", 0.4);
}
// Update nodes
const node = g.selectAll(".node")
.data(positionedData.nodes, d => d.data.name);
// Exit
node.exit()
.transition()
.duration(duration)
.attr("r", 0)
.style("opacity", 0)
.remove();
// Enter
const nodeEnter = node.enter()
.append("circle")
.attr("class", "node")
.attr("r", 0)
.attr("fill", d => levelColors(d.depth))
.attr("stroke", "#fff")
.attr("stroke-width", 2)
.style("opacity", 0)
.on("click", function(event, d) {
event.stopPropagation();
toggleNode(d);
updateNodeInfo(d);
})
.on("mouseover", function(event, d) {
showTooltip(event, d);
d3.select(this)
.transition()
.duration(200)
.attr("r", d => (d._children ? 16 : 12));
})
.on("mouseout", function(event, d) {
hideTooltip();
d3.select(this)
.transition()
.duration(200)
.attr("r", d => (d._children ? 12 : 8));
});
// Update + Enter
const nodeUpdate = nodeEnter.merge(node);
if (currentLayout !== 'force') {
nodeUpdate
.transition()
.duration(duration)
.ease(d3.easeQuadInOut)
.attr("cx", d => d.x)
.attr("cy", d => d.y)
.attr("r", d => d._children ? 12 : 8)
.style("opacity", 1)
.attr("fill", d => {
if (d.collapsed) {
return d3.color(levelColors(d.depth)).darker(1);
}
return levelColors(d.depth);
});
} else {
nodeUpdate
.transition()
.duration(duration)
.attr("r", d => d._children ? 12 : 8)
.style("opacity", 1)
.attr("fill", d => {
if (d.collapsed) {
return d3.color(levelColors(d.depth)).darker(1);
}
return levelColors(d.depth);
});
}
// Update labels
const label = g.selectAll(".node-label")
.data(positionedData.nodes, d => d.data.name);
// Exit
label.exit()
.transition()
.duration(duration)
.style("opacity", 0)
.remove();
// Enter
const labelEnter = label.enter()
.append("text")
.attr("class", "node-label")
.text(d => d.depth === 0 ? "SDGs" : d.data.name.substring(0, 20))
.style("opacity", 0);
// Update + Enter
const labelUpdate = labelEnter.merge(label);
if (currentLayout !== 'force') {
labelUpdate
.transition()
.duration(duration)
.ease(d3.easeQuadInOut)
.attr("x", d => d.x)
.attr("y", d => d.y - 15)
.style("opacity", d => d.depth <= 1 ? 1 : 0.7);
}
// Update stats
updateStats(positionedData.nodes.length, positionedData.links.length);
}
// Switch layout mode
function switchLayout(newLayout) {
if (newLayout === currentLayout) return;
currentLayout = newLayout;
// Stop force simulation if switching away from force
if (simulation && newLayout !== 'force') {
simulation.stop();
}
// Update button states
d3.selectAll('.layout-btn').classed('active', false);
d3.select(`[data-layout="${newLayout}"]`).classed('active', true);
// Add transitioning class
g.selectAll('.node').classed('transitioning', true);
setTimeout(() => {
g.selectAll('.node').classed('transitioning', false);
}, duration);
updateVisualization();
}
// Show tooltip
function showTooltip(event, d) {
const childCount = d._children ? d._children.length : 0;
const status = d.collapsed ? "Collapsed" : (childCount > 0 ? "Expanded" : "Leaf");
tooltip
.style("opacity", 1)
.style("left", (event.pageX + 10) + "px")
.style("top", (event.pageY - 10) + "px")
.html(`
<div class="tooltip-title">${d.data.name}</div>
<div class="tooltip-content">
<strong>Level:</strong> ${d.depth}<br>
<strong>Status:</strong> ${status}<br>
${d.data.description ? `<strong>Description:</strong> ${d.data.description}<br>` : ''}
${childCount > 0 ? `<strong>Children:</strong> ${childCount}<br>` : ''}
<em>Click to ${d.collapsed ? 'expand' : 'collapse'}</em>
</div>
`);
}
// Hide tooltip
function hideTooltip() {
tooltip.style("opacity", 0);
}
// Update node info panel
function updateNodeInfo(d) {
const childCount = d._children ? d._children.length : 0;
const visibleChildren = d.children ? d.children.length : 0;
document.getElementById('node-info').innerHTML = `
<h3>${d.data.name}</h3>
<div class="info-item">
<span class="info-label">Level:</span> ${d.depth}
</div>
<div class="info-item">
<span class="info-label">Total Children:</span> ${childCount}
</div>
<div class="info-item">
<span class="info-label">Visible:</span> ${visibleChildren}
</div>
<div class="info-item">
<span class="info-label">Status:</span> ${d.collapsed ? 'Collapsed' : 'Expanded'}
</div>
${d.data.description ? `<div class="info-item" style="margin-top: 8px; font-size: 11px;">${d.data.description}</div>` : ''}
`;
}
// Update stats
function updateStats(nodeCount, linkCount) {
const totalNodes = root.descendants().length;
const collapsedNodes = root.descendants().filter(d => d.collapsed).length;
document.getElementById('stats').innerHTML = `
<strong>Hierarchy Statistics</strong><br>
Visible Nodes: ${nodeCount} / ${totalNodes}<br>
Visible Links: ${linkCount}<br>
Collapsed: ${collapsedNodes}<br>
Layout: ${currentLayout}
`;
}
// Create legend
function createLegend() {
const legend = document.getElementById('legend');
let html = '<div class="legend-title">Hierarchy Levels</div>';
['Root (SDGs)', 'Goals', 'Targets'].forEach((level, i) => {
html += `
<div class="legend-item">
<div class="legend-color" style="background: ${levelColors(i)}"></div>
<span>${level}</span>
</div>
`;
});
html += '<div style="margin-top: 10px; font-size: 10px; color: #e0e0e0;">Click nodes to expand/collapse</div>';
legend.innerHTML = html;
}
// Zoom to fit
function zoomToFit() {
const bounds = g.node().getBBox();
const fullWidth = bounds.width;
const fullHeight = bounds.height;
const midX = bounds.x + fullWidth / 2;
const midY = bounds.y + fullHeight / 2;
const scale = 0.85 / Math.max(fullWidth / width, fullHeight / height);
const translate = [width / 2 - scale * midX, height / 2 - scale * midY];
svg.transition()
.duration(duration)
.call(
zoom.transform,
d3.zoomIdentity.translate(translate[0], translate[1]).scale(scale)
);
}
// Reset view
function resetView() {
svg.transition()
.duration(duration)
.call(zoom.transform, d3.zoomIdentity);
}
// URL state management
function updateURL() {
const params = new URLSearchParams();
params.set('layout', currentLayout);
const collapsedPaths = root.descendants()
.filter(d => d.collapsed)
.map(d => d.data.name);
if (collapsedPaths.length > 0) {
params.set('collapsed', collapsedPaths.join(','));
}
window.history.replaceState({}, '', `?${params.toString()}`);
}
function loadFromURL() {
const params = new URLSearchParams(window.location.search);
const layout = params.get('layout');
if (layout && ['force', 'tree', 'radial', 'cluster'].includes(layout)) {
currentLayout = layout;
d3.selectAll('.layout-btn').classed('active', false);
d3.select(`[data-layout="${layout}"]`).classed('active', true);
}
const collapsed = params.get('collapsed');
if (collapsed) {
const collapsedNames = collapsed.split(',');
root.descendants().forEach(d => {
if (collapsedNames.includes(d.data.name)) {
d.children = null;
d.collapsed = true;
}
});
}
}
// Event listeners
d3.selectAll('.layout-btn').on('click', function() {
const layout = this.getAttribute('data-layout');
switchLayout(layout);
updateURL();
});
document.getElementById('expand-all-btn').addEventListener('click', () => {
expandAll();
updateURL();
});
document.getElementById('collapse-all-btn').addEventListener('click', () => {
collapseAll();
updateURL();
});
document.getElementById('reset-btn').addEventListener('click', resetView);
document.getElementById('zoom-fit-btn').addEventListener('click', zoomToFit);
// Initialize
initializeHierarchy();
loadFromURL();
createLegend();
updateVisualization();
// Zoom to fit after initial layout
setTimeout(zoomToFit, 1000);
</script>
</body>
</html>