905 lines
34 KiB
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>
|