1101 lines
40 KiB
HTML
1101 lines
40 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 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>
|