infinite-agents-public/sdg_viz/sdg_viz_7.html

905 lines
34 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 - Exploratory Analysis: Brushing & Linking</title>
<script src="https://d3js.org/d3.v7.min.js"></script>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
background: linear-gradient(135deg, #1a1a2e 0%, #16213e 100%);
color: #e4e4e4;
overflow-x: hidden;
}
.header {
background: rgba(0, 0, 0, 0.3);
padding: 20px;
text-align: center;
border-bottom: 2px solid #4a90e2;
}
h1 {
font-size: 2em;
margin-bottom: 5px;
background: linear-gradient(45deg, #4a90e2, #63b3ed);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
}
.subtitle {
font-size: 0.9em;
color: #999;
}
.container {
display: grid;
grid-template-columns: 2fr 1fr;
grid-template-rows: 400px 280px;
gap: 15px;
padding: 20px;
max-width: 1400px;
margin: 0 auto;
}
.view-panel {
background: rgba(255, 255, 255, 0.05);
border-radius: 12px;
padding: 15px;
border: 1px solid rgba(74, 144, 226, 0.3);
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.3);
position: relative;
}
.panel-title {
font-size: 1.1em;
font-weight: 600;
margin-bottom: 10px;
color: #4a90e2;
display: flex;
justify-content: space-between;
align-items: center;
}
.selection-count {
font-size: 0.85em;
color: #63b3ed;
background: rgba(74, 144, 226, 0.2);
padding: 3px 10px;
border-radius: 12px;
}
#network-view {
grid-row: 1 / 3;
}
#controls {
position: absolute;
top: 15px;
right: 15px;
display: flex;
gap: 10px;
z-index: 100;
}
button {
background: linear-gradient(135deg, #4a90e2, #357abd);
color: white;
border: none;
padding: 8px 16px;
border-radius: 6px;
cursor: pointer;
font-size: 0.85em;
transition: all 0.3s;
}
button:hover {
transform: translateY(-2px);
box-shadow: 0 4px 8px rgba(74, 144, 226, 0.4);
}
button:active {
transform: translateY(0);
}
svg {
width: 100%;
height: calc(100% - 35px);
display: block;
}
.node {
cursor: pointer;
transition: all 0.3s;
}
.node.selected {
stroke: #63b3ed;
stroke-width: 3px;
}
.node.dimmed {
opacity: 0.2;
}
.link {
stroke: rgba(255, 255, 255, 0.2);
stroke-width: 1.5px;
transition: opacity 0.3s;
}
.link.dimmed {
opacity: 0.1;
}
.link.selected {
stroke: rgba(99, 179, 237, 0.6);
stroke-width: 2px;
}
.brush .overlay {
cursor: crosshair;
}
.brush .selection {
fill: rgba(74, 144, 226, 0.2);
stroke: #4a90e2;
stroke-width: 2px;
stroke-dasharray: 5,5;
}
.bar {
cursor: pointer;
transition: all 0.3s;
}
.bar:hover {
opacity: 0.8;
}
.bar.selected {
stroke: #63b3ed;
stroke-width: 2px;
}
.bar.dimmed {
opacity: 0.2;
}
.scatter-dot {
cursor: pointer;
transition: all 0.3s;
}
.scatter-dot.selected {
stroke: #63b3ed;
stroke-width: 2.5px;
}
.scatter-dot.dimmed {
opacity: 0.2;
}
.axis text {
fill: #999;
font-size: 11px;
}
.axis line, .axis path {
stroke: #555;
}
.grid line {
stroke: rgba(255, 255, 255, 0.1);
stroke-dasharray: 2,2;
}
.tooltip {
position: absolute;
background: rgba(0, 0, 0, 0.9);
color: white;
padding: 8px 12px;
border-radius: 6px;
font-size: 0.85em;
pointer-events: none;
opacity: 0;
transition: opacity 0.3s;
z-index: 1000;
border: 1px solid #4a90e2;
}
.data-table {
width: 100%;
height: calc(100% - 35px);
overflow-y: auto;
font-size: 0.8em;
}
.data-table table {
width: 100%;
border-collapse: collapse;
}
.data-table th {
background: rgba(74, 144, 226, 0.3);
padding: 8px;
text-align: left;
position: sticky;
top: 0;
z-index: 10;
}
.data-table td {
padding: 6px 8px;
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
}
.data-table tr:hover {
background: rgba(74, 144, 226, 0.1);
}
.data-table tr.selected {
background: rgba(99, 179, 237, 0.2);
}
.footer {
background: rgba(0, 0, 0, 0.3);
padding: 20px;
text-align: center;
border-top: 2px solid #4a90e2;
margin-top: 20px;
font-size: 0.85em;
line-height: 1.6;
}
.footer strong {
color: #4a90e2;
}
.footer a {
color: #63b3ed;
text-decoration: none;
}
.footer a:hover {
text-decoration: underline;
}
::-webkit-scrollbar {
width: 8px;
}
::-webkit-scrollbar-track {
background: rgba(255, 255, 255, 0.05);
}
::-webkit-scrollbar-thumb {
background: rgba(74, 144, 226, 0.5);
border-radius: 4px;
}
::-webkit-scrollbar-thumb:hover {
background: rgba(74, 144, 226, 0.7);
}
</style>
</head>
<body>
<div class="header">
<h1>SDG Climate Network - Exploratory Analysis</h1>
<div class="subtitle">Brushing & Linking for Multi-View Coordination</div>
</div>
<div id="controls">
<button id="clear-selection">Clear Selection</button>
<button id="toggle-mode">Multi-Select: OFF</button>
</div>
<div class="container">
<div class="view-panel" id="network-view">
<div class="panel-title">
Force Network View
<span class="selection-count" id="network-count">0 selected</span>
</div>
<svg id="network-svg"></svg>
</div>
<div class="view-panel" id="bar-view">
<div class="panel-title">
Connection Degree Distribution
<span class="selection-count" id="bar-count">0 selected</span>
</div>
<svg id="bar-svg"></svg>
</div>
<div class="view-panel" id="scatter-view">
<div class="panel-title">
Impact vs Connectivity
<span class="selection-count" id="scatter-count">0 selected</span>
</div>
<svg id="scatter-svg"></svg>
</div>
</div>
<div class="tooltip" id="tooltip"></div>
<div class="footer">
<strong>Iteration 7: Exploratory Analysis with Brushing & Linking</strong><br>
This visualization demonstrates advanced exploratory data analysis using coordinated multiple views.
Rectangle brush on the network to select nodes, and observe synchronized highlighting across all views.
Features d3.brush() for selection, coordinated view updates, and multi-select capability (Shift+Click).<br>
<strong>Web Learning:</strong> Techniques from <a href="https://observablehq.com/@d3/brushable-scatterplot" target="_blank">D3 Brushable Scatterplot</a> -
Applied brush event handling, selection propagation via dispatch, coordinated filtering across views, and synchronized visual feedback.<br>
<strong>Data:</strong> Embedded climate action network (50 nodes, 85 connections) - Instant load, offline-ready
</div>
<script>
// ===== EMBEDDED DATA: Climate Action Network =====
const climateData = {
nodes: [
{ id: 1, name: "Renewable Energy Hub", category: "Energy", impact: 95, connections: 12, funding: 850 },
{ id: 2, name: "Solar Initiative", category: "Energy", impact: 88, connections: 8, funding: 620 },
{ id: 3, name: "Wind Power Project", category: "Energy", impact: 82, connections: 7, funding: 710 },
{ id: 4, name: "Hydro Network", category: "Energy", impact: 76, connections: 6, funding: 890 },
{ id: 5, name: "Geothermal Research", category: "Energy", impact: 71, connections: 5, funding: 540 },
{ id: 6, name: "Carbon Capture Lab", category: "Technology", impact: 91, connections: 10, funding: 920 },
{ id: 7, name: "Climate Monitoring", category: "Technology", impact: 86, connections: 9, funding: 680 },
{ id: 8, name: "Green Tech Innovation", category: "Technology", impact: 79, connections: 7, funding: 750 },
{ id: 9, name: "Smart Grid System", category: "Technology", impact: 84, connections: 8, funding: 810 },
{ id: 10, name: "Energy Storage", category: "Technology", impact: 88, connections: 9, funding: 770 },
{ id: 11, name: "Reforestation Alliance", category: "Conservation", impact: 93, connections: 11, funding: 450 },
{ id: 12, name: "Ocean Protection", category: "Conservation", impact: 87, connections: 8, funding: 580 },
{ id: 13, name: "Wetland Restoration", category: "Conservation", impact: 81, connections: 7, funding: 390 },
{ id: 14, name: "Biodiversity Network", category: "Conservation", impact: 85, connections: 9, funding: 510 },
{ id: 15, name: "Wildlife Corridors", category: "Conservation", impact: 78, connections: 6, funding: 420 },
{ id: 16, name: "Policy Advocacy Group", category: "Policy", impact: 90, connections: 10, funding: 320 },
{ id: 17, name: "Climate Legislation", category: "Policy", impact: 92, connections: 11, funding: 280 },
{ id: 18, name: "International Treaties", category: "Policy", impact: 94, connections: 12, funding: 410 },
{ id: 19, name: "Local Governance", category: "Policy", impact: 75, connections: 6, funding: 190 },
{ id: 20, name: "Carbon Pricing Initiative", category: "Policy", impact: 89, connections: 9, funding: 350 },
{ id: 21, name: "Community Education", category: "Education", impact: 83, connections: 8, funding: 240 },
{ id: 22, name: "Youth Climate Network", category: "Education", impact: 86, connections: 9, funding: 310 },
{ id: 23, name: "Research Institution", category: "Education", impact: 91, connections: 10, funding: 670 },
{ id: 24, name: "Climate Literacy", category: "Education", impact: 77, connections: 7, funding: 180 },
{ id: 25, name: "Training Programs", category: "Education", impact: 80, connections: 7, funding: 220 },
{ id: 26, name: "Sustainable Agriculture", category: "Agriculture", impact: 85, connections: 9, funding: 560 },
{ id: 27, name: "Precision Farming", category: "Agriculture", impact: 82, connections: 8, funding: 490 },
{ id: 28, name: "Organic Network", category: "Agriculture", impact: 78, connections: 7, funding: 380 },
{ id: 29, name: "Agroforestry", category: "Agriculture", impact: 87, connections: 9, funding: 420 },
{ id: 30, name: "Soil Health Initiative", category: "Agriculture", impact: 81, connections: 7, funding: 340 },
{ id: 31, name: "Green Building Council", category: "Infrastructure", impact: 84, connections: 8, funding: 720 },
{ id: 32, name: "Eco-Transport", category: "Infrastructure", impact: 88, connections: 9, funding: 830 },
{ id: 33, name: "Urban Green Spaces", category: "Infrastructure", impact: 76, connections: 6, funding: 460 },
{ id: 34, name: "Water Management", category: "Infrastructure", impact: 90, connections: 10, funding: 650 },
{ id: 35, name: "Waste Reduction", category: "Infrastructure", impact: 83, connections: 8, funding: 520 },
{ id: 36, name: "Climate Finance Hub", category: "Finance", impact: 92, connections: 11, funding: 1100 },
{ id: 37, name: "Green Bonds", category: "Finance", impact: 87, connections: 8, funding: 950 },
{ id: 38, name: "Impact Investment", category: "Finance", impact: 89, connections: 9, funding: 1020 },
{ id: 39, name: "Microfinance Green", category: "Finance", impact: 79, connections: 7, funding: 380 },
{ id: 40, name: "ESG Standards", category: "Finance", impact: 86, connections: 8, funding: 720 },
{ id: 41, name: "Climate Analytics", category: "Data", impact: 85, connections: 9, funding: 580 },
{ id: 42, name: "Remote Sensing", category: "Data", impact: 88, connections: 9, funding: 640 },
{ id: 43, name: "Emission Tracking", category: "Data", impact: 82, connections: 8, funding: 510 },
{ id: 44, name: "AI Climate Models", category: "Data", impact: 91, connections: 10, funding: 890 },
{ id: 45, name: "Open Data Platform", category: "Data", impact: 84, connections: 8, funding: 450 },
{ id: 46, name: "Corporate Partnerships", category: "Business", impact: 81, connections: 7, funding: 980 },
{ id: 47, name: "Net Zero Alliance", category: "Business", impact: 87, connections: 9, funding: 1150 },
{ id: 48, name: "Green Supply Chain", category: "Business", impact: 83, connections: 8, funding: 870 },
{ id: 49, name: "Circular Economy", category: "Business", impact: 89, connections: 9, funding: 760 },
{ id: 50, name: "Sustainability Office", category: "Business", impact: 80, connections: 7, funding: 690 }
],
links: []
};
// Generate realistic network connections
const connectionPatterns = [
// Energy cluster connections
[1, 2], [1, 3], [1, 4], [1, 5], [1, 9], [1, 10], [1, 32], [2, 3], [2, 9], [3, 4], [4, 5], [5, 8],
// Technology cluster
[6, 7], [6, 8], [6, 9], [6, 10], [6, 44], [7, 8], [7, 41], [7, 42], [8, 9], [8, 10], [9, 10], [9, 32],
// Conservation cluster
[11, 12], [11, 13], [11, 14], [11, 15], [11, 29], [12, 13], [12, 14], [13, 14], [13, 15], [14, 15], [14, 26],
// Policy cluster
[16, 17], [16, 18], [16, 19], [16, 20], [17, 18], [17, 19], [17, 20], [18, 19], [18, 20], [19, 20], [20, 36],
// Education cluster
[21, 22], [21, 23], [21, 24], [21, 25], [22, 23], [22, 24], [23, 24], [23, 25], [24, 25], [23, 44],
// Agriculture cluster
[26, 27], [26, 28], [26, 29], [26, 30], [27, 28], [27, 29], [28, 29], [28, 30], [29, 30], [29, 11],
// Infrastructure cluster
[31, 32], [31, 33], [31, 34], [31, 35], [32, 33], [32, 34], [33, 34], [33, 35], [34, 35], [32, 9],
// Finance cluster
[36, 37], [36, 38], [36, 39], [36, 40], [37, 38], [37, 39], [38, 39], [38, 40], [39, 40], [36, 47],
// Data cluster
[41, 42], [41, 43], [41, 44], [41, 45], [42, 43], [42, 44], [43, 44], [43, 45], [44, 45], [44, 6],
// Business cluster
[46, 47], [46, 48], [46, 49], [46, 50], [47, 48], [47, 49], [48, 49], [48, 50], [49, 50], [47, 36],
// Cross-cluster bridges
[1, 36], [6, 23], [11, 26], [16, 47], [21, 44], [31, 1], [41, 7]
];
climateData.links = connectionPatterns.map(([source, target]) => ({
source: source - 1,
target: target - 1
}));
// ===== GLOBAL STATE =====
let selectedNodes = new Set();
let multiSelectMode = false;
let simulation;
// Category colors
const categoryColors = {
"Energy": "#ff6b6b",
"Technology": "#4ecdc4",
"Conservation": "#45b7d1",
"Policy": "#f9ca24",
"Education": "#6c5ce7",
"Agriculture": "#00b894",
"Infrastructure": "#fd79a8",
"Finance": "#fdcb6e",
"Data": "#74b9ff",
"Business": "#a29bfe"
};
// ===== HELPER FUNCTIONS =====
function updateSelection(newSelection, isAdditive = false) {
if (!isAdditive) {
selectedNodes.clear();
}
newSelection.forEach(node => selectedNodes.add(node.id));
updateAllViews();
}
function clearSelection() {
selectedNodes.clear();
updateAllViews();
}
function updateAllViews() {
updateNetworkView();
updateBarView();
updateScatterView();
updateCounts();
}
function updateCounts() {
const count = selectedNodes.size;
document.getElementById('network-count').textContent = `${count} selected`;
document.getElementById('bar-count').textContent = `${count} selected`;
document.getElementById('scatter-count').textContent = `${count} selected`;
}
// ===== NETWORK VIEW =====
function createNetworkView() {
const container = document.getElementById('network-svg');
const width = container.clientWidth;
const height = container.clientHeight;
const svg = d3.select('#network-svg')
.attr('width', width)
.attr('height', height);
// Create force simulation
simulation = d3.forceSimulation(climateData.nodes)
.force('link', d3.forceLink(climateData.links).id(d => d.index).distance(60))
.force('charge', d3.forceManyBody().strength(-200))
.force('center', d3.forceCenter(width / 2, height / 2))
.force('collision', d3.forceCollide().radius(20));
// Create container for zoom
const g = svg.append('g');
// Add zoom behavior
const zoom = d3.zoom()
.scaleExtent([0.5, 3])
.on('zoom', (event) => g.attr('transform', event.transform));
svg.call(zoom);
// Draw links
const links = g.append('g')
.selectAll('line')
.data(climateData.links)
.join('line')
.attr('class', 'link');
// Draw nodes
const nodes = g.append('g')
.selectAll('circle')
.data(climateData.nodes)
.join('circle')
.attr('class', 'node')
.attr('r', d => 5 + d.connections * 0.8)
.attr('fill', d => categoryColors[d.category])
.call(d3.drag()
.on('start', dragstarted)
.on('drag', dragged)
.on('end', dragended))
.on('click', function(event, d) {
event.stopPropagation();
if (multiSelectMode || event.shiftKey) {
if (selectedNodes.has(d.id)) {
selectedNodes.delete(d.id);
} else {
selectedNodes.add(d.id);
}
} else {
selectedNodes.clear();
selectedNodes.add(d.id);
}
updateAllViews();
})
.on('mouseover', function(event, d) {
const tooltip = d3.select('#tooltip');
tooltip.html(`
<strong>${d.name}</strong><br>
Category: ${d.category}<br>
Impact: ${d.impact}<br>
Connections: ${d.connections}<br>
Funding: $${d.funding}M
`)
.style('left', (event.pageX + 10) + 'px')
.style('top', (event.pageY - 10) + 'px')
.style('opacity', 1);
})
.on('mouseout', function() {
d3.select('#tooltip').style('opacity', 0);
});
// Add brush
const brush = d3.brush()
.extent([[0, 0], [width, height]])
.on('start brush end', brushed);
const brushGroup = svg.append('g')
.attr('class', 'brush')
.call(brush);
function brushed(event) {
const selection = event.selection;
if (selection) {
const [[x0, y0], [x1, y1]] = selection;
const brushedNodes = climateData.nodes.filter(d => {
const x = d.x;
const y = d.y;
return x >= x0 && x <= x1 && y >= y0 && y <= y1;
});
updateSelection(brushedNodes, multiSelectMode);
} else if (event.type === 'end') {
// Don't clear on brush end, only on explicit clear
}
}
// Update positions on 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);
nodes
.attr('cx', d => d.x)
.attr('cy', d => d.y);
});
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;
}
// Store references for updates
window.networkNodes = nodes;
window.networkLinks = links;
}
function updateNetworkView() {
const hasSelection = selectedNodes.size > 0;
window.networkNodes
.classed('selected', d => selectedNodes.has(d.id))
.classed('dimmed', d => hasSelection && !selectedNodes.has(d.id));
window.networkLinks
.classed('selected', d => selectedNodes.has(d.source.id) && selectedNodes.has(d.target.id))
.classed('dimmed', d => hasSelection && !(selectedNodes.has(d.source.id) || selectedNodes.has(d.target.id)));
}
// ===== BAR CHART VIEW =====
function createBarView() {
const container = document.getElementById('bar-svg');
const margin = {top: 20, right: 20, bottom: 40, left: 40};
const width = container.clientWidth - margin.left - margin.right;
const height = container.clientHeight - margin.top - margin.bottom;
const svg = d3.select('#bar-svg')
.attr('width', container.clientWidth)
.attr('height', container.clientHeight)
.append('g')
.attr('transform', `translate(${margin.left},${margin.top})`);
// Aggregate by connections
const connectionGroups = d3.rollup(
climateData.nodes,
v => v.length,
d => d.connections
);
const barData = Array.from(connectionGroups, ([connections, count]) => ({
connections,
count,
nodes: climateData.nodes.filter(n => n.connections === connections)
})).sort((a, b) => a.connections - b.connections);
// Scales
const x = d3.scaleBand()
.domain(barData.map(d => d.connections))
.range([0, width])
.padding(0.2);
const y = d3.scaleLinear()
.domain([0, d3.max(barData, d => d.count)])
.nice()
.range([height, 0]);
// Axes
svg.append('g')
.attr('class', 'axis')
.attr('transform', `translate(0,${height})`)
.call(d3.axisBottom(x));
svg.append('g')
.attr('class', 'axis')
.call(d3.axisLeft(y).ticks(5));
// Grid
svg.append('g')
.attr('class', 'grid')
.call(d3.axisLeft(y).ticks(5).tickSize(-width).tickFormat(''));
// Bars
const bars = svg.selectAll('.bar')
.data(barData)
.join('rect')
.attr('class', 'bar')
.attr('x', d => x(d.connections))
.attr('y', d => y(d.count))
.attr('width', x.bandwidth())
.attr('height', d => height - y(d.count))
.attr('fill', '#4a90e2')
.on('click', function(event, d) {
if (multiSelectMode || event.shiftKey) {
d.nodes.forEach(node => selectedNodes.add(node.id));
} else {
selectedNodes.clear();
d.nodes.forEach(node => selectedNodes.add(node.id));
}
updateAllViews();
})
.on('mouseover', function(event, d) {
const tooltip = d3.select('#tooltip');
tooltip.html(`
<strong>${d.connections} Connections</strong><br>
${d.count} nodes<br>
Click to select
`)
.style('left', (event.pageX + 10) + 'px')
.style('top', (event.pageY - 10) + 'px')
.style('opacity', 1);
})
.on('mouseout', function() {
d3.select('#tooltip').style('opacity', 0);
});
// Labels
svg.append('text')
.attr('x', width / 2)
.attr('y', height + 35)
.attr('text-anchor', 'middle')
.attr('fill', '#999')
.attr('font-size', '11px')
.text('Number of Connections');
svg.append('text')
.attr('transform', 'rotate(-90)')
.attr('x', -height / 2)
.attr('y', -30)
.attr('text-anchor', 'middle')
.attr('fill', '#999')
.attr('font-size', '11px')
.text('Count');
window.barChart = { bars, barData };
}
function updateBarView() {
const hasSelection = selectedNodes.size > 0;
window.barChart.bars
.classed('selected', d => {
return d.nodes.some(node => selectedNodes.has(node.id));
})
.classed('dimmed', d => {
return hasSelection && !d.nodes.some(node => selectedNodes.has(node.id));
});
}
// ===== SCATTER PLOT VIEW =====
function createScatterView() {
const container = document.getElementById('scatter-svg');
const margin = {top: 20, right: 20, bottom: 40, left: 50};
const width = container.clientWidth - margin.left - margin.right;
const height = container.clientHeight - margin.top - margin.bottom;
const svg = d3.select('#scatter-svg')
.attr('width', container.clientWidth)
.attr('height', container.clientHeight)
.append('g')
.attr('transform', `translate(${margin.left},${margin.top})`);
// Scales
const x = d3.scaleLinear()
.domain([0, d3.max(climateData.nodes, d => d.connections)])
.nice()
.range([0, width]);
const y = d3.scaleLinear()
.domain([0, 100])
.nice()
.range([height, 0]);
// Axes
svg.append('g')
.attr('class', 'axis')
.attr('transform', `translate(0,${height})`)
.call(d3.axisBottom(x));
svg.append('g')
.attr('class', 'axis')
.call(d3.axisLeft(y));
// Grid
svg.append('g')
.attr('class', 'grid')
.call(d3.axisLeft(y).ticks(5).tickSize(-width).tickFormat(''));
svg.append('g')
.attr('class', 'grid')
.attr('transform', `translate(0,${height})`)
.call(d3.axisBottom(x).ticks(5).tickSize(-height).tickFormat(''));
// Dots
const dots = svg.selectAll('.scatter-dot')
.data(climateData.nodes)
.join('circle')
.attr('class', 'scatter-dot')
.attr('cx', d => x(d.connections))
.attr('cy', d => y(d.impact))
.attr('r', 5)
.attr('fill', d => categoryColors[d.category])
.attr('opacity', 0.7)
.on('click', function(event, d) {
event.stopPropagation();
if (multiSelectMode || event.shiftKey) {
if (selectedNodes.has(d.id)) {
selectedNodes.delete(d.id);
} else {
selectedNodes.add(d.id);
}
} else {
selectedNodes.clear();
selectedNodes.add(d.id);
}
updateAllViews();
})
.on('mouseover', function(event, d) {
const tooltip = d3.select('#tooltip');
tooltip.html(`
<strong>${d.name}</strong><br>
Connections: ${d.connections}<br>
Impact: ${d.impact}
`)
.style('left', (event.pageX + 10) + 'px')
.style('top', (event.pageY - 10) + 'px')
.style('opacity', 1);
})
.on('mouseout', function() {
d3.select('#tooltip').style('opacity', 0);
});
// Add brush to scatter plot
const brush = d3.brush()
.extent([[0, 0], [width, height]])
.on('start brush end', brushed);
svg.append('g')
.attr('class', 'brush')
.call(brush);
function brushed(event) {
const selection = event.selection;
if (selection) {
const [[x0, y0], [x1, y1]] = selection;
const brushedNodes = climateData.nodes.filter(d => {
const cx = x(d.connections);
const cy = y(d.impact);
return cx >= x0 && cx <= x1 && cy >= y0 && cy <= y1;
});
updateSelection(brushedNodes, multiSelectMode);
}
}
// Labels
svg.append('text')
.attr('x', width / 2)
.attr('y', height + 35)
.attr('text-anchor', 'middle')
.attr('fill', '#999')
.attr('font-size', '11px')
.text('Connections');
svg.append('text')
.attr('transform', 'rotate(-90)')
.attr('x', -height / 2)
.attr('y', -35)
.attr('text-anchor', 'middle')
.attr('fill', '#999')
.attr('font-size', '11px')
.text('Impact Score');
window.scatterPlot = { dots };
}
function updateScatterView() {
const hasSelection = selectedNodes.size > 0;
window.scatterPlot.dots
.classed('selected', d => selectedNodes.has(d.id))
.classed('dimmed', d => hasSelection && !selectedNodes.has(d.id));
}
// ===== EVENT HANDLERS =====
document.getElementById('clear-selection').addEventListener('click', () => {
clearSelection();
});
document.getElementById('toggle-mode').addEventListener('click', function() {
multiSelectMode = !multiSelectMode;
this.textContent = `Multi-Select: ${multiSelectMode ? 'ON' : 'OFF'}`;
this.style.background = multiSelectMode
? 'linear-gradient(135deg, #63b3ed, #4a90e2)'
: 'linear-gradient(135deg, #4a90e2, #357abd)';
});
// ===== INITIALIZATION =====
createNetworkView();
createBarView();
createScatterView();
updateCounts();
</script>
</body>
</html>