infinite-agents-public/sdg_viz/sdg_viz_10.html

631 lines
20 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters

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

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>SDG Visualization 10 - Practical Bipartite Dashboard</title>
<script src="https://d3js.org/d3.v7.min.js"></script>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Arial', sans-serif;
background: linear-gradient(135deg, #1a1a2e 0%, #16213e 100%);
color: #fff;
overflow: hidden;
}
#container {
width: 100vw;
height: 100vh;
display: flex;
}
#main-area {
flex: 1;
position: relative;
}
svg {
width: 100%;
height: 100%;
cursor: default;
}
.node {
cursor: grab;
stroke-width: 3px;
}
.node:active {
cursor: grabbing;
}
.node-topic {
fill: #0066FF;
stroke: #FFD700;
}
.node-source {
fill: #FF0000;
stroke: #FFFFFF;
}
.node:hover {
filter: brightness(1.3);
}
.link {
stroke: #4fc3f7;
stroke-opacity: 0.4;
stroke-width: 2px;
fill: none;
}
.link:hover {
stroke-opacity: 0.8;
stroke-width: 3px;
}
.node-label {
font-size: 13px;
font-weight: bold;
fill: #ffffff;
text-anchor: middle;
pointer-events: none;
text-shadow: 1px 1px 3px rgba(0, 0, 0, 0.8);
}
#header {
position: absolute;
top: 20px;
left: 20px;
background: rgba(0, 0, 0, 0.85);
padding: 20px 25px;
border-radius: 10px;
border: 2px solid #0066FF;
z-index: 10;
}
#header h1 {
font-size: 22px;
color: #0066FF;
margin-bottom: 8px;
text-transform: uppercase;
letter-spacing: 1px;
}
#header p {
font-size: 13px;
color: #aaa;
margin: 0;
}
#legend {
position: absolute;
top: 20px;
right: 20px;
background: rgba(0, 0, 0, 0.85);
padding: 18px 22px;
border-radius: 10px;
border: 2px solid #FF0000;
z-index: 10;
}
#legend h3 {
font-size: 14px;
color: #FF0000;
margin-bottom: 12px;
text-transform: uppercase;
}
.legend-item {
display: flex;
align-items: center;
margin-bottom: 10px;
font-size: 12px;
}
.legend-circle {
width: 22px;
height: 22px;
border-radius: 50%;
margin-right: 10px;
}
.legend-topic {
background: #0066FF;
border: 2px solid #FFD700;
}
.legend-source {
background: #FF0000;
border: 2px solid #FFFFFF;
}
#side-panel {
width: 350px;
background: rgba(0, 0, 0, 0.95);
border-left: 3px solid #0066FF;
padding: 30px;
overflow-y: auto;
transform: translateX(100%);
transition: transform 0.3s ease;
}
#side-panel.visible {
transform: translateX(0);
}
#side-panel h2 {
font-size: 24px;
color: #0066FF;
margin-bottom: 10px;
}
.panel-type {
font-size: 14px;
color: #FFD700;
margin-bottom: 20px;
text-transform: uppercase;
letter-spacing: 1px;
}
.panel-section {
margin-bottom: 25px;
}
.panel-section h3 {
font-size: 14px;
color: #4fc3f7;
margin-bottom: 12px;
text-transform: uppercase;
}
.connection-item {
background: rgba(255, 255, 255, 0.05);
padding: 10px 12px;
border-radius: 6px;
margin-bottom: 8px;
border-left: 3px solid #0066FF;
font-size: 13px;
}
.close-panel {
position: absolute;
top: 20px;
right: 20px;
background: transparent;
border: 2px solid #FF0000;
color: #FF0000;
font-size: 20px;
width: 35px;
height: 35px;
border-radius: 50%;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.3s;
}
.close-panel:hover {
background: #FF0000;
color: #fff;
}
#footer {
position: absolute;
bottom: 0;
left: 0;
right: 350px;
background: rgba(0, 0, 0, 0.9);
padding: 15px 20px;
font-size: 11px;
color: #aaa;
border-top: 2px solid rgba(0, 102, 255, 0.3);
}
#footer strong {
color: #0066FF;
}
#footer a {
color: #4fc3f7;
text-decoration: none;
}
.tooltip {
position: absolute;
background: rgba(0, 0, 0, 0.95);
padding: 10px 15px;
border-radius: 6px;
border: 2px solid #0066FF;
pointer-events: none;
z-index: 1000;
display: none;
font-size: 13px;
max-width: 250px;
}
.tooltip.visible {
display: block;
}
.tooltip-title {
font-weight: bold;
color: #0066FF;
margin-bottom: 4px;
}
.tooltip-details {
color: #ccc;
font-size: 11px;
}
</style>
</head>
<body>
<div id="container">
<div id="main-area">
<svg id="svg"></svg>
<div id="header">
<h1>SDG Data Dashboard</h1>
<p>Topics ↔ Data Sources</p>
</div>
<div id="legend">
<h3>Node Types</h3>
<div class="legend-item">
<div class="legend-circle legend-topic"></div>
<span>Topics (Blue)</span>
</div>
<div class="legend-item">
<div class="legend-circle legend-source"></div>
<span>Data Sources (Red)</span>
</div>
</div>
<div class="tooltip" id="tooltip">
<div class="tooltip-title"></div>
<div class="tooltip-details"></div>
</div>
<div id="footer">
<strong>Dashboard Features:</strong> Bipartite graph layout • Working drag-and-drop • Instant rendering (no animations) • Click nodes for details • Sonic color scheme (Blue topics, Red data sources)
<br>
<strong>Web Learning:</strong> Bipartite graph design patterns - Two-sided layout positioning, visual separation techniques, connection optimization
</div>
</div>
<div id="side-panel">
<button class="close-panel">×</button>
<h2 id="panel-name">Node Name</h2>
<div class="panel-type" id="panel-type">NODE TYPE</div>
<div class="panel-section">
<h3>Description</h3>
<p id="panel-description">Node description will appear here.</p>
</div>
<div class="panel-section">
<h3>Connections</h3>
<div id="panel-connections"></div>
</div>
</div>
</div>
<script>
// ============================================================================
// DATA STRUCTURE
// ============================================================================
const topics = [
{ id: 't1', name: 'Climate Change', description: 'Global climate action, carbon reduction, and environmental sustainability initiatives' },
{ id: 't2', name: 'Public Health', description: 'Healthcare access, disease prevention, and global health monitoring' },
{ id: 't3', name: 'Education', description: 'Educational access, literacy rates, and learning outcomes worldwide' },
{ id: 't4', name: 'Water Quality', description: 'Clean water access, sanitation, and water resource management' },
{ id: 't5', name: 'Biodiversity', description: 'Species conservation, ecosystem health, and environmental protection' },
{ id: 't6', name: 'Economic Growth', description: 'GDP trends, employment rates, and sustainable economic development' }
];
const sources = [
{ id: 's1', name: 'NOAA API', description: 'National Oceanic and Atmospheric Administration - Climate and weather data' },
{ id: 's2', name: 'WHO Database', description: 'World Health Organization - Global health statistics and disease tracking' },
{ id: 's3', name: 'World Bank', description: 'Economic indicators, development metrics, and poverty data' },
{ id: 's4', name: 'USGS', description: 'US Geological Survey - Water resources and environmental data' },
{ id: 's5', name: 'GBIF', description: 'Global Biodiversity Information Facility - Species occurrence data' },
{ id: 's6', name: 'NASA EarthData', description: 'Earth science data from satellites and research missions' }
];
// Connection mapping: which sources provide data for which topics
const connections = [
{ topic: 't1', source: 's1' },
{ topic: 't1', source: 's6' },
{ topic: 't2', source: 's2' },
{ topic: 't2', source: 's3' },
{ topic: 't3', source: 's3' },
{ topic: 't4', source: 's4' },
{ topic: 't4', source: 's3' },
{ topic: 't5', source: 's5' },
{ topic: 't5', source: 's6' },
{ topic: 't6', source: 's3' },
{ topic: 't1', source: 's4' },
{ topic: 't5', source: 's1' }
];
// ============================================================================
// SVG SETUP
// ============================================================================
const svg = d3.select('#svg');
const width = window.innerWidth - 350; // Account for side panel
const height = window.innerHeight;
const g = svg.append('g');
// ============================================================================
// BIPARTITE LAYOUT POSITIONING
// ============================================================================
const leftX = width * 0.25;
const rightX = width * 0.75;
const nodeRadius = 25;
// Position topics on the left
topics.forEach((topic, i) => {
const spacing = height / (topics.length + 1);
topic.x = leftX;
topic.y = spacing * (i + 1);
topic.fx = leftX; // Fix x position
topic.type = 'topic';
});
// Position sources on the right
sources.forEach((source, i) => {
const spacing = height / (sources.length + 1);
source.x = rightX;
source.y = spacing * (i + 1);
source.fx = rightX; // Fix x position
source.type = 'source';
});
// Combine all nodes
const nodes = [...topics, ...sources];
// Create links with proper references
const links = connections.map(conn => ({
source: nodes.find(n => n.id === conn.topic),
target: nodes.find(n => n.id === conn.source)
}));
// ============================================================================
// D3 FORCE SIMULATION (NO ENTRANCE ANIMATION)
// ============================================================================
const simulation = d3.forceSimulation(nodes)
.force('link', d3.forceLink(links)
.distance(200)
.strength(0.3))
.force('charge', d3.forceManyBody()
.strength(-50))
.force('y', d3.forceY(d => d.y).strength(0.1))
.force('collision', d3.forceCollide().radius(nodeRadius + 5))
.alphaDecay(0.05)
.velocityDecay(0.6);
// ============================================================================
// RENDER LINKS
// ============================================================================
const link = g.append('g')
.selectAll('path')
.data(links)
.join('path')
.attr('class', 'link')
.attr('d', d => {
return `M ${d.source.x} ${d.source.y} L ${d.target.x} ${d.target.y}`;
});
// ============================================================================
// RENDER NODES
// ============================================================================
const node = g.append('g')
.selectAll('circle')
.data(nodes)
.join('circle')
.attr('class', d => `node node-${d.type}`)
.attr('r', nodeRadius)
.attr('cx', d => d.x)
.attr('cy', d => d.y);
// ============================================================================
// RENDER LABELS
// ============================================================================
const labels = g.append('g')
.selectAll('text')
.data(nodes)
.join('text')
.attr('class', 'node-label')
.attr('x', d => d.x)
.attr('y', d => d.y + 4)
.text(d => d.name);
// ============================================================================
// DRAG BEHAVIOR (MUST WORK!)
// ============================================================================
function dragstarted(event, d) {
if (!event.active) simulation.alphaTarget(0.3).restart();
d.fy = d.y; // Only allow vertical dragging (x is fixed)
}
function dragged(event, d) {
d.fy = event.y;
}
function dragended(event, d) {
if (!event.active) simulation.alphaTarget(0);
d.fy = null; // Release vertical constraint when done
}
const drag = d3.drag()
.on('start', dragstarted)
.on('drag', dragged)
.on('end', dragended);
node.call(drag);
// ============================================================================
// SIMULATION TICK (UPDATE POSITIONS)
// ============================================================================
simulation.on('tick', () => {
link.attr('d', d => {
return `M ${d.source.x} ${d.source.y} L ${d.target.x} ${d.target.y}`;
});
node
.attr('cx', d => d.x)
.attr('cy', d => d.y);
labels
.attr('x', d => d.x)
.attr('y', d => d.y + 4);
});
// ============================================================================
// CLICK INTERACTION (SIDE PANEL)
// ============================================================================
const sidePanel = document.getElementById('side-panel');
const panelName = document.getElementById('panel-name');
const panelType = document.getElementById('panel-type');
const panelDescription = document.getElementById('panel-description');
const panelConnections = document.getElementById('panel-connections');
node.on('click', function(event, d) {
event.stopPropagation();
// Update panel content
panelName.textContent = d.name;
panelType.textContent = d.type === 'topic' ? 'TOPIC' : 'DATA SOURCE';
panelType.style.color = d.type === 'topic' ? '#0066FF' : '#FF0000';
panelDescription.textContent = d.description;
// Find connections
const nodeConnections = links.filter(link =>
link.source.id === d.id || link.target.id === d.id
);
panelConnections.innerHTML = '';
if (nodeConnections.length === 0) {
panelConnections.innerHTML = '<p style="color: #666;">No connections</p>';
} else {
nodeConnections.forEach(link => {
const connectedNode = link.source.id === d.id ? link.target : link.source;
const div = document.createElement('div');
div.className = 'connection-item';
div.style.borderLeftColor = connectedNode.type === 'topic' ? '#0066FF' : '#FF0000';
div.textContent = connectedNode.name;
panelConnections.appendChild(div);
});
}
// Show panel
sidePanel.classList.add('visible');
});
// Close panel button
document.querySelector('.close-panel').addEventListener('click', () => {
sidePanel.classList.remove('visible');
});
// Close panel when clicking outside
svg.on('click', () => {
sidePanel.classList.remove('visible');
});
// ============================================================================
// HOVER TOOLTIP
// ============================================================================
const tooltip = document.getElementById('tooltip');
node.on('mouseenter', function(event, d) {
const tooltipTitle = tooltip.querySelector('.tooltip-title');
const tooltipDetails = tooltip.querySelector('.tooltip-details');
tooltipTitle.textContent = d.name;
tooltipDetails.textContent = `${d.type === 'topic' ? 'Topic' : 'Data Source'} • Click for details`;
tooltip.classList.add('visible');
});
node.on('mousemove', function(event) {
tooltip.style.left = (event.pageX + 15) + 'px';
tooltip.style.top = (event.pageY + 15) + 'px';
});
node.on('mouseleave', function() {
tooltip.classList.remove('visible');
});
// ============================================================================
// LINK TOOLTIPS
// ============================================================================
link.on('mouseenter', function(event, d) {
const tooltipTitle = tooltip.querySelector('.tooltip-title');
const tooltipDetails = tooltip.querySelector('.tooltip-details');
tooltipTitle.textContent = 'Connection';
tooltipDetails.textContent = `${d.source.name}${d.target.name}`;
tooltip.classList.add('visible');
});
link.on('mousemove', function(event) {
tooltip.style.left = (event.pageX + 15) + 'px';
tooltip.style.top = (event.pageY + 15) + 'px';
});
link.on('mouseleave', function() {
tooltip.classList.remove('visible');
});
// ============================================================================
// WINDOW RESIZE HANDLER
// ============================================================================
window.addEventListener('resize', () => {
location.reload(); // Simple solution for demo
});
// ============================================================================
// INSTANT RENDER (NO ENTRANCE ANIMATIONS)
// ============================================================================
// Simulation runs a few ticks immediately to settle the layout
for (let i = 0; i < 50; i++) {
simulation.tick();
}
// Update initial positions
link.attr('d', d => `M ${d.source.x} ${d.source.y} L ${d.target.x} ${d.target.y}`);
node.attr('cx', d => d.x).attr('cy', d => d.y);
labels.attr('x', d => d.x).attr('y', d => d.y + 4);
// Then let simulation continue naturally for smooth interactions
simulation.alpha(0.3).restart();
</script>
</body>
</html>