infinite-agents-public/sdg_viz/sdg_viz_11.html

1101 lines
40 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>SDG Network Visualization 11 - Enhanced Side Panels & Information Design</title>
<script src="https://d3js.org/d3.v7.min.js"></script>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
background: linear-gradient(135deg, #1a1a2e 0%, #16213e 100%);
color: #fff;
overflow: hidden;
}
#container {
width: 100vw;
height: 100vh;
position: relative;
display: flex;
}
#main-view {
flex: 1;
position: relative;
}
svg {
width: 100%;
height: 100%;
cursor: grab;
}
svg:active {
cursor: grabbing;
}
/* Side Panel Styles */
#side-panel {
position: absolute;
right: -450px;
top: 0;
width: 450px;
height: 100vh;
background: linear-gradient(180deg, #0f1419 0%, #1a1f2e 100%);
border-left: 2px solid rgba(255, 255, 255, 0.1);
box-shadow: -5px 0 30px rgba(0, 0, 0, 0.5);
transition: right 0.3s cubic-bezier(0.4, 0, 0.2, 1);
z-index: 100;
overflow-y: auto;
overflow-x: hidden;
}
#side-panel.open {
right: 0;
}
#side-panel::-webkit-scrollbar {
width: 8px;
}
#side-panel::-webkit-scrollbar-track {
background: rgba(255, 255, 255, 0.05);
}
#side-panel::-webkit-scrollbar-thumb {
background: rgba(255, 255, 255, 0.2);
border-radius: 4px;
}
.panel-header {
padding: 30px;
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
position: relative;
}
.panel-close {
position: absolute;
top: 20px;
right: 20px;
width: 32px;
height: 32px;
background: rgba(255, 255, 255, 0.1);
border: 1px solid rgba(255, 255, 255, 0.2);
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
font-size: 18px;
transition: all 0.2s;
}
.panel-close:hover {
background: rgba(239, 83, 80, 0.3);
border-color: #ef5350;
transform: rotate(90deg);
}
.node-icon {
font-size: 48px;
margin-bottom: 15px;
}
.node-title {
font-size: 24px;
font-weight: 600;
margin-bottom: 10px;
color: #fff;
}
.node-badge {
display: inline-block;
padding: 6px 12px;
border-radius: 20px;
font-size: 11px;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.5px;
margin-bottom: 15px;
}
.badge-topic {
background: rgba(66, 133, 244, 0.2);
color: #4285f4;
border: 1px solid #4285f4;
}
.badge-datasource {
background: rgba(239, 83, 80, 0.2);
color: #ef5350;
border: 1px solid #ef5350;
}
.panel-section {
padding: 25px 30px;
border-bottom: 1px solid rgba(255, 255, 255, 0.05);
}
.section-title {
font-size: 13px;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 1px;
color: #888;
margin-bottom: 12px;
display: flex;
align-items: center;
gap: 8px;
}
.section-icon {
font-size: 16px;
}
.section-content {
color: #ccc;
line-height: 1.6;
font-size: 14px;
}
.connection-list {
list-style: none;
}
.connection-item {
padding: 12px;
background: rgba(255, 255, 255, 0.03);
border-left: 3px solid transparent;
margin-bottom: 8px;
border-radius: 4px;
cursor: pointer;
transition: all 0.2s;
display: flex;
align-items: center;
gap: 10px;
}
.connection-item:hover {
background: rgba(255, 255, 255, 0.08);
border-left-color: #4285f4;
transform: translateX(5px);
}
.connection-icon {
font-size: 20px;
}
.connection-name {
flex: 1;
font-size: 13px;
}
.connection-strength {
font-size: 11px;
color: #888;
background: rgba(255, 255, 255, 0.05);
padding: 2px 8px;
border-radius: 10px;
}
.metrics-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 15px;
}
.metric-card {
background: rgba(255, 255, 255, 0.03);
padding: 15px;
border-radius: 8px;
border: 1px solid rgba(255, 255, 255, 0.05);
}
.metric-label {
font-size: 11px;
color: #888;
text-transform: uppercase;
letter-spacing: 0.5px;
margin-bottom: 8px;
}
.metric-value {
font-size: 24px;
font-weight: 600;
color: #4285f4;
}
.link-list {
list-style: none;
}
.link-item {
margin-bottom: 10px;
}
.link-item a {
color: #4285f4;
text-decoration: none;
font-size: 13px;
display: flex;
align-items: center;
gap: 8px;
transition: all 0.2s;
}
.link-item a:hover {
color: #5a95f5;
transform: translateX(5px);
}
.link-icon {
font-size: 14px;
}
.action-buttons {
display: flex;
gap: 10px;
flex-wrap: wrap;
}
.action-btn {
flex: 1;
min-width: 120px;
padding: 10px 16px;
background: rgba(66, 133, 244, 0.1);
border: 1px solid #4285f4;
color: #4285f4;
border-radius: 6px;
cursor: pointer;
font-size: 12px;
font-weight: 600;
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
transition: all 0.2s;
}
.action-btn:hover {
background: rgba(66, 133, 244, 0.2);
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(66, 133, 244, 0.3);
}
.action-btn.secondary {
background: rgba(239, 83, 80, 0.1);
border-color: #ef5350;
color: #ef5350;
}
.action-btn.secondary:hover {
background: rgba(239, 83, 80, 0.2);
box-shadow: 0 4px 12px rgba(239, 83, 80, 0.3);
}
/* Network Styles */
.node {
cursor: pointer;
transition: opacity 0.2s;
}
.node circle {
stroke: #fff;
stroke-width: 2px;
}
.node.topic circle {
fill: #4285f4;
}
.node.datasource circle {
fill: #ef5350;
}
.node text {
font-size: 12px;
font-weight: 500;
pointer-events: none;
user-select: none;
}
.node.dimmed {
opacity: 0.2;
}
.link {
stroke: #555;
stroke-width: 2px;
stroke-opacity: 0.4;
}
.link.highlighted {
stroke: #ffd700;
stroke-width: 3px;
stroke-opacity: 0.8;
}
.link.dimmed {
stroke-opacity: 0.1;
}
/* Header */
#header {
position: absolute;
top: 0;
left: 0;
right: 0;
background: rgba(0, 0, 0, 0.7);
backdrop-filter: blur(10px);
padding: 20px 30px;
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
z-index: 10;
}
#header h1 {
font-size: 20px;
font-weight: 600;
margin-bottom: 5px;
}
#header p {
font-size: 13px;
color: #888;
}
/* Legend */
#legend {
position: absolute;
bottom: 20px;
left: 20px;
background: rgba(0, 0, 0, 0.8);
backdrop-filter: blur(10px);
padding: 20px;
border-radius: 8px;
border: 1px solid rgba(255, 255, 255, 0.1);
z-index: 10;
}
.legend-title {
font-size: 12px;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 1px;
color: #888;
margin-bottom: 15px;
}
.legend-item {
display: flex;
align-items: center;
gap: 10px;
margin-bottom: 10px;
font-size: 13px;
}
.legend-color {
width: 20px;
height: 20px;
border-radius: 50%;
border: 2px solid #fff;
}
/* Instructions */
#instructions {
position: absolute;
top: 100px;
right: 20px;
background: rgba(0, 0, 0, 0.8);
backdrop-filter: blur(10px);
padding: 20px;
border-radius: 8px;
border: 1px solid rgba(255, 255, 255, 0.1);
z-index: 10;
max-width: 280px;
}
.instruction-item {
display: flex;
align-items: start;
gap: 10px;
margin-bottom: 12px;
font-size: 12px;
color: #ccc;
}
.instruction-icon {
font-size: 16px;
margin-top: 2px;
}
/* Toast Notification */
#toast {
position: fixed;
bottom: 30px;
left: 50%;
transform: translateX(-50%) translateY(100px);
background: rgba(0, 0, 0, 0.95);
color: #fff;
padding: 15px 25px;
border-radius: 8px;
border: 1px solid rgba(66, 133, 244, 0.5);
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.5);
z-index: 1000;
opacity: 0;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
}
#toast.show {
opacity: 1;
transform: translateX(-50%) translateY(0);
}
</style>
</head>
<body>
<div id="container">
<div id="main-view">
<div id="header">
<h1>SDG Network: Topics ↔ Data Sources</h1>
<p>Bipartite visualization connecting SDG topics with real-world data sources</p>
</div>
<svg id="network"></svg>
<div id="legend">
<div class="legend-title">Node Types</div>
<div class="legend-item">
<div class="legend-color" style="background: #4285f4;"></div>
<span>Topics (Blue)</span>
</div>
<div class="legend-item">
<div class="legend-color" style="background: #ef5350;"></div>
<span>Data Sources (Red)</span>
</div>
</div>
<div id="instructions">
<div class="instruction-item">
<span class="instruction-icon">🖱️</span>
<span><strong>Click</strong> any node to view detailed information in the side panel</span>
</div>
<div class="instruction-item">
<span class="instruction-icon">👆</span>
<span><strong>Drag</strong> nodes to rearrange the network layout</span>
</div>
<div class="instruction-item">
<span class="instruction-icon">🔗</span>
<span><strong>Click connections</strong> in the side panel to highlight related nodes</span>
</div>
<div class="instruction-item">
<span class="instruction-icon">📋</span>
<span><strong>Copy API endpoints</strong> and download node data from action buttons</span>
</div>
</div>
</div>
<div id="side-panel">
<div class="panel-header">
<div class="panel-close" onclick="closeSidePanel()">×</div>
<div class="node-icon" id="panel-icon">📊</div>
<h2 class="node-title" id="panel-title">Node Title</h2>
<div class="node-badge badge-topic" id="panel-badge">Topic</div>
</div>
<div class="panel-section">
<div class="section-title">
<span class="section-icon">📝</span>
<span>Description</span>
</div>
<div class="section-content">
<p id="panel-description">Select a node to view its description.</p>
</div>
</div>
<div class="panel-section">
<div class="section-title">
<span class="section-icon">📊</span>
<span>Metrics</span>
</div>
<div class="metrics-grid" id="panel-metrics">
<!-- Metrics populated dynamically -->
</div>
</div>
<div class="panel-section">
<div class="section-title">
<span class="section-icon">🔗</span>
<span>Connections</span>
</div>
<ul class="connection-list" id="panel-connections">
<!-- Connections populated dynamically -->
</ul>
</div>
<div class="panel-section">
<div class="section-title">
<span class="section-icon">🌐</span>
<span>External Resources</span>
</div>
<ul class="link-list" id="panel-links">
<!-- Links populated dynamically -->
</ul>
</div>
<div class="panel-section">
<div class="section-title">
<span class="section-icon"></span>
<span>Actions</span>
</div>
<div class="action-buttons">
<button class="action-btn" onclick="copyEndpoint()">
<span>📋</span>
<span>Copy Endpoint</span>
</button>
<button class="action-btn" onclick="downloadNodeData()">
<span>⬇️</span>
<span>Download Data</span>
</button>
<button class="action-btn secondary" onclick="exploreRelated()">
<span>🔍</span>
<span>Explore Related</span>
</button>
</div>
</div>
</div>
</div>
<div id="toast"></div>
<script>
// ============================================================================
// DATA DEFINITION
// ============================================================================
const TOPICS = [
{
id: "t1",
name: "Climate Change Monitoring",
icon: "🌍",
description: "Track global climate indicators including temperature anomalies, CO2 levels, and extreme weather events. Essential for SDG 13 (Climate Action) and understanding environmental trends.",
sdgAlignment: "SDG 13, 14, 15",
dataTypes: ["Time Series", "Spatial", "Satellite"],
lastUpdated: "2025-10-08",
recordCount: "2.5M+"
},
{
id: "t2",
name: "Poverty & Economic Development",
icon: "💰",
description: "Analyze poverty rates, income distribution, and economic indicators across regions. Critical for measuring progress on SDG 1 (No Poverty) and SDG 10 (Reduced Inequalities).",
sdgAlignment: "SDG 1, 8, 10",
dataTypes: ["Economic", "Demographics", "Survey"],
lastUpdated: "2025-10-05",
recordCount: "1.8M+"
},
{
id: "t3",
name: "Health & Well-being",
icon: "🏥",
description: "Monitor disease prevalence, vaccination rates, healthcare access, and public health metrics worldwide. Core to SDG 3 (Good Health and Well-being) tracking.",
sdgAlignment: "SDG 3, 6",
dataTypes: ["Health Records", "Statistics", "Clinical"],
lastUpdated: "2025-10-09",
recordCount: "4.2M+"
},
{
id: "t4",
name: "Education Access & Quality",
icon: "📚",
description: "Examine enrollment rates, literacy levels, educational outcomes, and learning quality indicators globally. Fundamental to SDG 4 (Quality Education) assessment.",
sdgAlignment: "SDG 4, 5",
dataTypes: ["Educational", "Survey", "Performance"],
lastUpdated: "2025-10-07",
recordCount: "3.1M+"
},
{
id: "t5",
name: "Sustainable Cities & Infrastructure",
icon: "🏙️",
description: "Track urban development, infrastructure quality, public transportation, and sustainable city initiatives. Aligns with SDG 11 (Sustainable Cities and Communities).",
sdgAlignment: "SDG 11, 9",
dataTypes: ["Urban", "Infrastructure", "Geospatial"],
lastUpdated: "2025-10-06",
recordCount: "1.5M+"
},
{
id: "t6",
name: "Food Security & Agriculture",
icon: "🌾",
description: "Measure food availability, agricultural productivity, malnutrition rates, and sustainable farming practices. Essential for SDG 2 (Zero Hunger) monitoring.",
sdgAlignment: "SDG 2, 12, 15",
dataTypes: ["Agricultural", "Nutritional", "Production"],
lastUpdated: "2025-10-08",
recordCount: "2.9M+"
},
{
id: "t7",
name: "Clean Energy Transition",
icon: "⚡",
description: "Monitor renewable energy adoption, energy access rates, and carbon emissions from energy production. Critical for SDG 7 (Affordable and Clean Energy) progress.",
sdgAlignment: "SDG 7, 13",
dataTypes: ["Energy", "Production", "Consumption"],
lastUpdated: "2025-10-09",
recordCount: "2.1M+"
}
];
const DATA_SOURCES = [
{
id: "d1",
name: "World Bank Open Data",
icon: "🏦",
description: "Comprehensive global development database providing free access to economic, social, and environmental indicators for 217 countries. Updated quarterly with standardized metrics.",
apiEndpoint: "https://api.worldbank.org/v2/",
documentation: "https://datahelpdesk.worldbank.org/knowledgebase/articles/889392",
coverage: "Global (217 countries)",
updateFrequency: "Quarterly",
dataFormats: ["JSON", "XML", "CSV"],
apiKey: "Not Required"
},
{
id: "d2",
name: "NASA Earth Observations",
icon: "🛰️",
description: "Satellite-based environmental and climate data from NASA's Earth Science Division. Includes atmospheric, land, ocean, and cryosphere measurements with global coverage.",
apiEndpoint: "https://api.nasa.gov/planetary/earth/",
documentation: "https://api.nasa.gov/",
coverage: "Global Satellite Coverage",
updateFrequency: "Daily",
dataFormats: ["JSON", "GeoTIFF", "HDF5"],
apiKey: "Required (Free)"
},
{
id: "d3",
name: "WHO Global Health Observatory",
icon: "🌡️",
description: "World Health Organization's comprehensive health statistics covering disease burden, risk factors, health systems, and mortality data across 194 member states.",
apiEndpoint: "https://ghoapi.azureedge.net/api/",
documentation: "https://www.who.int/data/gho/info/gho-odata-api",
coverage: "194 WHO Member States",
updateFrequency: "Annual",
dataFormats: ["JSON", "XML", "CSV"],
apiKey: "Not Required"
},
{
id: "d4",
name: "UNESCO Institute for Statistics",
icon: "📖",
description: "Official source for internationally comparable education, science, culture, and communication statistics. Covers enrollment, literacy, research, and cultural indicators globally.",
apiEndpoint: "http://data.uis.unesco.org/",
documentation: "http://uis.unesco.org/en/uis-data-api",
coverage: "200+ Countries/Territories",
updateFrequency: "Annual",
dataFormats: ["SDMX", "CSV", "Excel"],
apiKey: "Not Required"
},
{
id: "d5",
name: "UN-Habitat Urban Data",
icon: "🏘️",
description: "Urban development indicators tracking city growth, slum populations, housing conditions, and sustainable urban planning metrics from the United Nations Human Settlements Programme.",
apiEndpoint: "https://data.unhabitat.org/api/",
documentation: "https://unhabitat.org/urban-data",
coverage: "Global Urban Areas",
updateFrequency: "Bi-annual",
dataFormats: ["JSON", "CSV", "GeoJSON"],
apiKey: "Required (Free)"
},
{
id: "d6",
name: "FAO Food Security Data",
icon: "🍽️",
description: "Food and Agriculture Organization's comprehensive database on global food production, trade, security, and agricultural practices. Essential for hunger and nutrition monitoring.",
apiEndpoint: "http://www.fao.org/faostat/en/#data",
documentation: "http://www.fao.org/faostat/en/#data",
coverage: "Global (245 Countries/Territories)",
updateFrequency: "Annual",
dataFormats: ["CSV", "Excel", "API"],
apiKey: "Not Required"
},
{
id: "d7",
name: "IEA Energy Statistics",
icon: "🔋",
description: "International Energy Agency's authoritative source for global energy data including production, consumption, prices, and renewable energy adoption across all major economies.",
apiEndpoint: "https://www.iea.org/data-and-statistics",
documentation: "https://www.iea.org/data-and-statistics/data-tools",
coverage: "Global (180+ Countries)",
updateFrequency: "Monthly/Annual",
dataFormats: ["CSV", "Excel", "JSON"],
apiKey: "Required (Paid)"
},
{
id: "d8",
name: "Global Forest Watch",
icon: "🌲",
description: "Real-time forest monitoring platform tracking deforestation, forest fires, and land use change using satellite imagery and AI analysis. Critical for environmental conservation.",
apiEndpoint: "https://data-api.globalforestwatch.org/",
documentation: "https://data-api.globalforestwatch.org/documentation",
coverage: "Global Forest Coverage",
updateFrequency: "Weekly",
dataFormats: ["JSON", "GeoJSON", "Shapefile"],
apiKey: "Not Required"
}
];
const CONNECTIONS = [
{ source: "t1", target: "d2", strength: "Strong" },
{ source: "t1", target: "d8", strength: "Strong" },
{ source: "t1", target: "d1", strength: "Medium" },
{ source: "t2", target: "d1", strength: "Strong" },
{ source: "t2", target: "d4", strength: "Medium" },
{ source: "t3", target: "d3", strength: "Strong" },
{ source: "t3", target: "d1", strength: "Medium" },
{ source: "t4", target: "d4", strength: "Strong" },
{ source: "t4", target: "d1", strength: "Medium" },
{ source: "t5", target: "d5", strength: "Strong" },
{ source: "t5", target: "d1", strength: "Medium" },
{ source: "t6", target: "d6", strength: "Strong" },
{ source: "t6", target: "d8", strength: "Medium" },
{ source: "t6", target: "d1", strength: "Medium" },
{ source: "t7", target: "d7", strength: "Strong" },
{ source: "t7", target: "d2", strength: "Medium" },
{ source: "t7", target: "d1", strength: "Medium" }
];
// ============================================================================
// VISUALIZATION SETUP
// ============================================================================
const width = window.innerWidth;
const height = window.innerHeight;
const svg = d3.select("#network")
.attr("viewBox", [0, 0, width, height]);
// Create graph data
const nodes = [...TOPICS.map(t => ({ ...t, type: "topic" })), ...DATA_SOURCES.map(d => ({ ...d, type: "datasource" }))];
const links = CONNECTIONS.map(c => ({ ...c }));
let currentNode = null;
// ============================================================================
// FORCE SIMULATION (NO entrance animations - start at alpha 0)
// ============================================================================
const simulation = d3.forceSimulation(nodes)
.force("link", d3.forceLink(links).id(d => d.id).distance(200).strength(0.5))
.force("charge", d3.forceManyBody().strength(-800))
.force("x", d3.forceX(d => d.type === "topic" ? width * 0.25 : width * 0.75).strength(0.3))
.force("y", d3.forceY(height / 2).strength(0.1))
.force("collision", d3.forceCollide().radius(60))
.alpha(0) // Start at 0 - NO entrance animation
.alphaTarget(0);
// ============================================================================
// RENDER LINKS
// ============================================================================
const link = svg.append("g")
.selectAll("line")
.data(links)
.join("line")
.attr("class", "link");
// ============================================================================
// RENDER NODES
// ============================================================================
const node = svg.append("g")
.selectAll("g")
.data(nodes)
.join("g")
.attr("class", d => `node ${d.type}`)
.call(d3.drag()
.on("start", dragstarted)
.on("drag", dragged)
.on("end", dragended))
.on("click", (event, d) => {
event.stopPropagation();
openSidePanel(d);
});
node.append("circle")
.attr("r", 40);
node.append("text")
.attr("dy", 60)
.attr("text-anchor", "middle")
.text(d => d.name)
.style("fill", "#fff")
.style("font-size", "13px")
.style("font-weight", "500");
node.append("text")
.attr("dy", 8)
.attr("text-anchor", "middle")
.text(d => d.icon)
.style("font-size", "28px");
// ============================================================================
// SIMULATION TICK
// ============================================================================
simulation.on("tick", () => {
link
.attr("x1", d => d.source.x)
.attr("y1", d => d.source.y)
.attr("x2", d => d.target.x)
.attr("y2", d => d.target.y);
node.attr("transform", d => `translate(${d.x},${d.y})`);
});
// Run simulation to initial positions
simulation.tick(300);
// ============================================================================
// DRAG HANDLERS (WORKING drag functionality)
// ============================================================================
function dragstarted(event, d) {
if (!event.active) simulation.alphaTarget(0.3).restart();
d.fx = d.x;
d.fy = d.y;
}
function dragged(event, d) {
d.fx = event.x;
d.fy = event.y;
}
function dragended(event, d) {
if (!event.active) simulation.alphaTarget(0);
d.fx = null;
d.fy = null;
}
// ============================================================================
// SIDE PANEL FUNCTIONS (300ms smooth transitions from web learning)
// ============================================================================
function openSidePanel(nodeData) {
currentNode = nodeData;
const panel = document.getElementById("side-panel");
// Update header
document.getElementById("panel-icon").textContent = nodeData.icon;
document.getElementById("panel-title").textContent = nodeData.name;
const badge = document.getElementById("panel-badge");
badge.textContent = nodeData.type === "topic" ? "Topic" : "Data Source";
badge.className = `node-badge ${nodeData.type === "topic" ? "badge-topic" : "badge-datasource"}`;
// Update description
document.getElementById("panel-description").textContent = nodeData.description;
// Update metrics
const metricsHtml = nodeData.type === "topic"
? `
<div class="metric-card">
<div class="metric-label">SDG Alignment</div>
<div class="metric-value" style="font-size: 14px; color: #4285f4;">${nodeData.sdgAlignment}</div>
</div>
<div class="metric-card">
<div class="metric-label">Data Types</div>
<div class="metric-value" style="font-size: 14px; color: #66bb6a;">${nodeData.dataTypes.length}</div>
</div>
<div class="metric-card">
<div class="metric-label">Record Count</div>
<div class="metric-value" style="font-size: 16px;">${nodeData.recordCount}</div>
</div>
<div class="metric-card">
<div class="metric-label">Last Updated</div>
<div class="metric-value" style="font-size: 14px; color: #ffb74d;">${nodeData.lastUpdated}</div>
</div>
`
: `
<div class="metric-card">
<div class="metric-label">Coverage</div>
<div class="metric-value" style="font-size: 13px; color: #ef5350;">${nodeData.coverage}</div>
</div>
<div class="metric-card">
<div class="metric-label">Update Frequency</div>
<div class="metric-value" style="font-size: 13px; color: #66bb6a;">${nodeData.updateFrequency}</div>
</div>
<div class="metric-card">
<div class="metric-label">Formats</div>
<div class="metric-value" style="font-size: 13px;">${nodeData.dataFormats.join(", ")}</div>
</div>
<div class="metric-card">
<div class="metric-label">API Key</div>
<div class="metric-value" style="font-size: 13px; color: ${nodeData.apiKey === "Not Required" ? "#66bb6a" : "#ffb74d"};">${nodeData.apiKey}</div>
</div>
`;
document.getElementById("panel-metrics").innerHTML = metricsHtml;
// Update connections with smooth reveal
const connectedIds = links
.filter(l => l.source.id === nodeData.id || l.target.id === nodeData.id)
.map(l => l.source.id === nodeData.id ? { node: l.target, strength: l.strength } : { node: l.source, strength: l.strength });
const connectionsHtml = connectedIds.map((conn, i) => `
<li class="connection-item" onclick="highlightConnection('${conn.node.id}')" style="animation-delay: ${i * 50}ms;">
<span class="connection-icon">${conn.node.icon}</span>
<span class="connection-name">${conn.node.name}</span>
<span class="connection-strength">${conn.strength}</span>
</li>
`).join("");
document.getElementById("panel-connections").innerHTML = connectionsHtml;
// Update external links
const linksHtml = nodeData.type === "datasource"
? `
<li class="link-item">
<a href="${nodeData.documentation}" target="_blank">
<span class="link-icon">📚</span>
<span>API Documentation</span>
</a>
</li>
<li class="link-item">
<a href="${nodeData.apiEndpoint}" target="_blank">
<span class="link-icon">🔌</span>
<span>API Endpoint</span>
</a>
</li>
`
: `
<li class="link-item">
<a href="https://sdgs.un.org/goals" target="_blank">
<span class="link-icon">🎯</span>
<span>UN SDG Goals</span>
</a>
</li>
<li class="link-item">
<a href="https://unstats.un.org/sdgs/" target="_blank">
<span class="link-icon">📊</span>
<span>SDG Indicators Database</span>
</a>
</li>
`;
document.getElementById("panel-links").innerHTML = linksHtml;
// Open panel with 300ms smooth transition (from web learning)
panel.classList.add("open");
// Highlight connected nodes
highlightConnections(nodeData);
}
function closeSidePanel() {
document.getElementById("side-panel").classList.remove("open");
clearHighlights();
}
function highlightConnections(nodeData) {
const connectedIds = new Set(
links
.filter(l => l.source.id === nodeData.id || l.target.id === nodeData.id)
.flatMap(l => [l.source.id, l.target.id])
);
node.classed("dimmed", d => d.id !== nodeData.id && !connectedIds.has(d.id));
link.classed("highlighted", l => l.source.id === nodeData.id || l.target.id === nodeData.id)
.classed("dimmed", l => l.source.id !== nodeData.id && l.target.id !== nodeData.id);
}
function highlightConnection(nodeId) {
const targetNode = nodes.find(n => n.id === nodeId);
if (targetNode) {
openSidePanel(targetNode);
}
}
function clearHighlights() {
node.classed("dimmed", false);
link.classed("highlighted", false).classed("dimmed", false);
}
// ============================================================================
// ACTION BUTTON HANDLERS
// ============================================================================
function copyEndpoint() {
if (!currentNode) return;
const text = currentNode.type === "datasource"
? currentNode.apiEndpoint
: `Topic: ${currentNode.name}`;
navigator.clipboard.writeText(text).then(() => {
showToast("✅ Copied to clipboard!");
});
}
function downloadNodeData() {
if (!currentNode) return;
const data = JSON.stringify(currentNode, null, 2);
const blob = new Blob([data], { type: "application/json" });
const url = URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url;
a.download = `${currentNode.id}_data.json`;
a.click();
URL.revokeObjectURL(url);
showToast("⬇️ Data downloaded successfully!");
}
function exploreRelated() {
if (!currentNode) return;
const connectedIds = links
.filter(l => l.source.id === currentNode.id || l.target.id === currentNode.id)
.map(l => l.source.id === currentNode.id ? l.target.id : l.source.id);
showToast(`🔍 Found ${connectedIds.length} related nodes`);
}
function showToast(message) {
const toast = document.getElementById("toast");
toast.textContent = message;
toast.classList.add("show");
setTimeout(() => {
toast.classList.remove("show");
}, 3000);
}
// ============================================================================
// CLOSE PANEL ON BACKGROUND CLICK
// ============================================================================
svg.on("click", () => {
if (document.getElementById("side-panel").classList.contains("open")) {
closeSidePanel();
}
});
// ============================================================================
// WEB LEARNING ATTRIBUTION
// ============================================================================
console.log("SDG Visualization 11 - Enhanced Side Panels & Information Design");
console.log("Web Learning Source: https://observablehq.com/@d3/bar-chart-transitions");
console.log("Techniques Applied:");
console.log("- 300ms transition timing for smooth panel slide-in (cubic-bezier easing)");
console.log("- Sequential reveals with staggered delays (50ms per connection item)");
console.log("- Coordinated multi-element transitions (highlight + dim animations)");
console.log("- Natural motion with CSS cubic-bezier(0.4, 0, 0.2, 1) for panel");
</script>
</body>
</html>