infinite-agents-public/sdg_viz/sdg_viz_12.html

397 lines
13 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 - Iteration 12: Refined Aesthetics & Beautiful Nodes</title>
<script src="https://d3js.org/d3.v7.min.js"></script>
<style>
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;600;700&display=swap');
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Inter', 'Roboto', sans-serif;
background: linear-gradient(135deg, #0a0e27 0%, #1a1f3a 50%, #0a0e27 100%);
color: #ffffff;
overflow: hidden;
}
#container {
width: 100vw;
height: 100vh;
position: relative;
}
svg {
width: 100%;
height: 100%;
display: block;
}
.edge {
fill: none;
stroke-width: 2;
opacity: 0.6;
transition: opacity 0.3s ease, stroke-width 0.3s ease;
}
.edge:hover {
opacity: 1;
stroke-width: 3;
}
.node {
cursor: move;
filter: drop-shadow(0 4px 8px rgba(0, 0, 0, 0.4));
transition: filter 0.3s ease;
}
.node:hover {
filter: drop-shadow(0 8px 16px rgba(0, 0, 0, 0.6));
}
.node-circle {
transition: transform 0.3s ease, filter 0.3s ease;
}
.node:hover .node-circle {
transform: scale(1.2);
filter: brightness(1.2);
}
.node-label {
font-size: 15px;
font-weight: 600;
fill: #ffffff;
text-anchor: middle;
pointer-events: none;
text-shadow: 1px 1px 3px rgba(0, 0, 0, 0.8),
-1px -1px 3px rgba(0, 0, 0, 0.8);
user-select: none;
}
.legend {
position: absolute;
top: 30px;
right: 30px;
background: rgba(15, 20, 40, 0.92);
padding: 20px 25px;
border-radius: 12px;
border: 2px solid rgba(255, 255, 255, 0.1);
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.4);
}
.legend-title {
font-size: 18px;
font-weight: 700;
margin-bottom: 15px;
color: #ffffff;
letter-spacing: 0.5px;
}
.legend-item {
display: flex;
align-items: center;
margin: 12px 0;
font-size: 14px;
font-weight: 500;
}
.legend-color {
width: 28px;
height: 28px;
border-radius: 50%;
margin-right: 12px;
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.3);
}
.legend-color.topic {
background: linear-gradient(135deg, #0066FF 0%, #0044CC 100%);
border: 3px solid #FFD700;
box-shadow: 0 0 15px rgba(0, 102, 255, 0.5);
}
.legend-color.datasource {
background: linear-gradient(135deg, #FF0000 0%, #CC0000 100%);
border: 3px solid #FFFFFF;
box-shadow: 0 0 15px rgba(255, 0, 0, 0.5);
}
.info {
position: absolute;
bottom: 30px;
left: 30px;
font-size: 13px;
color: rgba(255, 255, 255, 0.6);
font-weight: 500;
}
.title {
position: absolute;
top: 30px;
left: 30px;
font-size: 28px;
font-weight: 700;
color: #ffffff;
text-shadow: 2px 2px 8px rgba(0, 0, 0, 0.6);
letter-spacing: 0.5px;
}
</style>
</head>
<body>
<div id="container">
<div class="title">SDG Network Dashboard</div>
<svg id="svg"></svg>
<div class="legend">
<div class="legend-title">Node Types</div>
<div class="legend-item">
<div class="legend-color topic"></div>
<span>SDG Topics</span>
</div>
<div class="legend-item">
<div class="legend-color datasource"></div>
<span>Data Sources</span>
</div>
</div>
<div class="info">Drag nodes to explore connections | Hover for details</div>
</div>
<script>
// Web Source: https://observablehq.com/@d3/color-schemes
// Learning Applied: Color harmony with complementary colors (blue/yellow, red/white)
// Contrast ratios for accessibility, gradient color schemes for depth
const width = window.innerWidth;
const height = window.innerHeight;
const svg = d3.select("#svg")
.attr("viewBox", [0, 0, width, height]);
// Define gradients for nodes
const defs = svg.append("defs");
// Topic gradient (blue)
const topicGradient = defs.append("radialGradient")
.attr("id", "topicGradient");
topicGradient.append("stop")
.attr("offset", "0%")
.attr("stop-color", "#3399FF");
topicGradient.append("stop")
.attr("offset", "100%")
.attr("stop-color", "#0044CC");
// Data source gradient (red)
const datasourceGradient = defs.append("radialGradient")
.attr("id", "datasourceGradient");
datasourceGradient.append("stop")
.attr("offset", "0%")
.attr("stop-color", "#FF3333");
datasourceGradient.append("stop")
.attr("offset", "100%")
.attr("stop-color", "#CC0000");
// Inner glow filter for nodes
const glowFilter = defs.append("filter")
.attr("id", "glow");
glowFilter.append("feGaussianBlur")
.attr("stdDeviation", "3")
.attr("result", "coloredBlur");
const feMerge = glowFilter.append("feMerge");
feMerge.append("feMergeNode")
.attr("in", "coloredBlur");
feMerge.append("feMergeNode")
.attr("in", "SourceGraphic");
// Data: Bipartite network with SDG topics and data sources
const topics = [
{ id: "SDG1", name: "No Poverty", type: "topic" },
{ id: "SDG3", name: "Good Health", type: "topic" },
{ id: "SDG4", name: "Quality Education", type: "topic" },
{ id: "SDG7", name: "Clean Energy", type: "topic" },
{ id: "SDG13", name: "Climate Action", type: "topic" },
{ id: "SDG15", name: "Life on Land", type: "topic" }
];
const dataSources = [
{ id: "UN", name: "UN Statistics", type: "datasource" },
{ id: "WorldBank", name: "World Bank", type: "datasource" },
{ id: "WHO", name: "WHO Data", type: "datasource" },
{ id: "IEA", name: "IEA Reports", type: "datasource" },
{ id: "NASA", name: "NASA Climate", type: "datasource" },
{ id: "FAO", name: "FAO Statistics", type: "datasource" }
];
const nodes = [...topics, ...dataSources];
// Create meaningful connections
const links = [
{ source: "SDG1", target: "UN" },
{ source: "SDG1", target: "WorldBank" },
{ source: "SDG3", target: "WHO" },
{ source: "SDG3", target: "UN" },
{ source: "SDG4", target: "UN" },
{ source: "SDG4", target: "WorldBank" },
{ source: "SDG7", target: "IEA" },
{ source: "SDG7", target: "WorldBank" },
{ source: "SDG13", target: "NASA" },
{ source: "SDG13", target: "IEA" },
{ source: "SDG13", target: "UN" },
{ source: "SDG15", target: "FAO" },
{ source: "SDG15", target: "NASA" },
{ source: "SDG15", target: "UN" }
];
// Bipartite layout with perfect spacing
const leftX = width * 0.25;
const rightX = width * 0.75;
const verticalSpacing = height / (Math.max(topics.length, dataSources.length) + 1);
topics.forEach((node, i) => {
node.x = leftX;
node.y = verticalSpacing * (i + 1.5);
node.fx = leftX;
node.fy = node.y;
});
dataSources.forEach((node, i) => {
node.x = rightX;
node.y = verticalSpacing * (i + 1.5);
node.fx = rightX;
node.fy = node.y;
});
// Create gradient definitions for edges
links.forEach((link, i) => {
const gradientId = `edgeGradient${i}`;
const gradient = defs.append("linearGradient")
.attr("id", gradientId)
.attr("gradientUnits", "userSpaceOnUse");
gradient.append("stop")
.attr("offset", "0%")
.attr("stop-color", "#0066FF")
.attr("stop-opacity", 0.6);
gradient.append("stop")
.attr("offset", "100%")
.attr("stop-color", "#FF0000")
.attr("stop-opacity", 0.6);
link.gradientId = gradientId;
});
// Draw edges
const edgeGroup = svg.append("g").attr("class", "edges");
const edges = edgeGroup.selectAll("path")
.data(links)
.join("path")
.attr("class", "edge")
.attr("stroke", d => `url(#${d.gradientId})`)
.attr("d", d => {
const sourceNode = nodes.find(n => n.id === d.source);
const targetNode = nodes.find(n => n.id === d.target);
const dx = targetNode.x - sourceNode.x;
const dy = targetNode.y - sourceNode.y;
const dr = Math.sqrt(dx * dx + dy * dy) * 0.5;
return `M ${sourceNode.x},${sourceNode.y} Q ${(sourceNode.x + targetNode.x) / 2},${(sourceNode.y + targetNode.y) / 2 - 50} ${targetNode.x},${targetNode.y}`;
});
// Draw nodes
const nodeGroup = svg.append("g").attr("class", "nodes");
const nodeElements = nodeGroup.selectAll("g")
.data(nodes)
.join("g")
.attr("class", "node")
.attr("transform", d => `translate(${d.x},${d.y})`)
.call(d3.drag()
.on("start", dragstarted)
.on("drag", dragged)
.on("end", dragended));
// Large beautiful node circles with borders and effects
nodeElements.append("circle")
.attr("class", "node-circle")
.attr("r", 30)
.attr("fill", d => d.type === "topic" ? "url(#topicGradient)" : "url(#datasourceGradient)")
.attr("stroke", d => d.type === "topic" ? "#FFD700" : "#FFFFFF")
.attr("stroke-width", 4)
.attr("filter", "url(#glow)");
// Node labels
nodeElements.append("text")
.attr("class", "node-label")
.attr("dy", 50)
.text(d => d.name)
.style("font-size", "15px")
.style("font-weight", "600");
// Update edge gradients based on actual positions
function updateEdgeGradients() {
links.forEach((link, i) => {
const sourceNode = nodes.find(n => n.id === link.source);
const targetNode = nodes.find(n => n.id === link.target);
d3.select(`#${link.gradientId}`)
.attr("x1", sourceNode.x)
.attr("y1", sourceNode.y)
.attr("x2", targetNode.x)
.attr("y2", targetNode.y);
});
}
updateEdgeGradients();
// Drag functions with working behavior
function dragstarted(event, d) {
d3.select(this).raise();
d.fx = d.x;
d.fy = d.y;
}
function dragged(event, d) {
d.fx = event.x;
d.fy = event.y;
d.x = event.x;
d.y = event.y;
d3.select(this)
.attr("transform", `translate(${d.x},${d.y})`);
// Update connected edges
edges.attr("d", link => {
const sourceNode = nodes.find(n => n.id === link.source);
const targetNode = nodes.find(n => n.id === link.target);
const dx = targetNode.x - sourceNode.x;
const dy = targetNode.y - sourceNode.y;
return `M ${sourceNode.x},${sourceNode.y} Q ${(sourceNode.x + targetNode.x) / 2},${(sourceNode.y + targetNode.y) / 2 - 50} ${targetNode.x},${targetNode.y}`;
});
updateEdgeGradients();
}
function dragended(event, d) {
// Keep node at dragged position
}
// Hover effects for connected edges
nodeElements.on("mouseenter", function(event, d) {
edges.style("opacity", link => {
return (link.source === d.id || link.target === d.id) ? 1 : 0.2;
});
}).on("mouseleave", function() {
edges.style("opacity", 0.6);
});
</script>
</body>
</html>