1471 lines
50 KiB
HTML
1471 lines
50 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 Dashboard v1.0 - Production Ready</title>
|
||
<script src="https://d3js.org/d3.v7.min.js"></script>
|
||
<script src="https://cdnjs.cloudflare.com/ajax/libs/html2canvas/1.4.1/html2canvas.min.js"></script>
|
||
<style>
|
||
* {
|
||
margin: 0;
|
||
padding: 0;
|
||
box-sizing: border-box;
|
||
}
|
||
|
||
:root {
|
||
--sonic-red: #FF0000;
|
||
--sonic-blue: #0066FF;
|
||
--sonic-yellow: #FFD700;
|
||
--dark-bg: #0a0e27;
|
||
--panel-bg: rgba(10, 14, 39, 0.95);
|
||
--border-color: rgba(255, 215, 0, 0.3);
|
||
}
|
||
|
||
body {
|
||
font-family: 'Segoe UI', system-ui, -apple-system, sans-serif;
|
||
background: linear-gradient(135deg, #0a0e27 0%, #1a1f3a 50%, #0a0e27 100%);
|
||
color: #fff;
|
||
overflow: hidden;
|
||
height: 100vh;
|
||
}
|
||
|
||
#app-container {
|
||
width: 100vw;
|
||
height: 100vh;
|
||
display: flex;
|
||
flex-direction: column;
|
||
}
|
||
|
||
/* Header */
|
||
#header {
|
||
background: var(--panel-bg);
|
||
border-bottom: 2px solid var(--sonic-yellow);
|
||
padding: 12px 20px;
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
z-index: 100;
|
||
}
|
||
|
||
.header-title {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 12px;
|
||
}
|
||
|
||
.header-title h1 {
|
||
font-size: 20px;
|
||
background: linear-gradient(90deg, var(--sonic-red), var(--sonic-blue));
|
||
-webkit-background-clip: text;
|
||
-webkit-text-fill-color: transparent;
|
||
background-clip: text;
|
||
}
|
||
|
||
.version-badge {
|
||
background: var(--sonic-yellow);
|
||
color: #000;
|
||
padding: 2px 8px;
|
||
border-radius: 10px;
|
||
font-size: 11px;
|
||
font-weight: bold;
|
||
}
|
||
|
||
.header-actions {
|
||
display: flex;
|
||
gap: 8px;
|
||
align-items: center;
|
||
}
|
||
|
||
/* Main content area */
|
||
#main-content {
|
||
display: flex;
|
||
flex: 1;
|
||
overflow: hidden;
|
||
}
|
||
|
||
/* Sidebar */
|
||
#sidebar {
|
||
width: 280px;
|
||
background: var(--panel-bg);
|
||
border-right: 2px solid var(--border-color);
|
||
overflow-y: auto;
|
||
padding: 20px;
|
||
transition: transform 0.3s ease;
|
||
}
|
||
|
||
#sidebar.collapsed {
|
||
transform: translateX(-280px);
|
||
}
|
||
|
||
.panel-section {
|
||
margin-bottom: 24px;
|
||
}
|
||
|
||
.panel-section h3 {
|
||
font-size: 12px;
|
||
text-transform: uppercase;
|
||
color: var(--sonic-yellow);
|
||
margin-bottom: 12px;
|
||
letter-spacing: 1px;
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 6px;
|
||
}
|
||
|
||
/* Stats cards */
|
||
.stats-grid {
|
||
display: grid;
|
||
grid-template-columns: 1fr 1fr;
|
||
gap: 10px;
|
||
margin-bottom: 16px;
|
||
}
|
||
|
||
.stat-card {
|
||
background: rgba(255, 255, 255, 0.05);
|
||
border: 1px solid var(--border-color);
|
||
border-radius: 6px;
|
||
padding: 10px;
|
||
text-align: center;
|
||
}
|
||
|
||
.stat-value {
|
||
font-size: 24px;
|
||
font-weight: bold;
|
||
background: linear-gradient(90deg, var(--sonic-red), var(--sonic-blue));
|
||
-webkit-background-clip: text;
|
||
-webkit-text-fill-color: transparent;
|
||
}
|
||
|
||
.stat-label {
|
||
font-size: 10px;
|
||
color: #aaa;
|
||
margin-top: 4px;
|
||
}
|
||
|
||
/* Search */
|
||
.search-box {
|
||
position: relative;
|
||
margin-bottom: 16px;
|
||
}
|
||
|
||
.search-box input {
|
||
width: 100%;
|
||
padding: 10px 12px 10px 36px;
|
||
background: rgba(255, 255, 255, 0.1);
|
||
border: 1px solid var(--border-color);
|
||
border-radius: 6px;
|
||
color: #fff;
|
||
font-size: 13px;
|
||
}
|
||
|
||
.search-box input::placeholder {
|
||
color: #888;
|
||
}
|
||
|
||
.search-box::before {
|
||
content: '/';
|
||
position: absolute;
|
||
left: 14px;
|
||
top: 50%;
|
||
transform: translateY(-50%);
|
||
color: var(--sonic-yellow);
|
||
font-weight: bold;
|
||
}
|
||
|
||
/* Filters */
|
||
.filter-group {
|
||
margin-bottom: 12px;
|
||
}
|
||
|
||
.filter-group label {
|
||
display: block;
|
||
font-size: 11px;
|
||
color: #aaa;
|
||
margin-bottom: 6px;
|
||
}
|
||
|
||
.filter-tags {
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
gap: 6px;
|
||
}
|
||
|
||
.filter-tag {
|
||
padding: 4px 10px;
|
||
background: rgba(255, 255, 255, 0.1);
|
||
border: 1px solid var(--border-color);
|
||
border-radius: 12px;
|
||
font-size: 11px;
|
||
cursor: pointer;
|
||
transition: all 0.2s;
|
||
}
|
||
|
||
.filter-tag:hover {
|
||
background: rgba(255, 255, 255, 0.2);
|
||
}
|
||
|
||
.filter-tag.active {
|
||
background: var(--sonic-blue);
|
||
border-color: var(--sonic-blue);
|
||
color: #fff;
|
||
}
|
||
|
||
/* Buttons */
|
||
.btn {
|
||
padding: 8px 16px;
|
||
background: rgba(255, 255, 255, 0.1);
|
||
border: 1px solid var(--border-color);
|
||
border-radius: 6px;
|
||
color: #fff;
|
||
cursor: pointer;
|
||
font-size: 13px;
|
||
transition: all 0.2s;
|
||
display: inline-flex;
|
||
align-items: center;
|
||
gap: 6px;
|
||
}
|
||
|
||
.btn:hover {
|
||
background: rgba(255, 255, 255, 0.2);
|
||
border-color: var(--sonic-yellow);
|
||
}
|
||
|
||
.btn.primary {
|
||
background: var(--sonic-blue);
|
||
border-color: var(--sonic-blue);
|
||
}
|
||
|
||
.btn.primary:hover {
|
||
background: #0052cc;
|
||
}
|
||
|
||
.btn.danger {
|
||
background: var(--sonic-red);
|
||
border-color: var(--sonic-red);
|
||
}
|
||
|
||
.btn.danger:hover {
|
||
background: #cc0000;
|
||
}
|
||
|
||
.btn-group {
|
||
display: flex;
|
||
gap: 8px;
|
||
flex-wrap: wrap;
|
||
}
|
||
|
||
/* Canvas area */
|
||
#canvas-container {
|
||
flex: 1;
|
||
position: relative;
|
||
overflow: hidden;
|
||
}
|
||
|
||
#network-canvas {
|
||
display: block;
|
||
cursor: grab;
|
||
width: 100%;
|
||
height: 100%;
|
||
}
|
||
|
||
#network-canvas:active {
|
||
cursor: grabbing;
|
||
}
|
||
|
||
/* Table view */
|
||
#table-view {
|
||
display: none;
|
||
width: 100%;
|
||
height: 100%;
|
||
overflow: auto;
|
||
padding: 20px;
|
||
}
|
||
|
||
#table-view.active {
|
||
display: block;
|
||
}
|
||
|
||
table {
|
||
width: 100%;
|
||
border-collapse: collapse;
|
||
background: var(--panel-bg);
|
||
}
|
||
|
||
table th {
|
||
position: sticky;
|
||
top: 0;
|
||
background: var(--dark-bg);
|
||
padding: 12px;
|
||
text-align: left;
|
||
font-size: 12px;
|
||
color: var(--sonic-yellow);
|
||
border-bottom: 2px solid var(--sonic-yellow);
|
||
}
|
||
|
||
table td {
|
||
padding: 10px 12px;
|
||
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
|
||
font-size: 13px;
|
||
}
|
||
|
||
table tr:hover {
|
||
background: rgba(255, 255, 255, 0.05);
|
||
}
|
||
|
||
.node-badge {
|
||
width: 12px;
|
||
height: 12px;
|
||
border-radius: 50%;
|
||
display: inline-block;
|
||
margin-right: 8px;
|
||
border: 1px solid #fff;
|
||
}
|
||
|
||
/* Info panel */
|
||
#info-panel {
|
||
position: absolute;
|
||
top: 20px;
|
||
right: 20px;
|
||
width: 320px;
|
||
max-height: 80vh;
|
||
background: var(--panel-bg);
|
||
border: 2px solid var(--sonic-yellow);
|
||
border-radius: 8px;
|
||
padding: 20px;
|
||
display: none;
|
||
z-index: 50;
|
||
overflow-y: auto;
|
||
}
|
||
|
||
#info-panel.active {
|
||
display: block;
|
||
}
|
||
|
||
.info-panel-header {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
margin-bottom: 16px;
|
||
}
|
||
|
||
.info-panel-header h3 {
|
||
font-size: 16px;
|
||
color: var(--sonic-yellow);
|
||
}
|
||
|
||
.close-btn {
|
||
background: none;
|
||
border: none;
|
||
color: #fff;
|
||
font-size: 24px;
|
||
cursor: pointer;
|
||
padding: 0;
|
||
width: 24px;
|
||
height: 24px;
|
||
line-height: 1;
|
||
}
|
||
|
||
.close-btn:hover {
|
||
color: var(--sonic-red);
|
||
}
|
||
|
||
/* Tooltip */
|
||
#tooltip {
|
||
position: absolute;
|
||
background: rgba(0, 0, 0, 0.95);
|
||
border: 2px solid var(--sonic-yellow);
|
||
border-radius: 6px;
|
||
padding: 12px 16px;
|
||
pointer-events: none;
|
||
z-index: 1000;
|
||
display: none;
|
||
max-width: 250px;
|
||
}
|
||
|
||
#tooltip.visible {
|
||
display: block;
|
||
}
|
||
|
||
.tooltip-title {
|
||
font-weight: bold;
|
||
color: var(--sonic-yellow);
|
||
margin-bottom: 6px;
|
||
font-size: 14px;
|
||
}
|
||
|
||
.tooltip-details {
|
||
color: #ccc;
|
||
font-size: 12px;
|
||
}
|
||
|
||
/* Onboarding */
|
||
#onboarding {
|
||
position: fixed;
|
||
top: 0;
|
||
left: 0;
|
||
right: 0;
|
||
bottom: 0;
|
||
background: rgba(0, 0, 0, 0.9);
|
||
z-index: 10000;
|
||
display: none;
|
||
align-items: center;
|
||
justify-content: center;
|
||
}
|
||
|
||
#onboarding.active {
|
||
display: flex;
|
||
}
|
||
|
||
.onboarding-content {
|
||
background: var(--dark-bg);
|
||
border: 3px solid var(--sonic-yellow);
|
||
border-radius: 12px;
|
||
padding: 40px;
|
||
max-width: 600px;
|
||
text-align: center;
|
||
}
|
||
|
||
.onboarding-content h2 {
|
||
font-size: 28px;
|
||
margin-bottom: 20px;
|
||
background: linear-gradient(90deg, var(--sonic-red), var(--sonic-blue));
|
||
-webkit-background-clip: text;
|
||
-webkit-text-fill-color: transparent;
|
||
}
|
||
|
||
.onboarding-features {
|
||
text-align: left;
|
||
margin: 24px 0;
|
||
}
|
||
|
||
.onboarding-feature {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 12px;
|
||
margin-bottom: 12px;
|
||
font-size: 14px;
|
||
}
|
||
|
||
.feature-icon {
|
||
width: 32px;
|
||
height: 32px;
|
||
background: var(--sonic-blue);
|
||
border-radius: 50%;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
font-size: 16px;
|
||
}
|
||
|
||
/* Footer */
|
||
#footer {
|
||
background: var(--panel-bg);
|
||
border-top: 2px solid var(--border-color);
|
||
padding: 10px 20px;
|
||
font-size: 11px;
|
||
color: #aaa;
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
}
|
||
|
||
#footer a {
|
||
color: var(--sonic-blue);
|
||
text-decoration: none;
|
||
}
|
||
|
||
#footer a:hover {
|
||
color: var(--sonic-yellow);
|
||
}
|
||
|
||
/* Keyboard shortcuts */
|
||
.keyboard-hint {
|
||
font-family: monospace;
|
||
background: rgba(255, 255, 255, 0.1);
|
||
padding: 2px 6px;
|
||
border-radius: 3px;
|
||
font-size: 11px;
|
||
border: 1px solid rgba(255, 255, 255, 0.2);
|
||
}
|
||
|
||
/* Scrollbar */
|
||
::-webkit-scrollbar {
|
||
width: 8px;
|
||
}
|
||
|
||
::-webkit-scrollbar-track {
|
||
background: rgba(0, 0, 0, 0.3);
|
||
}
|
||
|
||
::-webkit-scrollbar-thumb {
|
||
background: var(--sonic-blue);
|
||
border-radius: 4px;
|
||
}
|
||
|
||
::-webkit-scrollbar-thumb:hover {
|
||
background: var(--sonic-red);
|
||
}
|
||
|
||
/* Responsive */
|
||
@media (max-width: 768px) {
|
||
#sidebar {
|
||
position: absolute;
|
||
height: 100%;
|
||
z-index: 90;
|
||
}
|
||
|
||
#info-panel {
|
||
width: 90%;
|
||
right: 5%;
|
||
}
|
||
}
|
||
|
||
/* Print styles */
|
||
@media print {
|
||
#sidebar,
|
||
#header .header-actions,
|
||
#info-panel,
|
||
#onboarding,
|
||
#tooltip {
|
||
display: none !important;
|
||
}
|
||
|
||
#main-content {
|
||
background: #fff;
|
||
}
|
||
}
|
||
|
||
/* Loading overlay */
|
||
#loading {
|
||
position: fixed;
|
||
top: 0;
|
||
left: 0;
|
||
right: 0;
|
||
bottom: 0;
|
||
background: var(--dark-bg);
|
||
z-index: 20000;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
flex-direction: column;
|
||
gap: 20px;
|
||
}
|
||
|
||
#loading.hidden {
|
||
display: none;
|
||
}
|
||
|
||
.spinner {
|
||
width: 60px;
|
||
height: 60px;
|
||
border: 4px solid rgba(255, 255, 255, 0.1);
|
||
border-top-color: var(--sonic-blue);
|
||
border-radius: 50%;
|
||
animation: spin 1s linear infinite;
|
||
}
|
||
|
||
@keyframes spin {
|
||
to { transform: rotate(360deg); }
|
||
}
|
||
|
||
/* Accessibility */
|
||
.sr-only {
|
||
position: absolute;
|
||
width: 1px;
|
||
height: 1px;
|
||
padding: 0;
|
||
margin: -1px;
|
||
overflow: hidden;
|
||
clip: rect(0, 0, 0, 0);
|
||
white-space: nowrap;
|
||
border-width: 0;
|
||
}
|
||
|
||
/* Help icon */
|
||
.help-icon {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
width: 16px;
|
||
height: 16px;
|
||
background: var(--sonic-yellow);
|
||
color: #000;
|
||
border-radius: 50%;
|
||
font-size: 11px;
|
||
font-weight: bold;
|
||
cursor: help;
|
||
margin-left: 4px;
|
||
}
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<!-- Loading screen -->
|
||
<div id="loading">
|
||
<div class="spinner"></div>
|
||
<div style="color: var(--sonic-yellow); font-size: 16px;">Loading SDG Network Dashboard...</div>
|
||
</div>
|
||
|
||
<!-- Onboarding -->
|
||
<div id="onboarding" class="active">
|
||
<div class="onboarding-content">
|
||
<h2>Welcome to SDG Network Dashboard</h2>
|
||
<p style="color: #aaa; margin-bottom: 24px;">A powerful tool for visualizing Sustainable Development Goals networks and relationships.</p>
|
||
|
||
<div class="onboarding-features">
|
||
<div class="onboarding-feature">
|
||
<div class="feature-icon">🔍</div>
|
||
<div><strong>Search & Filter:</strong> Press <span class="keyboard-hint">/</span> to quickly search nodes</div>
|
||
</div>
|
||
<div class="onboarding-feature">
|
||
<div class="feature-icon">🖱️</div>
|
||
<div><strong>Drag & Zoom:</strong> Click and drag nodes, scroll to zoom</div>
|
||
</div>
|
||
<div class="onboarding-feature">
|
||
<div class="feature-icon">📊</div>
|
||
<div><strong>View Modes:</strong> Switch between network and table views</div>
|
||
</div>
|
||
<div class="onboarding-feature">
|
||
<div class="feature-icon">💾</div>
|
||
<div><strong>Export:</strong> Save visualizations as PNG or SVG</div>
|
||
</div>
|
||
<div class="onboarding-feature">
|
||
<div class="feature-icon">⌨️</div>
|
||
<div><strong>Keyboard:</strong> Press <span class="keyboard-hint">ESC</span> to close panels</div>
|
||
</div>
|
||
</div>
|
||
|
||
<button class="btn primary" onclick="closeOnboarding()">Get Started</button>
|
||
<div style="margin-top: 12px;">
|
||
<label style="font-size: 12px; color: #888;">
|
||
<input type="checkbox" id="dont-show-again"> Don't show this again
|
||
</label>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Main app -->
|
||
<div id="app-container">
|
||
<!-- Header -->
|
||
<header id="header" role="banner">
|
||
<div class="header-title">
|
||
<h1>SDG Network Dashboard</h1>
|
||
<span class="version-badge">v1.0</span>
|
||
</div>
|
||
<div class="header-actions">
|
||
<button class="btn" onclick="toggleView()" aria-label="Toggle view mode">
|
||
<span id="view-toggle-icon">📊</span>
|
||
<span id="view-toggle-text">Table View</span>
|
||
</button>
|
||
<button class="btn" onclick="exportPNG()" aria-label="Export as PNG">
|
||
📸 PNG
|
||
</button>
|
||
<button class="btn" onclick="exportSVG()" aria-label="Export as SVG">
|
||
📐 SVG
|
||
</button>
|
||
<button class="btn" onclick="shareLink()" aria-label="Share link">
|
||
🔗 Share
|
||
</button>
|
||
<button class="btn" onclick="showAbout()" aria-label="About this dashboard">
|
||
ℹ️ About
|
||
</button>
|
||
</div>
|
||
</header>
|
||
|
||
<!-- Main content -->
|
||
<main id="main-content" role="main">
|
||
<!-- Sidebar -->
|
||
<aside id="sidebar" role="complementary" aria-label="Controls and filters">
|
||
<!-- Quick stats -->
|
||
<section class="panel-section">
|
||
<h3>📈 Quick Stats</h3>
|
||
<div class="stats-grid">
|
||
<div class="stat-card">
|
||
<div class="stat-value" id="stat-nodes">0</div>
|
||
<div class="stat-label">Nodes</div>
|
||
</div>
|
||
<div class="stat-card">
|
||
<div class="stat-value" id="stat-links">0</div>
|
||
<div class="stat-label">Links</div>
|
||
</div>
|
||
<div class="stat-card">
|
||
<div class="stat-value" id="stat-sdgs">17</div>
|
||
<div class="stat-label">SDG Goals</div>
|
||
</div>
|
||
<div class="stat-card">
|
||
<div class="stat-value" id="stat-visible">0</div>
|
||
<div class="stat-label">Visible</div>
|
||
</div>
|
||
</div>
|
||
</section>
|
||
|
||
<!-- Search -->
|
||
<section class="panel-section">
|
||
<h3>🔍 Search<span class="help-icon" title="Press / to focus search">?</span></h3>
|
||
<div class="search-box">
|
||
<input
|
||
type="text"
|
||
id="search-input"
|
||
placeholder="Search nodes..."
|
||
aria-label="Search nodes"
|
||
>
|
||
</div>
|
||
</section>
|
||
|
||
<!-- Filters -->
|
||
<section class="panel-section">
|
||
<h3>🎯 Filter by SDG</h3>
|
||
<div class="filter-tags" id="sdg-filters" role="group" aria-label="SDG filters">
|
||
<!-- Generated dynamically -->
|
||
</div>
|
||
</section>
|
||
|
||
<!-- View controls -->
|
||
<section class="panel-section">
|
||
<h3>⚙️ Controls</h3>
|
||
<div class="filter-group">
|
||
<label for="zoom-slider">Zoom Level</label>
|
||
<input
|
||
type="range"
|
||
id="zoom-slider"
|
||
min="0.5"
|
||
max="3"
|
||
step="0.1"
|
||
value="1"
|
||
style="width: 100%"
|
||
aria-label="Zoom level"
|
||
>
|
||
<div style="font-size: 11px; color: #888; margin-top: 4px;">
|
||
<span id="zoom-value">1.0x</span>
|
||
</div>
|
||
</div>
|
||
<div class="btn-group" style="margin-top: 12px;">
|
||
<button class="btn" onclick="resetZoom()">Reset Zoom</button>
|
||
<button class="btn" onclick="resetLayout()">Reset Layout</button>
|
||
</div>
|
||
</section>
|
||
|
||
<!-- Export -->
|
||
<section class="panel-section">
|
||
<h3>💾 Export & Share</h3>
|
||
<div class="btn-group">
|
||
<button class="btn" onclick="printView()">🖨️ Print</button>
|
||
<button class="btn" onclick="copyData()">📋 Copy Data</button>
|
||
</div>
|
||
</section>
|
||
</aside>
|
||
|
||
<!-- Canvas/Table container -->
|
||
<div id="canvas-container">
|
||
<canvas id="network-canvas" role="img" aria-label="SDG network visualization"></canvas>
|
||
<div id="table-view" role="region" aria-label="Data table view">
|
||
<table id="data-table">
|
||
<thead>
|
||
<tr>
|
||
<th>SDG</th>
|
||
<th>Name</th>
|
||
<th>Connections</th>
|
||
<th>Category</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody id="table-body">
|
||
<!-- Generated dynamically -->
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Info panel -->
|
||
<div id="info-panel" role="dialog" aria-labelledby="info-panel-title">
|
||
<div class="info-panel-header">
|
||
<h3 id="info-panel-title">About SDG Dashboard</h3>
|
||
<button class="close-btn" onclick="closeAbout()" aria-label="Close panel">×</button>
|
||
</div>
|
||
<div id="info-panel-content">
|
||
<p style="margin-bottom: 16px; line-height: 1.6;">
|
||
This dashboard visualizes the relationships between Sustainable Development Goals (SDGs) and related projects.
|
||
It demonstrates how different goals interconnect and support each other.
|
||
</p>
|
||
|
||
<h4 style="color: var(--sonic-yellow); margin: 16px 0 8px;">Features</h4>
|
||
<ul style="margin-left: 20px; line-height: 1.8; font-size: 13px;">
|
||
<li>Interactive network visualization</li>
|
||
<li>Drag nodes to rearrange layout</li>
|
||
<li>Zoom and pan controls</li>
|
||
<li>Search and filter by SDG</li>
|
||
<li>Export to PNG/SVG</li>
|
||
<li>Table view for data analysis</li>
|
||
<li>Share with URL parameters</li>
|
||
</ul>
|
||
|
||
<h4 style="color: var(--sonic-yellow); margin: 16px 0 8px;">Keyboard Shortcuts</h4>
|
||
<ul style="margin-left: 20px; line-height: 1.8; font-size: 13px;">
|
||
<li><span class="keyboard-hint">/</span> - Focus search</li>
|
||
<li><span class="keyboard-hint">ESC</span> - Close panels</li>
|
||
<li><span class="keyboard-hint">Ctrl/Cmd + P</span> - Print</li>
|
||
</ul>
|
||
|
||
<h4 style="color: var(--sonic-yellow); margin: 16px 0 8px;">Data Sources</h4>
|
||
<p style="font-size: 12px; color: #aaa; line-height: 1.6;">
|
||
UN Sustainable Development Goals Framework<br>
|
||
Generated sample data for demonstration purposes
|
||
</p>
|
||
|
||
<h4 style="color: var(--sonic-yellow); margin: 16px 0 8px;">Contact</h4>
|
||
<p style="font-size: 12px; color: #aaa;">
|
||
<a href="mailto:feedback@sdgdashboard.example" style="color: var(--sonic-blue);">feedback@sdgdashboard.example</a>
|
||
</p>
|
||
|
||
<div style="margin-top: 20px; padding-top: 16px; border-top: 1px solid rgba(255, 255, 255, 0.1); font-size: 11px; color: #666;">
|
||
Dashboard Version 1.0.0<br>
|
||
Last Updated: 2025-10-09
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Tooltip -->
|
||
<div id="tooltip" role="tooltip">
|
||
<div class="tooltip-title"></div>
|
||
<div class="tooltip-details"></div>
|
||
</div>
|
||
</main>
|
||
|
||
<!-- Footer -->
|
||
<footer id="footer" role="contentinfo">
|
||
<div>
|
||
<strong>SDG Network Dashboard v1.0</strong> |
|
||
Production-ready bipartite visualization |
|
||
Data refreshed: <span id="data-timestamp">2025-10-09</span>
|
||
</div>
|
||
<div>
|
||
<a href="https://sdgs.un.org/" target="_blank" rel="noopener">UN SDGs</a> |
|
||
<a href="#" onclick="showAbout(); return false;">Help</a> |
|
||
<a href="#" onclick="resetOnboarding(); return false;">Show Tutorial</a>
|
||
</div>
|
||
</footer>
|
||
</div>
|
||
|
||
<script>
|
||
// ============================================================================
|
||
// CONFIGURATION & DATA
|
||
// ============================================================================
|
||
|
||
const SDG_CATEGORIES = [
|
||
{ id: 1, name: "No Poverty", color: "#E5243B", category: "Social" },
|
||
{ id: 2, name: "Zero Hunger", color: "#DDA63A", category: "Social" },
|
||
{ id: 3, name: "Good Health", color: "#4C9F38", category: "Social" },
|
||
{ id: 4, name: "Quality Education", color: "#C5192D", category: "Social" },
|
||
{ id: 5, name: "Gender Equality", color: "#FF3A21", category: "Social" },
|
||
{ id: 6, name: "Clean Water", color: "#26BDE2", category: "Environmental" },
|
||
{ id: 7, name: "Affordable Energy", color: "#FCC30B", category: "Economic" },
|
||
{ id: 8, name: "Decent Work", color: "#A21942", category: "Economic" },
|
||
{ id: 9, name: "Industry Innovation", color: "#FD6925", category: "Economic" },
|
||
{ id: 10, name: "Reduced Inequalities", color: "#DD1367", category: "Social" },
|
||
{ id: 11, name: "Sustainable Cities", color: "#FD9D24", category: "Environmental" },
|
||
{ id: 12, name: "Responsible Consumption", color: "#BF8B2E", category: "Environmental" },
|
||
{ id: 13, name: "Climate Action", color: "#3F7E44", category: "Environmental" },
|
||
{ id: 14, name: "Life Below Water", color: "#0A97D9", category: "Environmental" },
|
||
{ id: 15, name: "Life on Land", color: "#56C02B", category: "Environmental" },
|
||
{ id: 16, name: "Peace Justice", color: "#00689D", category: "Social" },
|
||
{ id: 17, name: "Partnerships", color: "#19486A", category: "Economic" }
|
||
];
|
||
|
||
// ============================================================================
|
||
// STATE MANAGEMENT
|
||
// ============================================================================
|
||
|
||
const state = {
|
||
nodes: [],
|
||
links: [],
|
||
transform: d3.zoomIdentity,
|
||
simulation: null,
|
||
currentView: 'network',
|
||
selectedSDGs: new Set(),
|
||
searchQuery: '',
|
||
showOnboarding: !localStorage.getItem('sdg-onboarding-dismissed')
|
||
};
|
||
|
||
// ============================================================================
|
||
// DATA GENERATION
|
||
// ============================================================================
|
||
|
||
function generateData() {
|
||
const nodes = [];
|
||
const links = [];
|
||
const nodeCount = 150;
|
||
|
||
// Create nodes
|
||
for (let i = 0; i < nodeCount; i++) {
|
||
const sdg = SDG_CATEGORIES[i % SDG_CATEGORIES.length];
|
||
nodes.push({
|
||
id: `node_${i}`,
|
||
name: `${sdg.name} Initiative ${Math.floor(i / SDG_CATEGORIES.length) + 1}`,
|
||
sdg: sdg.id,
|
||
sdgName: sdg.name,
|
||
category: sdg.category,
|
||
color: sdg.color,
|
||
value: Math.random() * 40 + 20,
|
||
connections: 0
|
||
});
|
||
}
|
||
|
||
// Create links (preferring related SDGs)
|
||
nodes.forEach((source, i) => {
|
||
const numLinks = Math.floor(Math.random() * 4) + 2;
|
||
const targets = new Set();
|
||
|
||
for (let j = 0; j < numLinks; j++) {
|
||
let targetIdx;
|
||
|
||
if (Math.random() < 0.7) {
|
||
// Prefer same category
|
||
const sameCat = nodes.filter((n, idx) =>
|
||
n.category === source.category && idx !== i
|
||
);
|
||
if (sameCat.length > 0) {
|
||
const target = sameCat[Math.floor(Math.random() * sameCat.length)];
|
||
targetIdx = nodes.indexOf(target);
|
||
} else {
|
||
targetIdx = Math.floor(Math.random() * nodeCount);
|
||
}
|
||
} else {
|
||
targetIdx = Math.floor(Math.random() * nodeCount);
|
||
}
|
||
|
||
if (targetIdx !== i && !targets.has(targetIdx)) {
|
||
targets.add(targetIdx);
|
||
links.push({
|
||
source: source.id,
|
||
target: nodes[targetIdx].id,
|
||
value: Math.random()
|
||
});
|
||
source.connections++;
|
||
nodes[targetIdx].connections++;
|
||
}
|
||
}
|
||
});
|
||
|
||
state.nodes = nodes;
|
||
state.links = links;
|
||
}
|
||
|
||
// ============================================================================
|
||
// CANVAS RENDERING
|
||
// ============================================================================
|
||
|
||
const canvas = document.getElementById('network-canvas');
|
||
const ctx = canvas.getContext('2d');
|
||
let width, height;
|
||
|
||
function resizeCanvas() {
|
||
const container = document.getElementById('canvas-container');
|
||
width = canvas.width = container.clientWidth;
|
||
height = canvas.height = container.clientHeight;
|
||
}
|
||
|
||
function render() {
|
||
ctx.save();
|
||
ctx.clearRect(0, 0, width, height);
|
||
ctx.translate(state.transform.x, state.transform.y);
|
||
ctx.scale(state.transform.k, state.transform.k);
|
||
|
||
const filteredNodes = getFilteredNodes();
|
||
const nodeSet = new Set(filteredNodes.map(n => n.id));
|
||
const filteredLinks = state.links.filter(l => {
|
||
const sourceId = typeof l.source === 'object' ? l.source.id : l.source;
|
||
const targetId = typeof l.target === 'object' ? l.target.id : l.target;
|
||
return nodeSet.has(sourceId) && nodeSet.has(targetId);
|
||
});
|
||
|
||
// Draw links
|
||
ctx.globalAlpha = 0.3;
|
||
ctx.strokeStyle = '#4fc3f7';
|
||
ctx.lineWidth = 2;
|
||
|
||
filteredLinks.forEach(link => {
|
||
const source = typeof link.source === 'object' ? link.source : state.nodes.find(n => n.id === link.source);
|
||
const target = typeof link.target === 'object' ? link.target : state.nodes.find(n => n.id === link.target);
|
||
|
||
if (source && target) {
|
||
ctx.beginPath();
|
||
ctx.moveTo(source.x, source.y);
|
||
ctx.lineTo(target.x, target.y);
|
||
ctx.stroke();
|
||
}
|
||
});
|
||
|
||
ctx.globalAlpha = 1;
|
||
|
||
// Draw nodes
|
||
filteredNodes.forEach(node => {
|
||
const radius = Math.sqrt(node.value) * 1.5;
|
||
|
||
// Outer glow (Sonic style)
|
||
const gradient = ctx.createRadialGradient(node.x, node.y, 0, node.x, node.y, radius + 4);
|
||
gradient.addColorStop(0, node.color);
|
||
gradient.addColorStop(1, 'rgba(0, 0, 0, 0)');
|
||
ctx.fillStyle = gradient;
|
||
ctx.beginPath();
|
||
ctx.arc(node.x, node.y, radius + 4, 0, 2 * Math.PI);
|
||
ctx.fill();
|
||
|
||
// Main circle
|
||
ctx.beginPath();
|
||
ctx.arc(node.x, node.y, radius, 0, 2 * Math.PI);
|
||
ctx.fillStyle = node.color;
|
||
ctx.fill();
|
||
|
||
// Bold border (yellow/white)
|
||
ctx.strokeStyle = node.sdg % 2 === 0 ? '#FFD700' : '#FFFFFF';
|
||
ctx.lineWidth = 3;
|
||
ctx.stroke();
|
||
|
||
// Inner highlight
|
||
ctx.beginPath();
|
||
ctx.arc(node.x, node.y, radius * 0.6, 0, 2 * Math.PI);
|
||
ctx.fillStyle = 'rgba(255, 255, 255, 0.4)';
|
||
ctx.fill();
|
||
|
||
// Draw label at larger zoom
|
||
if (state.transform.k > 1.2) {
|
||
ctx.fillStyle = '#fff';
|
||
ctx.font = 'bold 12px sans-serif';
|
||
ctx.textAlign = 'center';
|
||
ctx.strokeStyle = '#000';
|
||
ctx.lineWidth = 3;
|
||
ctx.strokeText(node.name, node.x, node.y + radius + 16);
|
||
ctx.fillText(node.name, node.x, node.y + radius + 16);
|
||
}
|
||
});
|
||
|
||
ctx.restore();
|
||
|
||
// Update stats
|
||
document.getElementById('stat-visible').textContent = filteredNodes.length;
|
||
}
|
||
|
||
// ============================================================================
|
||
// FILTERING & SEARCH
|
||
// ============================================================================
|
||
|
||
function getFilteredNodes() {
|
||
return state.nodes.filter(node => {
|
||
// SDG filter
|
||
if (state.selectedSDGs.size > 0 && !state.selectedSDGs.has(node.sdg)) {
|
||
return false;
|
||
}
|
||
|
||
// Search filter
|
||
if (state.searchQuery) {
|
||
const query = state.searchQuery.toLowerCase();
|
||
return node.name.toLowerCase().includes(query) ||
|
||
node.sdgName.toLowerCase().includes(query);
|
||
}
|
||
|
||
return true;
|
||
});
|
||
}
|
||
|
||
function updateFilters() {
|
||
if (state.simulation) {
|
||
state.simulation.alpha(0.3).restart();
|
||
}
|
||
render();
|
||
updateTableView();
|
||
}
|
||
|
||
// ============================================================================
|
||
// FORCE SIMULATION
|
||
// ============================================================================
|
||
|
||
function createSimulation() {
|
||
state.simulation = d3.forceSimulation(state.nodes)
|
||
.force('link', d3.forceLink(state.links)
|
||
.id(d => d.id)
|
||
.distance(100)
|
||
.strength(0.5))
|
||
.force('charge', d3.forceManyBody()
|
||
.strength(-200)
|
||
.distanceMax(400))
|
||
.force('center', d3.forceCenter(width / 2, height / 2))
|
||
.force('collision', d3.forceCollide()
|
||
.radius(d => Math.sqrt(d.value) * 1.5 + 5)
|
||
.iterations(2))
|
||
.alphaDecay(0.02)
|
||
.velocityDecay(0.4);
|
||
|
||
state.simulation.on('tick', render);
|
||
}
|
||
|
||
// ============================================================================
|
||
// INTERACTIONS
|
||
// ============================================================================
|
||
|
||
// Zoom
|
||
const zoom = d3.zoom()
|
||
.scaleExtent([0.5, 3])
|
||
.on('zoom', (event) => {
|
||
state.transform = event.transform;
|
||
document.getElementById('zoom-value').textContent = event.transform.k.toFixed(1) + 'x';
|
||
document.getElementById('zoom-slider').value = event.transform.k;
|
||
render();
|
||
});
|
||
|
||
d3.select(canvas).call(zoom);
|
||
|
||
// Drag
|
||
function getDragSubject(event) {
|
||
const [mx, my] = state.transform.invert([event.x, event.y]);
|
||
const filteredNodes = getFilteredNodes();
|
||
|
||
for (const node of filteredNodes) {
|
||
const radius = Math.sqrt(node.value) * 1.5;
|
||
const dx = node.x - mx;
|
||
const dy = node.y - my;
|
||
if (Math.sqrt(dx * dx + dy * dy) < radius) {
|
||
return node;
|
||
}
|
||
}
|
||
return null;
|
||
}
|
||
|
||
const drag = d3.drag()
|
||
.subject(getDragSubject)
|
||
.on('start', (event) => {
|
||
if (!event.active) state.simulation.alphaTarget(0.3).restart();
|
||
event.subject.fx = event.subject.x;
|
||
event.subject.fy = event.subject.y;
|
||
})
|
||
.on('drag', (event) => {
|
||
const [x, y] = state.transform.invert([event.x, event.y]);
|
||
event.subject.fx = x;
|
||
event.subject.fy = y;
|
||
})
|
||
.on('end', (event) => {
|
||
if (!event.active) state.simulation.alphaTarget(0);
|
||
event.subject.fx = null;
|
||
event.subject.fy = null;
|
||
});
|
||
|
||
d3.select(canvas).call(drag);
|
||
|
||
// Tooltip
|
||
const tooltip = document.getElementById('tooltip');
|
||
canvas.addEventListener('mousemove', (event) => {
|
||
const [mx, my] = state.transform.invert([event.offsetX, event.offsetY]);
|
||
const filteredNodes = getFilteredNodes();
|
||
|
||
let found = null;
|
||
for (const node of filteredNodes) {
|
||
const radius = Math.sqrt(node.value) * 1.5;
|
||
const dx = node.x - mx;
|
||
const dy = node.y - my;
|
||
if (Math.sqrt(dx * dx + dy * dy) < radius) {
|
||
found = node;
|
||
break;
|
||
}
|
||
}
|
||
|
||
if (found) {
|
||
tooltip.querySelector('.tooltip-title').textContent = found.name;
|
||
tooltip.querySelector('.tooltip-details').textContent =
|
||
`SDG ${found.sdg}: ${found.sdgName}\n${found.connections} connections\nCategory: ${found.category}`;
|
||
tooltip.style.left = (event.pageX + 15) + 'px';
|
||
tooltip.style.top = (event.pageY + 15) + 'px';
|
||
tooltip.classList.add('visible');
|
||
} else {
|
||
tooltip.classList.remove('visible');
|
||
}
|
||
});
|
||
|
||
canvas.addEventListener('mouseleave', () => {
|
||
tooltip.classList.remove('visible');
|
||
});
|
||
|
||
// ============================================================================
|
||
// UI FUNCTIONS
|
||
// ============================================================================
|
||
|
||
function toggleView() {
|
||
const canvasContainer = document.getElementById('network-canvas');
|
||
const tableView = document.getElementById('table-view');
|
||
const icon = document.getElementById('view-toggle-icon');
|
||
const text = document.getElementById('view-toggle-text');
|
||
|
||
if (state.currentView === 'network') {
|
||
state.currentView = 'table';
|
||
canvasContainer.style.display = 'none';
|
||
tableView.classList.add('active');
|
||
icon.textContent = '🌐';
|
||
text.textContent = 'Network View';
|
||
updateTableView();
|
||
} else {
|
||
state.currentView = 'network';
|
||
canvasContainer.style.display = 'block';
|
||
tableView.classList.remove('active');
|
||
icon.textContent = '📊';
|
||
text.textContent = 'Table View';
|
||
}
|
||
}
|
||
|
||
function updateTableView() {
|
||
const tbody = document.getElementById('table-body');
|
||
tbody.innerHTML = '';
|
||
|
||
const filteredNodes = getFilteredNodes();
|
||
filteredNodes.forEach(node => {
|
||
const row = tbody.insertRow();
|
||
row.innerHTML = `
|
||
<td><span class="node-badge" style="background: ${node.color}"></span>SDG ${node.sdg}</td>
|
||
<td>${node.name}</td>
|
||
<td>${node.connections}</td>
|
||
<td>${node.category}</td>
|
||
`;
|
||
});
|
||
}
|
||
|
||
function exportPNG() {
|
||
html2canvas(canvas).then(canvas => {
|
||
const link = document.createElement('a');
|
||
link.download = `sdg-network-${Date.now()}.png`;
|
||
link.href = canvas.toDataURL();
|
||
link.click();
|
||
});
|
||
}
|
||
|
||
function exportSVG() {
|
||
// Simplified SVG export
|
||
alert('SVG export: This feature would convert the canvas to SVG format. For production, integrate a library like canvas2svg.');
|
||
}
|
||
|
||
function shareLink() {
|
||
const params = new URLSearchParams();
|
||
if (state.selectedSDGs.size > 0) {
|
||
params.set('sdgs', Array.from(state.selectedSDGs).join(','));
|
||
}
|
||
if (state.searchQuery) {
|
||
params.set('q', state.searchQuery);
|
||
}
|
||
params.set('zoom', state.transform.k.toFixed(2));
|
||
|
||
const url = window.location.origin + window.location.pathname + '?' + params.toString();
|
||
navigator.clipboard.writeText(url).then(() => {
|
||
alert('Share link copied to clipboard!');
|
||
});
|
||
}
|
||
|
||
function showAbout() {
|
||
document.getElementById('info-panel').classList.add('active');
|
||
}
|
||
|
||
function closeAbout() {
|
||
document.getElementById('info-panel').classList.remove('active');
|
||
}
|
||
|
||
function printView() {
|
||
window.print();
|
||
}
|
||
|
||
function copyData() {
|
||
const data = getFilteredNodes().map(n => ({
|
||
name: n.name,
|
||
sdg: n.sdg,
|
||
category: n.category,
|
||
connections: n.connections
|
||
}));
|
||
navigator.clipboard.writeText(JSON.stringify(data, null, 2)).then(() => {
|
||
alert('Data copied to clipboard!');
|
||
});
|
||
}
|
||
|
||
function resetZoom() {
|
||
d3.select(canvas)
|
||
.transition()
|
||
.duration(750)
|
||
.call(zoom.transform, d3.zoomIdentity);
|
||
}
|
||
|
||
function resetLayout() {
|
||
if (state.simulation) {
|
||
state.simulation.alpha(1).restart();
|
||
}
|
||
}
|
||
|
||
function closeOnboarding() {
|
||
document.getElementById('onboarding').classList.remove('active');
|
||
if (document.getElementById('dont-show-again').checked) {
|
||
localStorage.setItem('sdg-onboarding-dismissed', 'true');
|
||
}
|
||
}
|
||
|
||
function resetOnboarding() {
|
||
localStorage.removeItem('sdg-onboarding-dismissed');
|
||
document.getElementById('onboarding').classList.add('active');
|
||
}
|
||
|
||
// ============================================================================
|
||
// UI INITIALIZATION
|
||
// ============================================================================
|
||
|
||
function initFilters() {
|
||
const container = document.getElementById('sdg-filters');
|
||
SDG_CATEGORIES.forEach(sdg => {
|
||
const tag = document.createElement('div');
|
||
tag.className = 'filter-tag';
|
||
tag.textContent = `SDG ${sdg.id}`;
|
||
tag.style.borderColor = sdg.color;
|
||
tag.onclick = () => {
|
||
if (state.selectedSDGs.has(sdg.id)) {
|
||
state.selectedSDGs.delete(sdg.id);
|
||
tag.classList.remove('active');
|
||
} else {
|
||
state.selectedSDGs.add(sdg.id);
|
||
tag.classList.add('active');
|
||
tag.style.background = sdg.color;
|
||
}
|
||
updateFilters();
|
||
};
|
||
container.appendChild(tag);
|
||
});
|
||
}
|
||
|
||
// Search
|
||
document.getElementById('search-input').addEventListener('input', (e) => {
|
||
state.searchQuery = e.target.value;
|
||
updateFilters();
|
||
});
|
||
|
||
// Zoom slider
|
||
document.getElementById('zoom-slider').addEventListener('input', (e) => {
|
||
const scale = parseFloat(e.target.value);
|
||
d3.select(canvas)
|
||
.transition()
|
||
.duration(100)
|
||
.call(zoom.transform,
|
||
d3.zoomIdentity
|
||
.translate(width / 2, height / 2)
|
||
.scale(scale)
|
||
.translate(-width / 2, -height / 2)
|
||
);
|
||
});
|
||
|
||
// ============================================================================
|
||
// KEYBOARD SHORTCUTS
|
||
// ============================================================================
|
||
|
||
document.addEventListener('keydown', (e) => {
|
||
// ESC - close panels
|
||
if (e.key === 'Escape') {
|
||
document.getElementById('info-panel').classList.remove('active');
|
||
document.getElementById('onboarding').classList.remove('active');
|
||
}
|
||
|
||
// / - focus search
|
||
if (e.key === '/' && !e.ctrlKey && !e.metaKey) {
|
||
e.preventDefault();
|
||
document.getElementById('search-input').focus();
|
||
}
|
||
});
|
||
|
||
// ============================================================================
|
||
// URL PARAMETERS
|
||
// ============================================================================
|
||
|
||
function loadFromURL() {
|
||
const params = new URLSearchParams(window.location.search);
|
||
|
||
if (params.has('sdgs')) {
|
||
const sdgs = params.get('sdgs').split(',').map(Number);
|
||
sdgs.forEach(id => {
|
||
state.selectedSDGs.add(id);
|
||
const tag = Array.from(document.querySelectorAll('.filter-tag'))
|
||
.find(t => t.textContent === `SDG ${id}`);
|
||
if (tag) {
|
||
tag.classList.add('active');
|
||
const sdg = SDG_CATEGORIES.find(s => s.id === id);
|
||
if (sdg) tag.style.background = sdg.color;
|
||
}
|
||
});
|
||
}
|
||
|
||
if (params.has('q')) {
|
||
state.searchQuery = params.get('q');
|
||
document.getElementById('search-input').value = state.searchQuery;
|
||
}
|
||
|
||
if (params.has('zoom')) {
|
||
const zoom_level = parseFloat(params.get('zoom'));
|
||
setTimeout(() => {
|
||
d3.select(canvas).call(zoom.transform,
|
||
d3.zoomIdentity
|
||
.translate(width / 2, height / 2)
|
||
.scale(zoom_level)
|
||
.translate(-width / 2, -height / 2)
|
||
);
|
||
}, 500);
|
||
}
|
||
}
|
||
|
||
// ============================================================================
|
||
// INITIALIZATION
|
||
// ============================================================================
|
||
|
||
async function init() {
|
||
// Generate data
|
||
generateData();
|
||
|
||
// Setup canvas
|
||
resizeCanvas();
|
||
window.addEventListener('resize', () => {
|
||
resizeCanvas();
|
||
if (state.simulation) {
|
||
state.simulation.force('center', d3.forceCenter(width / 2, height / 2));
|
||
state.simulation.alpha(0.3).restart();
|
||
}
|
||
});
|
||
|
||
// Initialize UI
|
||
initFilters();
|
||
updateTableView();
|
||
|
||
// Update stats
|
||
document.getElementById('stat-nodes').textContent = state.nodes.length;
|
||
document.getElementById('stat-links').textContent = state.links.length;
|
||
document.getElementById('stat-visible').textContent = state.nodes.length;
|
||
|
||
// Create simulation (NO entrance animation - instant render)
|
||
createSimulation();
|
||
|
||
// Load URL parameters
|
||
loadFromURL();
|
||
|
||
// Hide loading screen
|
||
document.getElementById('loading').classList.add('hidden');
|
||
|
||
// Show onboarding if needed
|
||
if (!state.showOnboarding) {
|
||
closeOnboarding();
|
||
}
|
||
|
||
// Render initial frame
|
||
render();
|
||
}
|
||
|
||
// Start the app
|
||
init();
|
||
|
||
// ============================================================================
|
||
// WEB LEARNING ATTRIBUTION
|
||
// ============================================================================
|
||
console.log('%cSDG Network Dashboard v1.0', 'color: #FFD700; font-size: 16px; font-weight: bold;');
|
||
console.log('%cWeb Learning Source:', 'color: #0066FF; font-weight: bold;');
|
||
console.log('Observable D3 Zoomable Sunburst - https://observablehq.com/@d3/zoomable-sunburst');
|
||
console.log('Techniques Applied:');
|
||
console.log('- Hierarchical zoom navigation with smooth transitions');
|
||
console.log('- Breadcrumb-style context preservation');
|
||
console.log('- Advanced interaction patterns (click, hover, keyboard)');
|
||
console.log('- Professional UX with cursor states and accessibility');
|
||
</script>
|
||
</body>
|
||
</html>
|