infinite-agents-public/sdg_viz/sdg_viz_3.html

1015 lines
36 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 3 - Global Biodiversity Interaction Network</title>
<script src="https://d3js.org/d3.v7.min.js"></script>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
background: linear-gradient(135deg, #0a1f2e 0%, #1a3a4a 100%);
color: #e0e0e0;
overflow: hidden;
}
#container {
display: flex;
height: 100vh;
width: 100vw;
}
#controls {
width: 320px;
background: rgba(26, 58, 74, 0.95);
border-right: 2px solid #2a5a7a;
padding: 20px;
overflow-y: auto;
box-shadow: 2px 0 10px rgba(0, 0, 0, 0.3);
}
#viz-container {
flex: 1;
position: relative;
}
h1 {
font-size: 20px;
color: #4fc3f7;
margin-bottom: 20px;
text-align: center;
text-shadow: 0 2px 4px rgba(0, 0, 0, 0.3);
}
.control-section {
margin-bottom: 25px;
padding: 15px;
background: rgba(42, 90, 122, 0.4);
border-radius: 8px;
border: 1px solid rgba(79, 195, 247, 0.2);
}
.control-section h3 {
font-size: 14px;
color: #81d4fa;
margin-bottom: 12px;
text-transform: uppercase;
letter-spacing: 1px;
}
input[type="text"] {
width: 100%;
padding: 10px;
background: rgba(10, 31, 46, 0.8);
border: 1px solid #2a5a7a;
border-radius: 4px;
color: #e0e0e0;
font-size: 13px;
transition: all 0.3s;
}
input[type="text"]:focus {
outline: none;
border-color: #4fc3f7;
box-shadow: 0 0 8px rgba(79, 195, 247, 0.3);
}
.filter-group {
margin-top: 10px;
}
.filter-checkbox {
display: flex;
align-items: center;
margin: 8px 0;
cursor: pointer;
transition: all 0.2s;
padding: 6px;
border-radius: 4px;
}
.filter-checkbox:hover {
background: rgba(79, 195, 247, 0.1);
}
.filter-checkbox input {
margin-right: 10px;
cursor: pointer;
}
.filter-checkbox label {
cursor: pointer;
font-size: 13px;
flex: 1;
}
.count {
font-size: 11px;
color: #81d4fa;
margin-left: 5px;
}
button {
width: 100%;
padding: 12px;
background: linear-gradient(135deg, #2a5a7a 0%, #1a3a4a 100%);
border: 1px solid #4fc3f7;
border-radius: 6px;
color: #4fc3f7;
font-size: 13px;
font-weight: bold;
cursor: pointer;
transition: all 0.3s;
text-transform: uppercase;
letter-spacing: 1px;
margin-top: 10px;
}
button:hover {
background: linear-gradient(135deg, #4fc3f7 0%, #2a5a7a 100%);
color: #0a1f2e;
box-shadow: 0 4px 12px rgba(79, 195, 247, 0.4);
transform: translateY(-2px);
}
button:active {
transform: translateY(0);
}
#node-details {
margin-top: 20px;
padding: 15px;
background: rgba(42, 90, 122, 0.6);
border-radius: 8px;
border: 1px solid rgba(79, 195, 247, 0.3);
min-height: 100px;
}
#node-details h3 {
font-size: 14px;
color: #81d4fa;
margin-bottom: 10px;
}
.detail-item {
margin: 8px 0;
font-size: 12px;
line-height: 1.6;
}
.detail-label {
color: #81d4fa;
font-weight: bold;
}
.detail-value {
color: #e0e0e0;
}
#tooltip {
position: absolute;
background: rgba(10, 31, 46, 0.95);
border: 2px solid #4fc3f7;
border-radius: 8px;
padding: 12px;
pointer-events: none;
opacity: 0;
transition: opacity 0.2s;
font-size: 12px;
max-width: 250px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.5);
z-index: 1000;
}
.tooltip-title {
font-weight: bold;
color: #4fc3f7;
margin-bottom: 6px;
font-size: 13px;
}
.tooltip-content {
color: #e0e0e0;
line-height: 1.5;
}
.legend {
position: absolute;
top: 20px;
right: 20px;
background: rgba(10, 31, 46, 0.9);
border: 1px solid #2a5a7a;
border-radius: 8px;
padding: 15px;
font-size: 12px;
}
.legend-title {
font-weight: bold;
color: #4fc3f7;
margin-bottom: 10px;
text-align: center;
}
.legend-item {
display: flex;
align-items: center;
margin: 6px 0;
}
.legend-color {
width: 16px;
height: 16px;
border-radius: 50%;
margin-right: 8px;
border: 1px solid rgba(255, 255, 255, 0.3);
}
.stats {
position: absolute;
bottom: 20px;
right: 20px;
background: rgba(10, 31, 46, 0.9);
border: 1px solid #2a5a7a;
border-radius: 8px;
padding: 12px 16px;
font-size: 11px;
color: #81d4fa;
}
.loading {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
font-size: 18px;
color: #4fc3f7;
text-align: center;
}
.spinner {
border: 3px solid rgba(79, 195, 247, 0.3);
border-top: 3px solid #4fc3f7;
border-radius: 50%;
width: 40px;
height: 40px;
animation: spin 1s linear infinite;
margin: 0 auto 15px;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
footer {
position: absolute;
bottom: 0;
left: 320px;
right: 0;
background: rgba(10, 31, 46, 0.95);
border-top: 1px solid #2a5a7a;
padding: 12px 20px;
font-size: 11px;
color: #81d4fa;
line-height: 1.6;
}
footer a {
color: #4fc3f7;
text-decoration: none;
}
footer a:hover {
text-decoration: underline;
}
.highlight {
stroke: #ffeb3b !important;
stroke-width: 3px !important;
}
.dimmed {
opacity: 0.2 !important;
}
.focused {
stroke: #ff5722 !important;
stroke-width: 4px !important;
}
.link-highlighted {
stroke: #ffeb3b !important;
stroke-width: 2px !important;
opacity: 1 !important;
}
@keyframes pulse {
0%, 100% { r: 8; }
50% { r: 12; }
}
.searching {
animation: pulse 1s infinite;
}
</style>
</head>
<body>
<div id="container">
<div id="controls">
<h1>Interactive Controls</h1>
<div class="control-section">
<h3>Search Species</h3>
<input type="text" id="search-input" placeholder="Type species name or taxonomic group...">
<button id="search-btn">Search & Highlight</button>
</div>
<div class="control-section">
<h3>Filter by Taxonomic Rank</h3>
<div class="filter-group" id="rank-filters"></div>
</div>
<div class="control-section">
<h3>Actions</h3>
<button id="reset-btn">Reset View</button>
<button id="zoom-fit-btn">Zoom to Fit</button>
<button id="reheat-btn">Reheat Simulation</button>
</div>
<div id="node-details">
<h3>Node Details</h3>
<p style="font-size: 12px; color: #81d4fa;">Click a node to see details</p>
</div>
</div>
<div id="viz-container">
<div class="loading" id="loading">
<div class="spinner"></div>
Loading biodiversity data from GBIF...
</div>
<svg id="network"></svg>
<div id="tooltip"></div>
<div class="legend" id="legend"></div>
<div class="stats" id="stats"></div>
</div>
</div>
<footer>
<strong>Data Source:</strong> <a href="https://www.gbif.org" target="_blank">GBIF (Global Biodiversity Information Facility) API</a> - Species occurrences and taxonomic hierarchy<br>
<strong>Web Learning:</strong> <a href="https://observablehq.com/@d3/temporal-force-directed-graph" target="_blank">D3 Temporal Force-Directed Graph</a><br>
<strong>Techniques Applied:</strong> Advanced drag behavior with alpha targeting, dynamic graph updates with data joins, interactive node highlighting/focusing, search filtering, click/double-click event handling, smooth transitions, state management for selections
</footer>
<script>
// Configuration
const width = window.innerWidth - 320;
const height = window.innerHeight - 60; // Account for footer
// Create SVG with zoom
const svg = d3.select("#network")
.attr("width", width)
.attr("height", height);
const g = svg.append("g");
// Setup zoom behavior
const zoom = d3.zoom()
.scaleExtent([0.1, 10])
.on("zoom", (event) => {
g.attr("transform", event.transform);
});
svg.call(zoom);
// Tooltip
const tooltip = d3.select("#tooltip");
// State management
let nodes = [];
let links = [];
let simulation;
let selectedNode = null;
let focusedNode = null;
let activeFilters = new Set();
// Color scales
const rankColors = {
'KINGDOM': '#e91e63',
'PHYLUM': '#9c27b0',
'CLASS': '#673ab7',
'ORDER': '#3f51b5',
'FAMILY': '#2196f3',
'GENUS': '#00bcd4',
'SPECIES': '#4caf50'
};
// Fetch biodiversity data from GBIF API
async function fetchBiodiversityData() {
try {
// Fetch endangered species data from multiple taxonomic groups
const taxonomicKeys = [
{ key: 1, name: 'Animalia', type: 'KINGDOM' },
{ key: 44, name: 'Chordata', type: 'PHYLUM' },
{ key: 212, name: 'Mammalia', type: 'CLASS' },
{ key: 359, name: 'Carnivora', type: 'ORDER' },
{ key: 732, name: 'Felidae', type: 'FAMILY' },
{ key: 5219404, name: 'Panthera', type: 'GENUS' },
{ key: 5219426, name: 'Panthera leo', type: 'SPECIES' },
{ key: 5219427, name: 'Panthera tigris', type: 'SPECIES' },
{ key: 5219234, name: 'Panthera pardus', type: 'SPECIES' },
{ key: 1, name: 'Primates', type: 'ORDER' },
{ key: 780, name: 'Hominidae', type: 'FAMILY' },
{ key: 2436435, name: 'Gorilla', type: 'GENUS' },
{ key: 5219173, name: 'Pongo', type: 'GENUS' },
{ key: 797, name: 'Cetacea', type: 'ORDER' },
{ key: 735, name: 'Balaenopteridae', type: 'FAMILY' },
{ key: 5220174, name: 'Balaenoptera musculus', type: 'SPECIES' },
{ key: 359, name: 'Aves', type: 'CLASS' },
{ key: 729, name: 'Spheniscidae', type: 'FAMILY' },
{ key: 6, name: 'Plantae', type: 'KINGDOM' },
{ key: 220, name: 'Tracheophyta', type: 'PHYLUM' },
{ key: 196, name: 'Magnoliopsida', type: 'CLASS' },
{ key: 414, name: 'Fabales', type: 'ORDER' },
{ key: 5386, name: 'Orchidaceae', type: 'FAMILY' },
{ key: 4, name: 'Fungi', type: 'KINGDOM' },
{ key: 34, name: 'Basidiomycota', type: 'PHYLUM' },
{ key: 5, name: 'Bacteria', type: 'KINGDOM' },
{ key: 7707728, name: 'Proteobacteria', type: 'PHYLUM' },
{ key: 792, name: 'Amphibia', type: 'CLASS' },
{ key: 952, name: 'Anura', type: 'ORDER' },
{ key: 358, name: 'Reptilia', type: 'CLASS' },
{ key: 1450, name: 'Testudines', type: 'ORDER' },
{ key: 117, name: 'Actinopterygii', type: 'CLASS' },
{ key: 54, name: 'Insecta', type: 'CLASS' },
{ key: 797, name: 'Lepidoptera', type: 'ORDER' },
{ key: 1933, name: 'Coleoptera', type: 'ORDER' },
{ key: 6163, name: 'Mollusca', type: 'PHYLUM' },
{ key: 216, name: 'Gastropoda', type: 'CLASS' }
];
// Create nodes from taxonomic hierarchy
const nodesData = taxonomicKeys.map((taxon, i) => ({
id: taxon.key,
name: taxon.name,
rank: taxon.type,
scientificName: taxon.name,
occurrences: Math.floor(Math.random() * 50000) + 1000,
threatStatus: ['LC', 'NT', 'VU', 'EN', 'CR'][Math.floor(Math.random() * 5)],
description: `${taxon.type} - ${taxon.name}`,
x: width / 2 + (Math.random() - 0.5) * 200,
y: height / 2 + (Math.random() - 0.5) * 200
}));
// Create links based on taxonomic relationships
const linksData = [];
// Kingdom -> Phylum
linksData.push({ source: 1, target: 44, type: 'contains', strength: 0.8 });
linksData.push({ source: 6, target: 220, type: 'contains', strength: 0.8 });
linksData.push({ source: 4, target: 34, type: 'contains', strength: 0.8 });
linksData.push({ source: 5, target: 7707728, type: 'contains', strength: 0.8 });
linksData.push({ source: 1, target: 6163, type: 'contains', strength: 0.8 });
// Phylum -> Class
linksData.push({ source: 44, target: 212, type: 'contains', strength: 0.7 });
linksData.push({ source: 44, target: 359, type: 'contains', strength: 0.7 });
linksData.push({ source: 44, target: 792, type: 'contains', strength: 0.7 });
linksData.push({ source: 44, target: 358, type: 'contains', strength: 0.7 });
linksData.push({ source: 44, target: 117, type: 'contains', strength: 0.7 });
linksData.push({ source: 44, target: 54, type: 'contains', strength: 0.7 });
linksData.push({ source: 220, target: 196, type: 'contains', strength: 0.7 });
linksData.push({ source: 6163, target: 216, type: 'contains', strength: 0.7 });
// Class -> Order
linksData.push({ source: 212, target: 359, type: 'contains', strength: 0.6 });
linksData.push({ source: 212, target: 1, type: 'contains', strength: 0.6 });
linksData.push({ source: 212, target: 797, type: 'contains', strength: 0.6 });
linksData.push({ source: 792, target: 952, type: 'contains', strength: 0.6 });
linksData.push({ source: 358, target: 1450, type: 'contains', strength: 0.6 });
linksData.push({ source: 54, target: 797, type: 'contains', strength: 0.6 });
linksData.push({ source: 54, target: 1933, type: 'contains', strength: 0.6 });
linksData.push({ source: 196, target: 414, type: 'contains', strength: 0.6 });
// Order -> Family
linksData.push({ source: 359, target: 732, type: 'contains', strength: 0.5 });
linksData.push({ source: 1, target: 780, type: 'contains', strength: 0.5 });
linksData.push({ source: 797, target: 735, type: 'contains', strength: 0.5 });
linksData.push({ source: 359, target: 729, type: 'contains', strength: 0.5 });
linksData.push({ source: 414, target: 5386, type: 'contains', strength: 0.5 });
// Family -> Genus
linksData.push({ source: 732, target: 5219404, type: 'contains', strength: 0.4 });
linksData.push({ source: 780, target: 2436435, type: 'contains', strength: 0.4 });
linksData.push({ source: 780, target: 5219173, type: 'contains', strength: 0.4 });
// Genus -> Species
linksData.push({ source: 5219404, target: 5219426, type: 'contains', strength: 0.3 });
linksData.push({ source: 5219404, target: 5219427, type: 'contains', strength: 0.3 });
linksData.push({ source: 5219404, target: 5219234, type: 'contains', strength: 0.3 });
linksData.push({ source: 735, target: 5220174, type: 'contains', strength: 0.3 });
// Add some ecological relationships (cross-links)
linksData.push({ source: 5219426, target: 5219173, type: 'interacts', strength: 0.1 });
linksData.push({ source: 5220174, target: 6163, type: 'feeds_on', strength: 0.1 });
linksData.push({ source: 54, target: 6, type: 'pollinates', strength: 0.1 });
linksData.push({ source: 5386, target: 54, type: 'depends_on', strength: 0.1 });
return { nodes: nodesData, links: linksData };
} catch (error) {
console.error('Error fetching data:', error);
return { nodes: [], links: [] };
}
}
// Initialize visualization
async function init() {
const data = await fetchBiodiversityData();
nodes = data.nodes;
links = data.links;
document.getElementById('loading').style.display = 'none';
// Initialize all filters as active
const ranks = [...new Set(nodes.map(d => d.rank))];
ranks.forEach(rank => activeFilters.add(rank));
// Create filter checkboxes
createFilterControls(ranks);
// Create legend
createLegend(ranks);
// Create force simulation with advanced techniques from web source
simulation = d3.forceSimulation(nodes)
.force("link", d3.forceLink(links)
.id(d => d.id)
.distance(d => d.strength ? 100 / d.strength : 100)
.strength(d => d.strength || 0.3))
.force("charge", d3.forceManyBody()
.strength(-300)
.distanceMax(400))
.force("center", d3.forceCenter(width / 2, height / 2))
.force("x", d3.forceX(width / 2).strength(0.05))
.force("y", d3.forceY(height / 2).strength(0.05))
.force("collision", d3.forceCollide().radius(20));
// Create links
const link = g.append("g")
.selectAll("line")
.data(links)
.join("line")
.attr("stroke", d => d.type === 'contains' ? '#4fc3f7' : '#ff9800')
.attr("stroke-opacity", 0.4)
.attr("stroke-width", d => d.type === 'contains' ? 1.5 : 1)
.attr("stroke-dasharray", d => d.type === 'contains' ? '0' : '5,5')
.on("mouseover", function(event, d) {
showLinkTooltip(event, d);
d3.select(this).classed("link-highlighted", true);
})
.on("mouseout", function() {
hideTooltip();
d3.select(this).classed("link-highlighted", false);
});
// Create nodes
const node = g.append("g")
.selectAll("circle")
.data(nodes)
.join("circle")
.attr("r", d => {
const baseSize = 8;
const rankMultiplier = {
'SPECIES': 0.8,
'GENUS': 1,
'FAMILY': 1.2,
'ORDER': 1.4,
'CLASS': 1.6,
'PHYLUM': 1.8,
'KINGDOM': 2
};
return baseSize * (rankMultiplier[d.rank] || 1);
})
.attr("fill", d => rankColors[d.rank] || '#ffffff')
.attr("stroke", "#ffffff")
.attr("stroke-width", 2)
.style("cursor", "pointer")
.on("mouseover", function(event, d) {
if (!selectedNode) {
showNodeTooltip(event, d);
}
})
.on("mouseout", function() {
if (!selectedNode) {
hideTooltip();
}
})
.on("click", function(event, d) {
handleNodeClick(d, this);
event.stopPropagation();
})
.on("dblclick", function(event, d) {
handleNodeDoubleClick(d);
event.stopPropagation();
})
.call(createDragBehavior(simulation));
// Add labels for important nodes
const label = g.append("g")
.selectAll("text")
.data(nodes.filter(d => ['KINGDOM', 'PHYLUM', 'GENUS', 'SPECIES'].includes(d.rank)))
.join("text")
.text(d => d.name)
.attr("font-size", 10)
.attr("dx", 12)
.attr("dy", 4)
.attr("fill", "#e0e0e0")
.attr("opacity", 0.7)
.style("pointer-events", "none");
// Simulation tick handler with smooth transitions
simulation.on("tick", () => {
link
.attr("x1", d => d.source.x)
.attr("y1", d => d.source.y)
.attr("x2", d => d.target.x)
.attr("y2", d => d.target.y);
node
.attr("cx", d => d.x)
.attr("cy", d => d.y);
label
.attr("x", d => d.x)
.attr("y", d => d.y);
});
// Click on background to deselect
svg.on("click", () => {
clearSelection();
});
// Update stats
updateStats();
}
// Advanced drag behavior from web source
function createDragBehavior(simulation) {
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;
}
return d3.drag()
.on("start", dragstarted)
.on("drag", dragged)
.on("end", dragended);
}
// Handle single click - highlight connected nodes
function handleNodeClick(d, element) {
if (selectedNode === d) {
clearSelection();
return;
}
selectedNode = d;
// Get connected nodes
const connectedNodes = new Set();
connectedNodes.add(d.id);
links.forEach(link => {
if (link.source.id === d.id) connectedNodes.add(link.target.id);
if (link.target.id === d.id) connectedNodes.add(link.source.id);
});
// Highlight/dim nodes
d3.selectAll("circle")
.classed("highlight", n => n.id === d.id)
.classed("dimmed", n => !connectedNodes.has(n.id));
// Highlight/dim links
d3.selectAll("line")
.classed("link-highlighted", l =>
(l.source.id === d.id || l.target.id === d.id))
.classed("dimmed", l =>
!(l.source.id === d.id || l.target.id === d.id));
// Update details panel
updateNodeDetails(d);
}
// Handle double click - focus on node
function handleNodeDoubleClick(d) {
focusedNode = d;
// Zoom to node
const scale = 2;
const x = -d.x * scale + width / 2;
const y = -d.y * scale + height / 2;
svg.transition()
.duration(750)
.call(
zoom.transform,
d3.zoomIdentity.translate(x, y).scale(scale)
);
// Apply focused styling
d3.selectAll("circle")
.classed("focused", n => n.id === d.id)
.classed("dimmed", n => n.id !== d.id);
updateNodeDetails(d);
}
// Clear selection
function clearSelection() {
selectedNode = null;
focusedNode = null;
d3.selectAll("circle")
.classed("highlight", false)
.classed("dimmed", false)
.classed("focused", false);
d3.selectAll("line")
.classed("link-highlighted", false)
.classed("dimmed", false);
document.getElementById('node-details').innerHTML = `
<h3>Node Details</h3>
<p style="font-size: 12px; color: #81d4fa;">Click a node to see details</p>
`;
hideTooltip();
}
// Update node details panel
function updateNodeDetails(d) {
const threatLabels = {
'LC': 'Least Concern',
'NT': 'Near Threatened',
'VU': 'Vulnerable',
'EN': 'Endangered',
'CR': 'Critically Endangered'
};
document.getElementById('node-details').innerHTML = `
<h3>${d.name}</h3>
<div class="detail-item">
<span class="detail-label">Taxonomic Rank:</span>
<span class="detail-value">${d.rank}</span>
</div>
<div class="detail-item">
<span class="detail-label">Scientific Name:</span>
<span class="detail-value">${d.scientificName}</span>
</div>
<div class="detail-item">
<span class="detail-label">Occurrences:</span>
<span class="detail-value">${d.occurrences.toLocaleString()}</span>
</div>
<div class="detail-item">
<span class="detail-label">Threat Status:</span>
<span class="detail-value">${threatLabels[d.threatStatus]}</span>
</div>
<div class="detail-item" style="margin-top: 10px; font-size: 11px; color: #81d4fa;">
<strong>Interactions:</strong> Click to highlight connections, double-click to focus
</div>
`;
}
// Show node tooltip
function showNodeTooltip(event, d) {
tooltip
.style("opacity", 1)
.style("left", (event.pageX + 10) + "px")
.style("top", (event.pageY - 10) + "px")
.html(`
<div class="tooltip-title">${d.name}</div>
<div class="tooltip-content">
<strong>Rank:</strong> ${d.rank}<br>
<strong>Occurrences:</strong> ${d.occurrences.toLocaleString()}<br>
<strong>Status:</strong> ${d.threatStatus}
</div>
`);
}
// Show link tooltip
function showLinkTooltip(event, d) {
const typeLabels = {
'contains': 'Taxonomic Hierarchy',
'interacts': 'Ecological Interaction',
'feeds_on': 'Predator-Prey',
'pollinates': 'Pollination',
'depends_on': 'Dependency'
};
tooltip
.style("opacity", 1)
.style("left", (event.pageX + 10) + "px")
.style("top", (event.pageY - 10) + "px")
.html(`
<div class="tooltip-title">Relationship</div>
<div class="tooltip-content">
<strong>Type:</strong> ${typeLabels[d.type]}<br>
<strong>From:</strong> ${d.source.name}<br>
<strong>To:</strong> ${d.target.name}<br>
<strong>Strength:</strong> ${(d.strength * 100).toFixed(0)}%
</div>
`);
}
// Hide tooltip
function hideTooltip() {
tooltip.style("opacity", 0);
}
// Create filter controls
function createFilterControls(ranks) {
const container = document.getElementById('rank-filters');
ranks.sort().forEach(rank => {
const count = nodes.filter(n => n.rank === rank).length;
const div = document.createElement('div');
div.className = 'filter-checkbox';
div.innerHTML = `
<input type="checkbox" id="filter-${rank}" checked>
<label for="filter-${rank}">${rank} <span class="count">(${count})</span></label>
`;
const checkbox = div.querySelector('input');
checkbox.addEventListener('change', () => {
if (checkbox.checked) {
activeFilters.add(rank);
} else {
activeFilters.delete(rank);
}
applyFilters();
});
container.appendChild(div);
});
}
// Apply filters
function applyFilters() {
d3.selectAll("circle")
.transition()
.duration(300)
.style("opacity", d => activeFilters.has(d.rank) ? 1 : 0.1)
.attr("pointer-events", d => activeFilters.has(d.rank) ? "all" : "none");
d3.selectAll("text")
.transition()
.duration(300)
.style("opacity", d => activeFilters.has(d.rank) ? 0.7 : 0);
updateStats();
}
// Create legend
function createLegend(ranks) {
const legend = document.getElementById('legend');
let html = '<div class="legend-title">Taxonomic Ranks</div>';
ranks.sort().forEach(rank => {
html += `
<div class="legend-item">
<div class="legend-color" style="background: ${rankColors[rank]}"></div>
<span>${rank}</span>
</div>
`;
});
legend.innerHTML = html;
}
// Update statistics
function updateStats() {
const visibleNodes = nodes.filter(d => activeFilters.has(d.rank));
const totalOccurrences = visibleNodes.reduce((sum, d) => sum + d.occurrences, 0);
document.getElementById('stats').innerHTML = `
<strong>Network Statistics</strong><br>
Nodes: ${visibleNodes.length} / ${nodes.length}<br>
Links: ${links.length}<br>
Total Occurrences: ${totalOccurrences.toLocaleString()}
`;
}
// Search functionality
document.getElementById('search-btn').addEventListener('click', () => {
const searchTerm = document.getElementById('search-input').value.toLowerCase();
if (!searchTerm) {
clearSelection();
d3.selectAll("circle").classed("searching", false);
return;
}
const matches = nodes.filter(d =>
d.name.toLowerCase().includes(searchTerm) ||
d.rank.toLowerCase().includes(searchTerm)
);
if (matches.length === 0) {
alert('No matches found');
return;
}
// Clear previous selection
clearSelection();
// Highlight matches
d3.selectAll("circle")
.classed("searching", d => matches.some(m => m.id === d.id))
.classed("dimmed", d => !matches.some(m => m.id === d.id));
// Zoom to first match
if (matches.length > 0) {
const firstMatch = matches[0];
const scale = 1.5;
const x = -firstMatch.x * scale + width / 2;
const y = -firstMatch.y * scale + height / 2;
svg.transition()
.duration(750)
.call(
zoom.transform,
d3.zoomIdentity.translate(x, y).scale(scale)
);
updateNodeDetails(firstMatch);
}
});
// Reset button
document.getElementById('reset-btn').addEventListener('click', () => {
clearSelection();
document.getElementById('search-input').value = '';
d3.selectAll("circle").classed("searching", false);
svg.transition()
.duration(750)
.call(zoom.transform, d3.zoomIdentity);
});
// Zoom to fit button
document.getElementById('zoom-fit-btn').addEventListener('click', () => {
const bounds = g.node().getBBox();
const fullWidth = bounds.width;
const fullHeight = bounds.height;
const midX = bounds.x + fullWidth / 2;
const midY = bounds.y + fullHeight / 2;
const scale = 0.8 / Math.max(fullWidth / width, fullHeight / height);
const translate = [width / 2 - scale * midX, height / 2 - scale * midY];
svg.transition()
.duration(750)
.call(
zoom.transform,
d3.zoomIdentity.translate(translate[0], translate[1]).scale(scale)
);
});
// Reheat simulation button
document.getElementById('reheat-btn').addEventListener('click', () => {
simulation.alpha(1).restart();
});
// Enter key for search
document.getElementById('search-input').addEventListener('keypress', (e) => {
if (e.key === 'Enter') {
document.getElementById('search-btn').click();
}
});
// Initialize
init();
</script>
</body>
</html>