739 lines
27 KiB
HTML
739 lines
27 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>SDG Network Viz 5 - Advanced Color Encodings & Visual Hierarchy</title>
|
|
<script src="https://d3js.org/d3.v7.min.js"></script>
|
|
<style>
|
|
body {
|
|
margin: 0;
|
|
padding: 20px;
|
|
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|
min-height: 100vh;
|
|
}
|
|
|
|
#container {
|
|
max-width: 1400px;
|
|
margin: 0 auto;
|
|
background: white;
|
|
border-radius: 12px;
|
|
box-shadow: 0 20px 60px rgba(0,0,0,0.3);
|
|
overflow: hidden;
|
|
}
|
|
|
|
header {
|
|
background: linear-gradient(135deg, #1e3a8a 0%, #3b82f6 100%);
|
|
color: white;
|
|
padding: 30px;
|
|
text-align: center;
|
|
}
|
|
|
|
h1 {
|
|
margin: 0 0 10px 0;
|
|
font-size: 2.2em;
|
|
font-weight: 700;
|
|
}
|
|
|
|
.subtitle {
|
|
font-size: 1.1em;
|
|
opacity: 0.9;
|
|
margin: 0;
|
|
}
|
|
|
|
#controls {
|
|
padding: 20px;
|
|
background: #f8fafc;
|
|
border-bottom: 2px solid #e2e8f0;
|
|
display: flex;
|
|
flex-wrap: wrap;
|
|
gap: 20px;
|
|
align-items: center;
|
|
justify-content: center;
|
|
}
|
|
|
|
.control-group {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 8px;
|
|
}
|
|
|
|
.control-group label {
|
|
font-weight: 600;
|
|
font-size: 0.9em;
|
|
color: #334155;
|
|
}
|
|
|
|
select, button {
|
|
padding: 10px 16px;
|
|
border-radius: 6px;
|
|
border: 2px solid #cbd5e1;
|
|
font-size: 1em;
|
|
background: white;
|
|
cursor: pointer;
|
|
transition: all 0.3s;
|
|
}
|
|
|
|
select:hover, button:hover {
|
|
border-color: #3b82f6;
|
|
box-shadow: 0 2px 8px rgba(59, 130, 246, 0.2);
|
|
}
|
|
|
|
button {
|
|
background: linear-gradient(135deg, #3b82f6 0%, #2563eb 100%);
|
|
color: white;
|
|
border: none;
|
|
font-weight: 600;
|
|
}
|
|
|
|
button:hover {
|
|
background: linear-gradient(135deg, #2563eb 0%, #1d4ed8 100%);
|
|
transform: translateY(-2px);
|
|
box-shadow: 0 4px 12px rgba(59, 130, 246, 0.4);
|
|
}
|
|
|
|
#viz-container {
|
|
position: relative;
|
|
padding: 20px;
|
|
}
|
|
|
|
svg {
|
|
display: block;
|
|
margin: 0 auto;
|
|
background: #ffffff;
|
|
border-radius: 8px;
|
|
}
|
|
|
|
.node {
|
|
cursor: pointer;
|
|
transition: all 0.3s;
|
|
}
|
|
|
|
.node:hover {
|
|
filter: brightness(1.2);
|
|
}
|
|
|
|
.node-circle {
|
|
stroke-width: 3;
|
|
}
|
|
|
|
.node-label {
|
|
font-size: 11px;
|
|
font-weight: 600;
|
|
text-anchor: middle;
|
|
pointer-events: none;
|
|
fill: #1e293b;
|
|
text-shadow: 0 0 3px white, 0 0 3px white, 0 0 3px white;
|
|
}
|
|
|
|
.link {
|
|
stroke-opacity: 0.4;
|
|
stroke-linecap: round;
|
|
}
|
|
|
|
.link:hover {
|
|
stroke-opacity: 0.8;
|
|
stroke-width: 3 !important;
|
|
}
|
|
|
|
#legend-container {
|
|
padding: 20px;
|
|
background: #f8fafc;
|
|
border-top: 2px solid #e2e8f0;
|
|
}
|
|
|
|
.legend-section {
|
|
margin-bottom: 20px;
|
|
}
|
|
|
|
.legend-title {
|
|
font-weight: 700;
|
|
font-size: 1.1em;
|
|
color: #1e293b;
|
|
margin-bottom: 12px;
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 8px;
|
|
}
|
|
|
|
.legend-items {
|
|
display: flex;
|
|
flex-wrap: wrap;
|
|
gap: 15px;
|
|
}
|
|
|
|
.legend-item {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 8px;
|
|
padding: 8px 12px;
|
|
background: white;
|
|
border-radius: 6px;
|
|
border: 2px solid #e2e8f0;
|
|
cursor: pointer;
|
|
transition: all 0.3s;
|
|
}
|
|
|
|
.legend-item:hover {
|
|
border-color: #3b82f6;
|
|
transform: translateY(-2px);
|
|
box-shadow: 0 4px 12px rgba(59, 130, 246, 0.2);
|
|
}
|
|
|
|
.legend-item.filtered {
|
|
opacity: 0.3;
|
|
}
|
|
|
|
.legend-swatch {
|
|
width: 24px;
|
|
height: 24px;
|
|
border-radius: 4px;
|
|
border: 2px solid #cbd5e1;
|
|
}
|
|
|
|
.legend-label {
|
|
font-size: 0.9em;
|
|
color: #334155;
|
|
font-weight: 500;
|
|
}
|
|
|
|
.gradient-legend {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 12px;
|
|
margin-top: 10px;
|
|
}
|
|
|
|
.gradient-bar {
|
|
width: 300px;
|
|
height: 20px;
|
|
border-radius: 4px;
|
|
border: 2px solid #cbd5e1;
|
|
}
|
|
|
|
.gradient-labels {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
font-size: 0.85em;
|
|
color: #64748b;
|
|
margin-top: 4px;
|
|
}
|
|
|
|
footer {
|
|
padding: 25px;
|
|
background: #1e293b;
|
|
color: #e2e8f0;
|
|
line-height: 1.8;
|
|
}
|
|
|
|
footer h3 {
|
|
color: #60a5fa;
|
|
margin-top: 0;
|
|
margin-bottom: 12px;
|
|
}
|
|
|
|
footer p {
|
|
margin: 8px 0;
|
|
}
|
|
|
|
footer strong {
|
|
color: #93c5fd;
|
|
}
|
|
|
|
.stats {
|
|
display: flex;
|
|
gap: 30px;
|
|
margin-top: 15px;
|
|
flex-wrap: wrap;
|
|
}
|
|
|
|
.stat-item {
|
|
background: #334155;
|
|
padding: 12px 20px;
|
|
border-radius: 6px;
|
|
border-left: 4px solid #60a5fa;
|
|
}
|
|
|
|
.stat-label {
|
|
font-size: 0.85em;
|
|
color: #94a3b8;
|
|
margin-bottom: 4px;
|
|
}
|
|
|
|
.stat-value {
|
|
font-size: 1.3em;
|
|
font-weight: 700;
|
|
color: #60a5fa;
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div id="container">
|
|
<header>
|
|
<h1>SDG Network Visualization - Advanced Color Encodings</h1>
|
|
<p class="subtitle">Multi-Dimensional Color Strategy with Visual Hierarchy</p>
|
|
</header>
|
|
|
|
<div id="controls">
|
|
<div class="control-group">
|
|
<label for="fillScheme">Node Fill Color (Category)</label>
|
|
<select id="fillScheme">
|
|
<option value="category10">Category10 (Categorical)</option>
|
|
<option value="accent">Accent (Categorical)</option>
|
|
<option value="dark2">Dark2 (Categorical)</option>
|
|
<option value="set3">Set3 (Categorical)</option>
|
|
</select>
|
|
</div>
|
|
|
|
<div class="control-group">
|
|
<label for="borderScheme">Node Border Color (Magnitude)</label>
|
|
<select id="borderScheme">
|
|
<option value="viridis">Viridis (Sequential)</option>
|
|
<option value="plasma">Plasma (Sequential)</option>
|
|
<option value="inferno">Inferno (Sequential)</option>
|
|
<option value="cividis">Cividis (Sequential)</option>
|
|
</select>
|
|
</div>
|
|
|
|
<div class="control-group">
|
|
<label for="edgeScheme">Edge Color Mode</label>
|
|
<select id="edgeScheme">
|
|
<option value="gradient">Gradient (Source→Target)</option>
|
|
<option value="strength">Strength (Sequential)</option>
|
|
<option value="rdylbu">RdYlBu (Diverging)</option>
|
|
</select>
|
|
</div>
|
|
|
|
<button id="resetBtn">Reset Filters</button>
|
|
</div>
|
|
|
|
<div id="viz-container">
|
|
<svg id="network"></svg>
|
|
</div>
|
|
|
|
<div id="legend-container">
|
|
<div class="legend-section">
|
|
<div class="legend-title">
|
|
<span>🎨</span>
|
|
<span>SDG Goal Categories (Click to Filter)</span>
|
|
</div>
|
|
<div class="legend-items" id="category-legend"></div>
|
|
</div>
|
|
|
|
<div class="legend-section">
|
|
<div class="legend-title">
|
|
<span>📊</span>
|
|
<span>Impact Magnitude (Border Color & Size)</span>
|
|
</div>
|
|
<div class="gradient-legend">
|
|
<div class="gradient-bar" id="magnitude-gradient"></div>
|
|
<div style="flex: 1;">
|
|
<div class="gradient-labels">
|
|
<span>Low Impact</span>
|
|
<span>Medium Impact</span>
|
|
<span>High Impact</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="legend-section">
|
|
<div class="legend-title">
|
|
<span>💫</span>
|
|
<span>Data Quality (Opacity)</span>
|
|
</div>
|
|
<div class="gradient-legend">
|
|
<div style="display: flex; gap: 20px; align-items: center;">
|
|
<div style="display: flex; align-items: center; gap: 8px;">
|
|
<div style="width: 30px; height: 30px; background: #3b82f6; opacity: 0.4; border-radius: 50%; border: 2px solid #1e40af;"></div>
|
|
<span style="font-size: 0.9em; color: #64748b;">Low Quality (40%)</span>
|
|
</div>
|
|
<div style="display: flex; align-items: center; gap: 8px;">
|
|
<div style="width: 30px; height: 30px; background: #3b82f6; opacity: 0.7; border-radius: 50%; border: 2px solid #1e40af;"></div>
|
|
<span style="font-size: 0.9em; color: #64748b;">Medium Quality (70%)</span>
|
|
</div>
|
|
<div style="display: flex; align-items: center; gap: 8px;">
|
|
<div style="width: 30px; height: 30px; background: #3b82f6; opacity: 1.0; border-radius: 50%; border: 2px solid #1e40af;"></div>
|
|
<span style="font-size: 0.9em; color: #64748b;">High Quality (100%)</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<footer>
|
|
<h3>Advanced Color Encoding Strategy</h3>
|
|
|
|
<p><strong>Multi-Attribute Encoding:</strong></p>
|
|
<ul style="margin: 8px 0; padding-left: 20px;">
|
|
<li><strong>Node Fill Color:</strong> SDG category (categorical - d3.schemeCategory10 default)</li>
|
|
<li><strong>Node Border Color:</strong> Impact magnitude (sequential - d3.interpolateViridis default)</li>
|
|
<li><strong>Node Opacity:</strong> Data quality/confidence (0.4-1.0 range)</li>
|
|
<li><strong>Node Size:</strong> Impact magnitude (radius 8-28px)</li>
|
|
<li><strong>Edge Color:</strong> Dynamic gradients from source to target node colors</li>
|
|
<li><strong>Edge Opacity:</strong> Connection strength (0.2-0.8 range)</li>
|
|
</ul>
|
|
|
|
<p><strong>Web Learning Source:</strong> Observable HQ - D3 Color Legend (@d3/color-legend)</p>
|
|
<p><strong>Techniques Applied:</strong></p>
|
|
<ul style="margin: 8px 0; padding-left: 20px;">
|
|
<li>Perceptually uniform color scales (Viridis, Plasma, Cividis) for sequential encoding</li>
|
|
<li>Categorical color schemes (Category10, Accent, Dark2) for SDG categories</li>
|
|
<li>Custom interactive legends with click-to-filter functionality</li>
|
|
<li>Gradient bars for continuous scale visualization</li>
|
|
<li>Multi-dimensional encoding: fill + border + opacity + size</li>
|
|
<li>Dynamic color scheme switching for real-time exploration</li>
|
|
</ul>
|
|
|
|
<p><strong>Data Source:</strong> Embedded SDG network data (17 goals, 45+ connections) - Zero API latency</p>
|
|
|
|
<div class="stats">
|
|
<div class="stat-item">
|
|
<div class="stat-label">Color Dimensions</div>
|
|
<div class="stat-value">4 Attributes</div>
|
|
</div>
|
|
<div class="stat-item">
|
|
<div class="stat-label">Color Schemes</div>
|
|
<div class="stat-value">8+ Options</div>
|
|
</div>
|
|
<div class="stat-item">
|
|
<div class="stat-label">Interactive Elements</div>
|
|
<div class="stat-value">Legends + Filters</div>
|
|
</div>
|
|
<div class="stat-item">
|
|
<div class="stat-label">Load Time</div>
|
|
<div class="stat-value"><1 Second</div>
|
|
</div>
|
|
</div>
|
|
|
|
<p style="margin-top: 20px; padding-top: 15px; border-top: 1px solid #475569; font-size: 0.9em; color: #94a3b8;">
|
|
SDG Network Visualization - Iteration 5 | Generated with web-enhanced learning |
|
|
Demonstrates advanced D3.js color encoding strategies
|
|
</p>
|
|
</footer>
|
|
</div>
|
|
|
|
<script>
|
|
// Embedded SDG Network Data with Multi-Dimensional Attributes
|
|
const sdgData = {
|
|
nodes: [
|
|
{ id: 'SDG1', name: 'No Poverty', category: 'Social', magnitude: 95, quality: 0.9 },
|
|
{ id: 'SDG2', name: 'Zero Hunger', category: 'Social', magnitude: 88, quality: 0.85 },
|
|
{ id: 'SDG3', name: 'Good Health', category: 'Social', magnitude: 92, quality: 0.95 },
|
|
{ id: 'SDG4', name: 'Quality Education', category: 'Social', magnitude: 85, quality: 0.8 },
|
|
{ id: 'SDG5', name: 'Gender Equality', category: 'Social', magnitude: 78, quality: 0.75 },
|
|
{ id: 'SDG6', name: 'Clean Water', category: 'Environmental', magnitude: 82, quality: 0.88 },
|
|
{ id: 'SDG7', name: 'Clean Energy', category: 'Environmental', magnitude: 90, quality: 0.92 },
|
|
{ id: 'SDG8', name: 'Economic Growth', category: 'Economic', magnitude: 87, quality: 0.85 },
|
|
{ id: 'SDG9', name: 'Innovation', category: 'Economic', magnitude: 75, quality: 0.7 },
|
|
{ id: 'SDG10', name: 'Reduced Inequalities', category: 'Social', magnitude: 70, quality: 0.65 },
|
|
{ id: 'SDG11', name: 'Sustainable Cities', category: 'Environmental', magnitude: 80, quality: 0.78 },
|
|
{ id: 'SDG12', name: 'Responsible Consumption', category: 'Environmental', magnitude: 72, quality: 0.68 },
|
|
{ id: 'SDG13', name: 'Climate Action', category: 'Environmental', magnitude: 98, quality: 0.98 },
|
|
{ id: 'SDG14', name: 'Life Below Water', category: 'Environmental', magnitude: 76, quality: 0.72 },
|
|
{ id: 'SDG15', name: 'Life On Land', category: 'Environmental', magnitude: 79, quality: 0.75 },
|
|
{ id: 'SDG16', name: 'Peace & Justice', category: 'Governance', magnitude: 68, quality: 0.6 },
|
|
{ id: 'SDG17', name: 'Partnerships', category: 'Governance', magnitude: 85, quality: 0.82 }
|
|
],
|
|
links: [
|
|
{ source: 'SDG1', target: 'SDG2', strength: 0.9 },
|
|
{ source: 'SDG1', target: 'SDG3', strength: 0.85 },
|
|
{ source: 'SDG1', target: 'SDG8', strength: 0.8 },
|
|
{ source: 'SDG2', target: 'SDG3', strength: 0.75 },
|
|
{ source: 'SDG2', target: 'SDG6', strength: 0.7 },
|
|
{ source: 'SDG2', target: 'SDG13', strength: 0.65 },
|
|
{ source: 'SDG3', target: 'SDG6', strength: 0.8 },
|
|
{ source: 'SDG3', target: 'SDG11', strength: 0.6 },
|
|
{ source: 'SDG4', target: 'SDG5', strength: 0.7 },
|
|
{ source: 'SDG4', target: 'SDG8', strength: 0.75 },
|
|
{ source: 'SDG4', target: 'SDG10', strength: 0.65 },
|
|
{ source: 'SDG5', target: 'SDG8', strength: 0.6 },
|
|
{ source: 'SDG5', target: 'SDG10', strength: 0.8 },
|
|
{ source: 'SDG6', target: 'SDG7', strength: 0.55 },
|
|
{ source: 'SDG6', target: 'SDG13', strength: 0.7 },
|
|
{ source: 'SDG6', target: 'SDG14', strength: 0.75 },
|
|
{ source: 'SDG7', target: 'SDG9', strength: 0.8 },
|
|
{ source: 'SDG7', target: 'SDG11', strength: 0.7 },
|
|
{ source: 'SDG7', target: 'SDG13', strength: 0.9 },
|
|
{ source: 'SDG8', target: 'SDG9', strength: 0.85 },
|
|
{ source: 'SDG8', target: 'SDG10', strength: 0.65 },
|
|
{ source: 'SDG9', target: 'SDG11', strength: 0.75 },
|
|
{ source: 'SDG9', target: 'SDG12', strength: 0.6 },
|
|
{ source: 'SDG11', target: 'SDG12', strength: 0.7 },
|
|
{ source: 'SDG11', target: 'SDG13', strength: 0.8 },
|
|
{ source: 'SDG12', target: 'SDG13', strength: 0.75 },
|
|
{ source: 'SDG12', target: 'SDG14', strength: 0.65 },
|
|
{ source: 'SDG12', target: 'SDG15', strength: 0.7 },
|
|
{ source: 'SDG13', target: 'SDG14', strength: 0.85 },
|
|
{ source: 'SDG13', target: 'SDG15', strength: 0.9 },
|
|
{ source: 'SDG14', target: 'SDG15', strength: 0.6 },
|
|
{ source: 'SDG16', target: 'SDG1', strength: 0.7 },
|
|
{ source: 'SDG16', target: 'SDG5', strength: 0.75 },
|
|
{ source: 'SDG16', target: 'SDG10', strength: 0.8 },
|
|
{ source: 'SDG17', target: 'SDG1', strength: 0.65 },
|
|
{ source: 'SDG17', target: 'SDG8', strength: 0.7 },
|
|
{ source: 'SDG17', target: 'SDG9', strength: 0.75 },
|
|
{ source: 'SDG17', target: 'SDG13', strength: 0.8 },
|
|
{ source: 'SDG17', target: 'SDG16', strength: 0.85 }
|
|
]
|
|
};
|
|
|
|
// Color scheme definitions
|
|
const colorSchemes = {
|
|
fill: {
|
|
category10: d3.schemeCategory10,
|
|
accent: d3.schemeAccent,
|
|
dark2: d3.schemeDark2,
|
|
set3: d3.schemeSet3
|
|
},
|
|
border: {
|
|
viridis: d3.interpolateViridis,
|
|
plasma: d3.interpolatePlasma,
|
|
inferno: d3.interpolateInferno,
|
|
cividis: d3.interpolateCividis
|
|
},
|
|
edge: {
|
|
rdylbu: d3.interpolateRdYlBu
|
|
}
|
|
};
|
|
|
|
// Visualization state
|
|
let currentFillScheme = 'category10';
|
|
let currentBorderScheme = 'viridis';
|
|
let currentEdgeScheme = 'gradient';
|
|
let filteredCategories = new Set();
|
|
|
|
// SVG dimensions
|
|
const width = 1200;
|
|
const height = 700;
|
|
|
|
// Create SVG
|
|
const svg = d3.select('#network')
|
|
.attr('width', width)
|
|
.attr('height', height);
|
|
|
|
// Create gradient definitions for edges
|
|
const defs = svg.append('defs');
|
|
|
|
// Force simulation
|
|
const simulation = d3.forceSimulation(sdgData.nodes)
|
|
.force('link', d3.forceLink(sdgData.links)
|
|
.id(d => d.id)
|
|
.distance(d => 120 - (d.strength * 40)))
|
|
.force('charge', d3.forceManyBody().strength(-400))
|
|
.force('center', d3.forceCenter(width / 2, height / 2))
|
|
.force('collision', d3.forceCollide().radius(d => getNodeRadius(d) + 5));
|
|
|
|
// Create link elements
|
|
const linkGroup = svg.append('g').attr('class', 'links');
|
|
|
|
// Create node elements
|
|
const nodeGroup = svg.append('g').attr('class', 'nodes');
|
|
|
|
// Helper functions
|
|
function getNodeRadius(d) {
|
|
return 8 + (d.magnitude / 100) * 20; // 8-28px based on magnitude
|
|
}
|
|
|
|
function getCategoryColor(category, scheme) {
|
|
const categories = ['Social', 'Environmental', 'Economic', 'Governance'];
|
|
const index = categories.indexOf(category);
|
|
return colorSchemes.fill[scheme][index % colorSchemes.fill[scheme].length];
|
|
}
|
|
|
|
function getMagnitudeColor(magnitude, scheme) {
|
|
const normalized = magnitude / 100; // 0-1 range
|
|
return colorSchemes.border[scheme](normalized);
|
|
}
|
|
|
|
function createGradientId(link) {
|
|
return `gradient-${link.source.id}-${link.target.id}`;
|
|
}
|
|
|
|
function updateVisualization() {
|
|
// Clear existing elements
|
|
linkGroup.selectAll('*').remove();
|
|
nodeGroup.selectAll('*').remove();
|
|
defs.selectAll('linearGradient').remove();
|
|
|
|
// Create gradients for edges
|
|
if (currentEdgeScheme === 'gradient') {
|
|
sdgData.links.forEach(link => {
|
|
const gradient = defs.append('linearGradient')
|
|
.attr('id', createGradientId(link))
|
|
.attr('gradientUnits', 'userSpaceOnUse');
|
|
|
|
gradient.append('stop')
|
|
.attr('offset', '0%')
|
|
.attr('stop-color', getCategoryColor(link.source.category, currentFillScheme));
|
|
|
|
gradient.append('stop')
|
|
.attr('offset', '100%')
|
|
.attr('stop-color', getCategoryColor(link.target.category, currentFillScheme));
|
|
});
|
|
}
|
|
|
|
// Draw links
|
|
const links = linkGroup.selectAll('line')
|
|
.data(sdgData.links)
|
|
.join('line')
|
|
.attr('class', 'link')
|
|
.attr('stroke-width', d => 1 + (d.strength * 3))
|
|
.style('stroke-opacity', d => 0.2 + (d.strength * 0.6));
|
|
|
|
// Set link colors based on scheme
|
|
if (currentEdgeScheme === 'gradient') {
|
|
links.attr('stroke', d => `url(#${createGradientId(d)})`);
|
|
} else if (currentEdgeScheme === 'strength') {
|
|
links.attr('stroke', d => colorSchemes.border.viridis(d.strength));
|
|
} else if (currentEdgeScheme === 'rdylbu') {
|
|
links.attr('stroke', d => colorSchemes.edge.rdylbu(d.strength));
|
|
}
|
|
|
|
// Draw nodes
|
|
const nodes = nodeGroup.selectAll('g.node')
|
|
.data(sdgData.nodes)
|
|
.join('g')
|
|
.attr('class', 'node')
|
|
.call(d3.drag()
|
|
.on('start', dragstarted)
|
|
.on('drag', dragged)
|
|
.on('end', dragended));
|
|
|
|
// Node circles
|
|
nodes.append('circle')
|
|
.attr('class', 'node-circle')
|
|
.attr('r', getNodeRadius)
|
|
.attr('fill', d => getCategoryColor(d.category, currentFillScheme))
|
|
.attr('stroke', d => getMagnitudeColor(d.magnitude, currentBorderScheme))
|
|
.style('opacity', d => d.quality);
|
|
|
|
// Node labels
|
|
nodes.append('text')
|
|
.attr('class', 'node-label')
|
|
.attr('dy', d => getNodeRadius(d) + 14)
|
|
.text(d => d.name);
|
|
|
|
// Apply filters
|
|
if (filteredCategories.size > 0) {
|
|
nodes.style('display', d => filteredCategories.has(d.category) ? 'none' : 'block');
|
|
links.style('display', d => {
|
|
return filteredCategories.has(d.source.category) ||
|
|
filteredCategories.has(d.target.category) ? 'none' : 'block';
|
|
});
|
|
}
|
|
|
|
// Update positions on simulation tick
|
|
simulation.on('tick', () => {
|
|
links
|
|
.attr('x1', d => d.source.x)
|
|
.attr('y1', d => d.source.y)
|
|
.attr('x2', d => d.target.x)
|
|
.attr('y2', d => d.target.y);
|
|
|
|
// Update gradient positions
|
|
if (currentEdgeScheme === 'gradient') {
|
|
defs.selectAll('linearGradient').each(function(_, i) {
|
|
const link = sdgData.links[i];
|
|
d3.select(this)
|
|
.attr('x1', link.source.x)
|
|
.attr('y1', link.source.y)
|
|
.attr('x2', link.target.x)
|
|
.attr('y2', link.target.y);
|
|
});
|
|
}
|
|
|
|
nodes.attr('transform', d => `translate(${d.x},${d.y})`);
|
|
});
|
|
}
|
|
|
|
// Drag functions
|
|
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;
|
|
}
|
|
|
|
// Create legends
|
|
function createCategoryLegend() {
|
|
const categories = [...new Set(sdgData.nodes.map(d => d.category))];
|
|
const legendContainer = d3.select('#category-legend');
|
|
|
|
legendContainer.selectAll('.legend-item')
|
|
.data(categories)
|
|
.join('div')
|
|
.attr('class', 'legend-item')
|
|
.classed('filtered', d => filteredCategories.has(d))
|
|
.on('click', function(event, category) {
|
|
if (filteredCategories.has(category)) {
|
|
filteredCategories.delete(category);
|
|
} else {
|
|
filteredCategories.add(category);
|
|
}
|
|
createCategoryLegend();
|
|
updateVisualization();
|
|
})
|
|
.html(d => `
|
|
<div class="legend-swatch" style="background: ${getCategoryColor(d, currentFillScheme)}"></div>
|
|
<span class="legend-label">${d}</span>
|
|
`);
|
|
}
|
|
|
|
function createMagnitudeGradient() {
|
|
const gradientBar = d3.select('#magnitude-gradient');
|
|
const steps = 20;
|
|
let gradientStr = 'linear-gradient(to right';
|
|
|
|
for (let i = 0; i <= steps; i++) {
|
|
const t = i / steps;
|
|
const color = getMagnitudeColor(t * 100, currentBorderScheme);
|
|
gradientStr += `, ${color} ${(t * 100)}%`;
|
|
}
|
|
gradientStr += ')';
|
|
|
|
gradientBar.style('background', gradientStr);
|
|
}
|
|
|
|
// Event listeners
|
|
d3.select('#fillScheme').on('change', function() {
|
|
currentFillScheme = this.value;
|
|
createCategoryLegend();
|
|
updateVisualization();
|
|
});
|
|
|
|
d3.select('#borderScheme').on('change', function() {
|
|
currentBorderScheme = this.value;
|
|
createMagnitudeGradient();
|
|
updateVisualization();
|
|
});
|
|
|
|
d3.select('#edgeScheme').on('change', function() {
|
|
currentEdgeScheme = this.value;
|
|
updateVisualization();
|
|
});
|
|
|
|
d3.select('#resetBtn').on('click', () => {
|
|
filteredCategories.clear();
|
|
createCategoryLegend();
|
|
updateVisualization();
|
|
});
|
|
|
|
// Initialize
|
|
createCategoryLegend();
|
|
createMagnitudeGradient();
|
|
updateVisualization();
|
|
</script>
|
|
</body>
|
|
</html> |