Home »

body { font-family: ‘Inter’, sans-serif; } .chart-container { position: relative; width: 100%; max-width: 100%; margin-left: auto; margin-right: auto; height: 350px; max-height: 400px; } .map-container { position: relative; width: 100%; aspect-ratio: 16/9; background: #e0f2fe; /* Light ocean blue */ border-radius: 0.75rem; overflow: hidden; border: 1px solid #bae6fd; } @media (min-width: 768px) { .chart-container { height: 400px; } } /* Custom range slider styling */ input[type=range] { -webkit-appearance: none; width: 100%; background: transparent; } input[type=range]::-webkit-slider-thumb { -webkit-appearance: none; height: 24px; width: 24px; border-radius: 50%; background: #0284c7; cursor: pointer; margin-top: -10px; box-shadow: 0 2px 4px rgba(0,0,0,0.2); border: 2px solid white; transition: transform 0.1s; } input[type=range]::-webkit-slider-thumb:hover { transform: scale(1.1); } input[type=range]::-webkit-slider-runnable-track { width: 100%; height: 6px; cursor: pointer; background: #cbd5e1; border-radius: 3px; } .species-btn { transition: all 0.2s ease; } .species-btn:hover { transform: translateY(-2px); box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06); border-color: #0ea5e9; }

How does larval dispersal connect MPAs and OECMs in the seascape?

Marine connectivity can be visualized in many waysβ€”from complex mathematical matrices and graph theory metrics to interactive spatial maps. Here, we’ll dive deeper into how these visual tools work together to tell a story about ocean life.

In this hypothetical case, we will see the connections between 3 Marine Protected Areas (MPAs) and 3 Other Effective area-based Conservation Measures (OECMs) using a simulated dispersal model. Adjust the biological parameters to see how ocean currents bind these conservation zones together!

Simulate Dispersal

Adjust the Pelagic Larval Duration (PLD) to simulate different species. Watch how the spatial network connections (lines) react on the map.

10d (Local) 60d (Widespread)

Species Reference (Click to apply)

* Scientific Note: These PLD values are general approximations derived from literature for illustrative purposes. In reality, larval duration and actual dispersal networks vary significantly depending on localized oceanographic currents, water temperature, seasonal spawning dynamics, and active larval swimming behavior.
Initializing simulation…
MPA
OECM
Larval Flow

Spatial Network Map

Hypothetical Spatial Layout
πŸ€” How to read this map: This visualization helps us instantly spot the physical pathways created by ocean currents.
  • Watch the lines: Notice how they change when you adjust the slider? Thicker, darker lines mean a stronger flow of larvae between two locations. Do you see the connections bridging the strictly protected MPAs (teal) to the sustainably managed OECMs (orange)?
  • Watch the circles: A larger circle means more larvae stay at their home reef (local retention). Short PLDs create large circles because babies settle quickly, while long PLDs shrink them as larvae drift far away.

Probability Matrix

The raw data driving the map. Rows show where larvae start (Source), columns show where they settle (Destination). Darker blues indicate a higher probability of connection. The diagonal line shows larvae staying at their home reef.

Avg Self-Recruitment
–%
Active Corridors
Strongest Link
πŸ€” How to read this matrix: Reading a matrix might look intimidating, but it’s just a grid of probabilities! Look at the row on the left (where larvae start) and follow it to a column (where they settle). A darker blue square means a higher chance of connection.

See the diagonal line of squares going from top-left to bottom-right? That represents larvae staying at their home reef (‘self-recruitment’). Try setting the slider to 10 days: this diagonal will turn dark blue because most babies stay home. Now slide it to 60 days: watch how that diagonal fades to white as larvae are swept away to other reefs!

Graph Theory: Node Roles

Source / Sink Balance

Total probability export vs import

πŸ€” How to read this chart: Think of ‘Sources’ (light blue) as the givers and ‘Sinks’ (purple) as the receivers of the seascape. Which location has the tallest light blue bar? That’s your champion exporter! A healthy network needs strong sources to supply larvae to the sinks, ensuring vulnerable reefs can recover after storms or bleaching events.

Individual Profile

Select a location to view its specific graph theory metrics.
πŸ€” How to read this profile: This ‘spider web’ chart gives you a quick snapshot of a single location’s personality. Is the shape pulled heavily towards ‘Export Potential’? Then it’s a vital nursery! Try changing the species slider: what happens to a node’s ‘Network Bridge’ score when you switch from a Giant Clam to a Spiny Lobster?

πŸ’‘ Spatial Planning Implications

MPA Seeding Efficiency

Analyzing how well MPA 1, MPA 2, and MPA 3 supply larvae to surrounding areas. At the currently selected PLD, we evaluate if these strictly protected zones are effectively acting as demographic sources for the broader network, exporting more larvae than they import.

OECM Connectivity Integration

Evaluating if OECM 1, OECM 2, and OECM 3 function as vital stepping stones. Ensuring fisheries in these areas are managed sustainably is crucial, especially if graph analysis shows they receive heavy larval subsidies from the upstream MPAs.

// — 1. DATA & STATE — // 6 Nodes representing hypothetical seascape locations const locations = [ { id: 0, name: ‘MPA 1’, type: ‘MPA’, color: ‘#14b8a6’, x: 50, y: 45 }, // Center (Teal) { id: 1, name: ‘MPA 2’, type: ‘MPA’, color: ‘#14b8a6’, x: 80, y: 60 }, // East { id: 2, name: ‘MPA 3’, type: ‘MPA’, color: ‘#14b8a6’, x: 15, y: 35 }, // Far West { id: 3, name: ‘OECM 1’, type: ‘OECM’, color: ‘#fb923c’, x: 30, y: 40 }, // West (Orange) { id: 4, name: ‘OECM 2’, type: ‘OECM’, color: ‘#fb923c’, x: 55, y: 75 }, // South { id: 5, name: ‘OECM 3’, type: ‘OECM’, color: ‘#fb923c’, x: 60, y: 20 } // North ]; const regionNames = locations.map(l => l.name); // Base Probability Matrix (6×6) for PLD = 30 // Rows = Source, Cols = Destination const baseMatrixTemplate = [ // MPA1, MPA2, MPA3, OECM1, OECM2, OECM3 [0.45, 0.15, 0.02, 0.08, 0.20, 0.10], // MPA 1 (Source) [0.10, 0.60, 0.01, 0.04, 0.15, 0.10], // MPA 2 (Source) [0.05, 0.01, 0.65, 0.20, 0.05, 0.04], // MPA 3 (Source) [0.15, 0.04, 0.15, 0.50, 0.10, 0.06], // OECM 1 (Source) [0.25, 0.10, 0.05, 0.10, 0.45, 0.05], // OECM 2 (Source) [0.15, 0.10, 0.02, 0.05, 0.08, 0.60] // OECM 3 (Source) ]; // State variables let currentPLD = 30; let currentMatrix = []; let selectedRegionIndex = 0; let spatialCanvas, ctx; // — 2. CORE LOGIC — // Function called by the species preset buttons window.setPLD = function(value) { document.getElementById(‘pld-slider’).value = value; currentPLD = value; updateAll(value); } function generateMatrix(pld) { // Factor ranges from roughly -0.2 (low PLD) to +0.3 (high PLD) const factor = (pld – 30) / 100; return baseMatrixTemplate.map((row, rIndex) => { return row.map((val, cIndex) => { if (rIndex === cIndex) { // Diagonal (Retention): Decreases as PLD goes up (larvae drift away) let newVal = val – (factor * 1.8); return Math.max(0.05, Math.min(0.95, newVal)); } else { // Off-diagonal (Dispersal): Increases as PLD goes up // Add a distance/adjacency bias based on coordinates const dx = locations[rIndex].x – locations[cIndex].x; const dy = locations[rIndex].y – locations[cIndex].y; const dist = Math.sqrt(dx*dx + dy*dy); // Closer islands connect faster when PLD increases const distPenalty = dist / 100; let newVal = val + (factor * 0.8) – (distPenalty * 0.1) + (pld/600); return Math.max(0.001, newVal); } }); }); } function calculateMetrics(matrix) { return locations.map((loc, i) => { let retention = matrix[i][i]; let exportSum = matrix[i].reduce((a, b) => a + b, 0) – retention; let importSum = matrix.map(row => row[i]).reduce((a, b) => a + b, 0) – retention; return { name: loc.name, type: loc.type, retention: retention, exportVal: exportSum, importVal: importSum, // Simple proxy for betweenness betweenness: (exportSum * importSum) * 15 }; }); } // — 3. CUSTOM HTML5 CANVAS MAP DRAWING — function initMapCanvas() { spatialCanvas = document.getElementById(‘spatialMapCanvas’); ctx = spatialCanvas.getContext(‘2d’); resizeCanvas(); window.addEventListener(‘resize’, resizeCanvas); } function resizeCanvas() { const container = spatialCanvas.parentElement; // Handle high-DPI displays for crisp rendering const dpr = window.devicePixelRatio || 1; const rect = container.getBoundingClientRect(); spatialCanvas.width = rect.width * dpr; spatialCanvas.height = rect.height * dpr; ctx.scale(dpr, dpr); drawNetworkMap(); // Redraw immediately after resize } function drawNetworkMap() { if (!ctx || currentMatrix.length === 0) return; const w = spatialCanvas.width / (window.devicePixelRatio || 1); const h = spatialCanvas.height / (window.devicePixelRatio || 1); ctx.clearRect(0, 0, w, h); // Calculate actual pixel coordinates from percentages const getCoords = (loc) => ({ x: (loc.x / 100) * w, y: (loc.y / 100) * h }); // 1. Draw Edges (Connections) const edgeThreshold = 0.03; // Only draw lines > 3% probability currentMatrix.forEach((row, sourceIdx) => { row.forEach((prob, destIdx) => { if (sourceIdx !== destIdx && prob > edgeThreshold) { const source = getCoords(locations[sourceIdx]); const dest = getCoords(locations[destIdx]); // Use quadratic curves for visual flair instead of straight lines // Curve depends on direction to separate two-way traffic const midX = (source.x + dest.x) / 2; const midY = (source.y + dest.y) / 2; const offset = (sourceIdx > destIdx) ? 20 : -20; const cpX = midX – (dest.y – source.y) * (offset / 100); const cpY = midY + (dest.x – source.x) * (offset / 100); ctx.beginPath(); ctx.moveTo(source.x, source.y); ctx.quadraticCurveTo(cpX, cpY, dest.x, dest.y); // Style based on probability const alpha = Math.min(1, prob * 3); // Scale opacity ctx.strokeStyle = `rgba(14, 165, 233, ${alpha})`; // Sky blue ctx.lineWidth = prob * 15; // Thicker lines for higher prob ctx.stroke(); // Add a small directional indicator (arrow head logic simplified to a dot near destination) const t = 0.8; // Position along curve const arrowX = (1-t)*(1-t)*source.x + 2*(1-t)*t*cpX + t*t*dest.x; const arrowY = (1-t)*(1-t)*source.y + 2*(1-t)*t*cpY + t*t*dest.y; ctx.beginPath(); ctx.arc(arrowX, arrowY, prob * 5 + 1, 0, Math.PI*2); ctx.fillStyle = `rgba(14, 165, 233, ${alpha})`; ctx.fill(); } }); }); // 2. Draw Nodes (Locations) locations.forEach((loc) => { const pos = getCoords(loc); const isSelected = (loc.id === selectedRegionIndex); const retentionProb = currentMatrix[loc.id][loc.id]; // Base shadow ctx.shadowColor = ‘rgba(0,0,0,0.2)’; ctx.shadowBlur = 10; ctx.shadowOffsetY = 4; // Node circle size based on retention (exaggerated multiplier for visibility) const radius = 10 + (retentionProb * 35); ctx.beginPath(); ctx.arc(pos.x, pos.y, radius, 0, Math.PI * 2); ctx.fillStyle = loc.color; ctx.fill(); // Selection stroke if(isSelected) { ctx.lineWidth = 3; ctx.strokeStyle = ‘#1e293b’; // slate-800 ctx.stroke(); } else { ctx.lineWidth = 2; ctx.strokeStyle = ‘#ffffff’; ctx.stroke(); } // Reset shadow for text ctx.shadowColor = ‘transparent’; ctx.shadowBlur = 0; ctx.shadowOffsetY = 0; // Label Background const label = loc.name; ctx.font = ‘bold 12px Inter, sans-serif’; const metrics = ctx.measureText(label); ctx.fillStyle = ‘rgba(255,255,255,0.85)’; ctx.beginPath(); ctx.roundRect(pos.x – metrics.width/2 – 6, pos.y + radius + 4, metrics.width + 12, 18, 4); ctx.fill(); // Label Text ctx.fillStyle = ‘#334155’; // slate-700 ctx.textAlign = ‘center’; ctx.textBaseline = ‘top’; ctx.fillText(label, pos.x, pos.y + radius + 7); }); } // — 4. CHARTS INITIALIZATION — let sourceSinkChart, radarChart; function initCharts() { // Stacked Bar const ctxSourceSink = document.getElementById(‘sourceSinkChart’).getContext(‘2d’); sourceSinkChart = new Chart(ctxSourceSink, { type: ‘bar’, data: { labels: regionNames, datasets: [ { label: ‘Export (Source)’, data: [], backgroundColor: ‘#38bdf8’, borderRadius: 2 }, // Sky 400 { label: ‘Import (Sink)’, data: [], backgroundColor: ‘#818cf8’, borderRadius: 2 }, // Indigo 400 { label: ‘Retention’, data: [], backgroundColor: ‘#cbd5e1’, borderRadius: 2 } // Slate 300 ] }, options: { responsive: true, maintainAspectRatio: false, plugins: { legend: { position: ‘bottom’, labels: { boxWidth: 12, font: {family: ‘Inter’} } } }, scales: { y: { beginAtZero: true, stacked: true, grid: { color: ‘#f1f5f9’ } }, x: { stacked: true, grid: { display: false } } } } }); // Radar Chart const ctxRadar = document.getElementById(‘radarChart’).getContext(‘2d’); radarChart = new Chart(ctxRadar, { type: ‘radar’, data: { labels: [‘Local Retention’, ‘Export Potential’, ‘Import Dependency’, ‘Network Bridge (Betweenness)’], datasets: [{ label: ‘Metrics’, data: [0,0,0,0], backgroundColor: ‘rgba(20, 184, 166, 0.2)’, // Teal 500 alpha borderColor: ‘#14b8a6’, pointBackgroundColor: ‘#14b8a6’, pointHoverRadius: 6 }] }, options: { responsive: true, maintainAspectRatio: false, plugins: { legend: { display: false } }, scales: { r: { angleLines: { color: ‘#e2e8f0’ }, grid: { color: ‘#e2e8f0’ }, pointLabels: { font: {family: ‘Inter’, size: 11}, color: ‘#64748b’ }, ticks: { display: false, min: 0, max: 1 } } } } }); } // — 5. UPDATE CYCLE — function updateAll(pld) { // 1. Data Math currentMatrix = generateMatrix(pld); const metrics = calculateMetrics(currentMatrix); // 2. UI Updates (Text/Sliders) document.getElementById(‘pld-value’).innerText = pld; const insightBox = document.getElementById(‘pld-insight’); if(pld < 20) { insightBox.innerHTML = `Short PLD (${pld}d): High local retention. Nodes are isolated. MPAs mostly reseed themselves. OECMs rely entirely on local management as external larval supply is low.`; insightBox.className = “bg-slate-100 border-l-4 border-slate-400 p-4 rounded text-sm text-slate-700 leading-relaxed transition-colors”; } else if (pld > 45) { insightBox.innerHTML = `Long PLD (${pld}d): Widespread mixing. Strong connections form across the seascape. The network acts as a single, connected metapopulation where OECMs receive significant larval subsidies from distant MPAs.`; insightBox.className = “bg-sky-50 border-l-4 border-sky-500 p-4 rounded text-sm text-sky-900 leading-relaxed transition-colors”; } else { insightBox.innerHTML = `Moderate PLD (${pld}d): A balanced state. MPAs show healthy self-seeding while actively exporting larvae to neighboring OECMs via functional corridors.`; insightBox.className = “bg-teal-50 border-l-4 border-teal-500 p-4 rounded text-sm text-teal-900 leading-relaxed transition-colors”; } // 3. Update Custom Canvas Map drawNetworkMap(); // 4. Update Matrix Heatmap (Plotly 6×6) const dataTrace = [{ z: currentMatrix, x: regionNames, y: regionNames, type: ‘heatmap’, colorscale: [ [‘0.0’, ‘#f8fafc’], // slate-50 [‘0.2’, ‘#bae6fd’], // sky-200 [‘0.5’, ‘#0ea5e9’], // sky-500 [‘1.0’, ‘#0369a1’] // sky-800 ], showscale: true, hovertemplate: ‘Source: %{y}
Dest: %{x}
Prob: %{z:.3f}’ }]; const layout = { margin: { t: 20, b: 60, l: 80, r: 10 }, xaxis: { tickangle: -45, title: { text: ‘Destination Node’, font: {size: 11, color: ‘#64748b’} } }, yaxis: { title: { text: ‘Source Node’, font: {size: 11, color: ‘#64748b’} }, autorange: ‘reversed’ }, paper_bgcolor: ‘transparent’, plot_bgcolor: ‘transparent’ }; Plotly.newPlot(‘matrixChart’, dataTrace, layout, {responsive: true, displayModeBar: false}); // 5. Update KPI Numbers const totalConnections = currentMatrix.flat().filter(x => x > 0.05).length; const avgRetention = (metrics.reduce((acc, m) => acc + m.retention, 0) / locations.length * 100).toFixed(1); let maxVal = 0; let maxLink = “–“; currentMatrix.forEach((row, r) => { row.forEach((val, c) => { if (r !== c && val > maxVal) { maxVal = val; maxLink = `${locations[r].name} β†’ ${locations[c].name}`; } }); }); document.getElementById(‘metric-connections’).innerText = totalConnections; document.getElementById(‘metric-retention’).innerText = avgRetention + “%”; document.getElementById(‘metric-link’).innerText = maxLink; // 6. Update Bar Chart sourceSinkChart.data.datasets[0].data = metrics.map(m => m.exportVal); sourceSinkChart.data.datasets[1].data = metrics.map(m => m.importVal); sourceSinkChart.data.datasets[2].data = metrics.map(m => m.retention); sourceSinkChart.update(); // 7. Update Radar & Description updateRadar(selectedRegionIndex, metrics); } function updateRadar(index, metrics) { const m = metrics[index]; const loc = locations[index]; // Normalize for visual radar layout const data = [ m.retention, m.exportVal * 1.5, // Scaled for visibility m.importVal * 1.5, Math.min(1, m.betweenness) ]; radarChart.data.datasets[0].data = data; radarChart.data.datasets[0].label = loc.name; radarChart.data.datasets[0].borderColor = loc.color; radarChart.data.datasets[0].backgroundColor = loc.type === ‘MPA’ ? ‘rgba(20, 184, 166, 0.2)’ : ‘rgba(251, 146, 60, 0.2)’; radarChart.data.datasets[0].pointBackgroundColor = loc.color; radarChart.update(); // Dynamic Description tailored to MPA/OECM const roleDesc = document.getElementById(‘role-description’); let text = `
${loc.name} (${loc.type})
`; if (m.exportVal > m.importVal + 0.05) { text += `Functions strongly as a Source. `; if(loc.type === ‘MPA’) text += `This means its strict protection is successfully subsidizing other areas with larvae.`; else text += `Despite being an OECM, it’s vital for seeding the network.`; } else if (m.importVal > m.exportVal + 0.05) { text += `Functions heavily as a Sink. `; if(loc.type === ‘OECM’) text += `Its fisheries likely depend on larvae floating in from the MPAs.`; else text += `As an MPA, it serves to protect imported populations.`; } else { text += `A balanced node. `; } if (m.betweenness > 0.4) { text += `

High Betweenness: Critical stepping-stone. If degraded, network connectivity drops sharply.`; } roleDesc.innerHTML = text; // Highlight selected node on the map drawNetworkMap(); } // — 6. BOOTSTRAP — document.addEventListener(‘DOMContentLoaded’, () => { initMapCanvas(); initCharts(); // Populate Dropdown const select = document.getElementById(‘region-selector’); locations.forEach((l, i) => { const opt = document.createElement(‘option’); opt.value = i; opt.innerText = `${l.name} (${l.type})`; select.appendChild(opt); }); // Initial Draw updateAll(30); // Event Listeners document.getElementById(‘pld-slider’).addEventListener(‘input’, (e) => { currentPLD = parseInt(e.target.value); updateAll(currentPLD); }); document.getElementById(‘region-selector’).addEventListener(‘change’, (e) => { selectedRegionIndex = parseInt(e.target.value); const metrics = calculateMetrics(currentMatrix); updateRadar(selectedRegionIndex, metrics); }); }); function scrollToSection(id) { document.getElementById(id).scrollIntoView({ behavior: ‘smooth’ }); }