infinite-agents-public/sdg_viz/sdg_viz_2.html

666 lines
24 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 2: Environmental Indicators Network</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, #2c3e50 0%, #34495e 100%);
color: white;
padding: 30px;
text-align: center;
}
.header h1 {
margin: 0 0 10px 0;
font-size: 2.5em;
font-weight: 300;
}
.header p {
margin: 0;
opacity: 0.9;
font-size: 1.1em;
}
#network {
background: #f8f9fa;
position: relative;
}
.controls {
padding: 20px 30px;
background: #ecf0f1;
border-bottom: 2px solid #bdc3c7;
display: flex;
gap: 20px;
align-items: center;
flex-wrap: wrap;
}
.controls label {
font-weight: 600;
color: #2c3e50;
}
.controls select, .controls button {
padding: 8px 16px;
border: 2px solid #3498db;
border-radius: 6px;
background: white;
color: #2c3e50;
font-size: 14px;
cursor: pointer;
transition: all 0.3s;
}
.controls button {
background: #3498db;
color: white;
font-weight: 600;
}
.controls button:hover {
background: #2980b9;
transform: translateY(-2px);
box-shadow: 0 4px 8px rgba(0,0,0,0.2);
}
.controls select:hover {
border-color: #2980b9;
}
.legend {
padding: 20px 30px;
background: white;
border-top: 2px solid #ecf0f1;
}
.legend h3 {
margin: 0 0 15px 0;
color: #2c3e50;
font-size: 1.3em;
}
.legend-section {
margin-bottom: 20px;
}
.legend-section h4 {
margin: 0 0 10px 0;
color: #34495e;
font-size: 1em;
font-weight: 600;
}
.legend-items {
display: flex;
gap: 20px;
flex-wrap: wrap;
}
.legend-item {
display: flex;
align-items: center;
gap: 8px;
}
.legend-color {
width: 24px;
height: 24px;
border-radius: 50%;
border: 2px solid #34495e;
}
.legend-size {
border-radius: 50%;
border: 2px solid #34495e;
background: #95a5a6;
}
.tooltip {
position: absolute;
background: rgba(44, 62, 80, 0.95);
color: white;
padding: 12px 16px;
border-radius: 8px;
pointer-events: none;
font-size: 13px;
line-height: 1.6;
opacity: 0;
transition: opacity 0.3s;
max-width: 300px;
box-shadow: 0 4px 12px rgba(0,0,0,0.4);
z-index: 1000;
}
.tooltip.visible {
opacity: 1;
}
.tooltip strong {
color: #3498db;
font-size: 14px;
}
.links line {
stroke-opacity: 0.6;
}
.nodes circle {
cursor: pointer;
transition: all 0.3s;
}
.nodes circle:hover {
stroke-width: 4px;
filter: brightness(1.2);
}
.nodes text {
pointer-events: none;
font-size: 11px;
font-weight: 600;
text-shadow: 1px 1px 2px white, -1px -1px 2px white;
}
.footer {
padding: 25px 30px;
background: #34495e;
color: white;
line-height: 1.8;
}
.footer h3 {
margin: 0 0 10px 0;
color: #3498db;
font-size: 1.2em;
}
.footer p {
margin: 5px 0;
}
.footer strong {
color: #ecf0f1;
}
.loading {
text-align: center;
padding: 40px;
font-size: 1.2em;
color: #7f8c8d;
}
.spinner {
border: 4px solid #ecf0f1;
border-top: 4px solid #3498db;
border-radius: 50%;
width: 40px;
height: 40px;
animation: spin 1s linear infinite;
margin: 20px auto;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
</style>
</head>
<body>
<div class="container">
<div class="header">
<h1>Environmental Indicators Network</h1>
<p>Climate & Environment Data from World Bank Open Data API</p>
</div>
<div class="controls">
<label for="yearSelect">Year:</label>
<select id="yearSelect">
<option value="2020">2020</option>
<option value="2019">2019</option>
<option value="2018">2018</option>
<option value="2017">2017</option>
<option value="2016">2016</option>
</select>
<label for="regionSelect">Region Focus:</label>
<select id="regionSelect">
<option value="all">All Regions</option>
<option value="asia">Asia & Pacific</option>
<option value="europe">Europe & Central Asia</option>
<option value="africa">Africa</option>
<option value="americas">Americas</option>
</select>
<button id="resetBtn">Reset View</button>
</div>
<div id="network">
<div class="loading">
<div class="spinner"></div>
Loading environmental data from World Bank API...
</div>
</div>
<div class="legend">
<h3>Legend</h3>
<div class="legend-section">
<h4>Node Colors (Indicator Categories)</h4>
<div class="legend-items" id="colorLegend"></div>
</div>
<div class="legend-section">
<h4>Node Sizes (Data Magnitude)</h4>
<div class="legend-items">
<div class="legend-item">
<div class="legend-size" style="width: 10px; height: 10px;"></div>
<span>Low values</span>
</div>
<div class="legend-item">
<div class="legend-size" style="width: 18px; height: 18px;"></div>
<span>Medium values</span>
</div>
<div class="legend-item">
<div class="legend-size" style="width: 26px; height: 26px;"></div>
<span>High values</span>
</div>
</div>
</div>
<div class="legend-section">
<h4>Edge Width (Correlation Strength)</h4>
<div class="legend-items">
<div class="legend-item">
<svg width="60" height="4"><line x1="0" y1="2" x2="60" y2="2" stroke="#95a5a6" stroke-width="1"/></svg>
<span>Weak correlation</span>
</div>
<div class="legend-item">
<svg width="60" height="6"><line x1="0" y1="3" x2="60" y2="3" stroke="#95a5a6" stroke-width="3"/></svg>
<span>Moderate correlation</span>
</div>
<div class="legend-item">
<svg width="60" height="8"><line x1="0" y1="4" x2="60" y2="4" stroke="#95a5a6" stroke-width="5"/></svg>
<span>Strong correlation</span>
</div>
</div>
</div>
</div>
<div class="footer">
<h3>Data Source & Learning Documentation</h3>
<p><strong>API Source:</strong> World Bank Open Data API (https://api.worldbank.org/v2/)</p>
<p><strong>Indicators:</strong> CO2 Emissions (EN.ATM.CO2E.PC), Forest Area (AG.LND.FRST.ZS), Renewable Energy (EG.FEC.RNEW.ZS), Access to Electricity (EG.ELC.ACCS.ZS), PM2.5 Air Pollution (EN.ATM.PM25.MC.M3)</p>
<p><strong>Web Learning Source:</strong> https://d3-graph-gallery.com/network.html</p>
<p><strong>Techniques Applied:</strong></p>
<ul style="margin: 5px 0; padding-left: 20px;">
<li><strong>Color Scales:</strong> d3.scaleOrdinal() with d3.schemeCategory10 for categorical indicator types</li>
<li><strong>Node Sizing:</strong> d3.scaleLinear() for proportional sizing based on data magnitude (5-25px range)</li>
<li><strong>Force Simulation:</strong> Multi-body forces, link forces, collision detection, and centering</li>
<li><strong>Visual Encodings:</strong> Size encodes value, color encodes category, edge width encodes correlation strength</li>
<li><strong>Edge Styling:</strong> Curved paths with variable stroke-width based on relationship strength</li>
<li><strong>Interactivity:</strong> Drag simulation, hover tooltips with rich data, zoom/pan behavior</li>
</ul>
<p><strong>Network Structure:</strong> Countries as nodes, environmental indicator correlations as edges. Nodes sized by normalized indicator values, colored by indicator category, connected when indicators show regional correlations.</p>
</div>
</div>
<div class="tooltip" id="tooltip"></div>
<script>
// Configuration
const width = 1400;
const height = 800;
// Color scale for indicator categories (d3.scaleOrdinal learned from web source)
const colorScale = d3.scaleOrdinal()
.domain(['Climate', 'Energy', 'Forest', 'Pollution', 'Other'])
.range(['#e74c3c', '#f39c12', '#27ae60', '#8e44ad', '#3498db']);
// Node size scale (d3.scaleLinear learned from web source)
const sizeScale = d3.scaleLinear()
.domain([0, 100])
.range([5, 25]);
// Edge width scale
const edgeWidthScale = d3.scaleLinear()
.domain([0, 1])
.range([1, 5]);
// Environmental indicators from World Bank API
const indicators = [
{ code: 'EN.ATM.CO2E.PC', name: 'CO2 Emissions (metric tons per capita)', category: 'Climate' },
{ code: 'AG.LND.FRST.ZS', name: 'Forest Area (% of land)', category: 'Forest' },
{ code: 'EG.FEC.RNEW.ZS', name: 'Renewable Energy (% of total)', category: 'Energy' },
{ code: 'EG.ELC.ACCS.ZS', name: 'Access to Electricity (% of population)', category: 'Energy' },
{ code: 'EN.ATM.PM25.MC.M3', name: 'PM2.5 Air Pollution (μg/m³)', category: 'Pollution' }
];
// Selected countries for network
const countries = [
{ code: 'USA', name: 'United States', region: 'americas' },
{ code: 'CHN', name: 'China', region: 'asia' },
{ code: 'IND', name: 'India', region: 'asia' },
{ code: 'BRA', name: 'Brazil', region: 'americas' },
{ code: 'DEU', name: 'Germany', region: 'europe' },
{ code: 'GBR', name: 'United Kingdom', region: 'europe' },
{ code: 'JPN', name: 'Japan', region: 'asia' },
{ code: 'ZAF', name: 'South Africa', region: 'africa' },
{ code: 'KEN', name: 'Kenya', region: 'africa' },
{ code: 'AUS', name: 'Australia', region: 'asia' },
{ code: 'CAN', name: 'Canada', region: 'americas' },
{ code: 'FRA', name: 'France', region: 'europe' },
{ code: 'IDN', name: 'Indonesia', region: 'asia' },
{ code: 'MEX', name: 'Mexico', region: 'americas' },
{ code: 'NGA', name: 'Nigeria', region: 'africa' }
];
// Create color legend
const colorLegend = d3.select('#colorLegend');
colorScale.domain().forEach(category => {
const item = colorLegend.append('div')
.attr('class', 'legend-item');
item.append('div')
.attr('class', 'legend-color')
.style('background-color', colorScale(category));
item.append('span').text(category);
});
// Tooltip
const tooltip = d3.select('#tooltip');
// Main data fetching and visualization
async function fetchIndicatorData(indicatorCode, year) {
const countryCodes = countries.map(c => c.code).join(';');
const url = `https://api.worldbank.org/v2/country/${countryCodes}/indicator/${indicatorCode}?date=${year}&format=json&per_page=100`;
try {
const response = await fetch(url);
const data = await response.json();
return data[1] || []; // World Bank API returns [metadata, data]
} catch (error) {
console.error(`Error fetching ${indicatorCode}:`, error);
return [];
}
}
async function buildNetworkData(year) {
// Fetch all indicator data
const allData = {};
for (const indicator of indicators) {
const data = await fetchIndicatorData(indicator.code, year);
allData[indicator.code] = data;
}
// Build nodes (countries with their indicator values)
const nodes = [];
const countryData = {};
countries.forEach(country => {
const nodeData = {
id: country.code,
name: country.name,
region: country.region,
indicators: {}
};
indicators.forEach(indicator => {
const countryIndicator = allData[indicator.code].find(d => d.countryiso3code === country.code);
if (countryIndicator && countryIndicator.value !== null) {
nodeData.indicators[indicator.code] = {
value: countryIndicator.value,
name: indicator.name,
category: indicator.category
};
}
});
// Calculate average indicator value for node sizing
const values = Object.values(nodeData.indicators).map(i => i.value);
nodeData.avgValue = values.length > 0 ? d3.mean(values) : 0;
// Assign primary category based on most significant indicator
const primaryIndicator = Object.entries(nodeData.indicators)
.sort((a, b) => b[1].value - a[1].value)[0];
nodeData.category = primaryIndicator ? primaryIndicator[1].category : 'Other';
nodes.push(nodeData);
countryData[country.code] = nodeData;
});
// Build edges (correlations between countries based on similar indicator patterns)
const links = [];
for (let i = 0; i < nodes.length; i++) {
for (let j = i + 1; j < nodes.length; j++) {
const node1 = nodes[i];
const node2 = nodes[j];
// Calculate correlation based on shared indicators
const sharedIndicators = Object.keys(node1.indicators).filter(
k => node2.indicators[k] !== undefined
);
if (sharedIndicators.length >= 2) {
// Simple correlation: inverse of average difference
const differences = sharedIndicators.map(k => {
const v1 = node1.indicators[k].value;
const v2 = node2.indicators[k].value;
return Math.abs(v1 - v2) / Math.max(v1, v2, 1);
});
const avgDiff = d3.mean(differences);
const correlation = Math.max(0, 1 - avgDiff);
// Only add edges with meaningful correlation
if (correlation > 0.3) {
links.push({
source: node1.id,
target: node2.id,
strength: correlation,
sharedCount: sharedIndicators.length
});
}
}
}
}
return { nodes, links };
}
function createVisualization(data) {
// Clear existing visualization
d3.select('#network').html('');
// Create SVG with zoom behavior
const svg = d3.select('#network')
.append('svg')
.attr('width', width)
.attr('height', height)
.attr('viewBox', [0, 0, width, height]);
// Add zoom behavior
const g = svg.append('g');
const zoom = d3.zoom()
.scaleExtent([0.5, 3])
.on('zoom', (event) => {
g.attr('transform', event.transform);
});
svg.call(zoom);
// Force simulation (learned from web source)
const simulation = d3.forceSimulation(data.nodes)
.force('link', d3.forceLink(data.links)
.id(d => d.id)
.distance(100)
.strength(d => d.strength * 0.5))
.force('charge', d3.forceManyBody()
.strength(-400))
.force('center', d3.forceCenter(width / 2, height / 2))
.force('collision', d3.forceCollide()
.radius(d => sizeScale(d.avgValue) + 5));
// Draw edges (curved paths for visual appeal)
const link = g.append('g')
.attr('class', 'links')
.selectAll('path')
.data(data.links)
.join('path')
.attr('stroke', '#95a5a6')
.attr('stroke-width', d => edgeWidthScale(d.strength))
.attr('fill', 'none')
.attr('opacity', 0.6);
// Draw nodes
const node = g.append('g')
.attr('class', 'nodes')
.selectAll('g')
.data(data.nodes)
.join('g')
.call(drag(simulation));
// Node circles with color scale and size scale
node.append('circle')
.attr('r', d => sizeScale(d.avgValue))
.attr('fill', d => colorScale(d.category))
.attr('stroke', '#2c3e50')
.attr('stroke-width', 2);
// Node labels
node.append('text')
.attr('dx', d => sizeScale(d.avgValue) + 5)
.attr('dy', 4)
.text(d => d.name)
.attr('fill', '#2c3e50');
// Hover interactions
node.on('mouseover', function(event, d) {
d3.select(this).select('circle')
.attr('stroke-width', 4);
// Create rich tooltip content
let tooltipHTML = `<strong>${d.name}</strong><br>`;
tooltipHTML += `Region: ${d.region}<br>`;
tooltipHTML += `Category: ${d.category}<br><br>`;
tooltipHTML += `<strong>Environmental Indicators:</strong><br>`;
Object.entries(d.indicators).forEach(([code, info]) => {
tooltipHTML += `${info.name}: ${info.value.toFixed(2)}<br>`;
});
tooltip.html(tooltipHTML)
.style('left', (event.pageX + 10) + 'px')
.style('top', (event.pageY - 10) + 'px')
.classed('visible', true);
})
.on('mouseout', function() {
d3.select(this).select('circle')
.attr('stroke-width', 2);
tooltip.classed('visible', false);
});
// Update positions on simulation tick
simulation.on('tick', () => {
link.attr('d', d => {
// Curved path for edges
const dx = d.target.x - d.source.x;
const dy = d.target.y - d.source.y;
const dr = Math.sqrt(dx * dx + dy * dy) * 2;
return `M${d.source.x},${d.source.y}A${dr},${dr} 0 0,1 ${d.target.x},${d.target.y}`;
});
node.attr('transform', d => `translate(${d.x},${d.y})`);
});
}
// Drag behavior for nodes
function drag(simulation) {
function dragstarted(event) {
if (!event.active) simulation.alphaTarget(0.3).restart();
event.subject.fx = event.subject.x;
event.subject.fy = event.subject.y;
}
function dragged(event) {
event.subject.fx = event.x;
event.subject.fy = event.y;
}
function dragended(event) {
if (!event.active) simulation.alphaTarget(0);
event.subject.fx = null;
event.subject.fy = null;
}
return d3.drag()
.on('start', dragstarted)
.on('drag', dragged)
.on('end', dragended);
}
// Initialize visualization
let currentData = null;
async function loadData() {
const year = document.getElementById('yearSelect').value;
const data = await buildNetworkData(year);
currentData = data;
createVisualization(data);
}
// Event listeners
document.getElementById('yearSelect').addEventListener('change', loadData);
document.getElementById('regionSelect').addEventListener('change', () => {
const region = document.getElementById('regionSelect').value;
if (!currentData) return;
let filteredData;
if (region === 'all') {
filteredData = currentData;
} else {
const filteredNodes = currentData.nodes.filter(n => n.region === region);
const nodeIds = new Set(filteredNodes.map(n => n.id));
const filteredLinks = currentData.links.filter(l =>
nodeIds.has(l.source.id || l.source) && nodeIds.has(l.target.id || l.target)
);
filteredData = { nodes: filteredNodes, links: filteredLinks };
}
createVisualization(filteredData);
});
document.getElementById('resetBtn').addEventListener('click', () => {
document.getElementById('yearSelect').value = '2020';
document.getElementById('regionSelect').value = 'all';
loadData();
});
// Load initial data
loadData();
</script>
</body>
</html>