d3 = require("d3@7")
L = require("leaflet@1.9.4")
// Add Leaflet CSS
html`<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css" />`OJS Dashboard
species_data = d3.csv("data/species_polygons.csv")
polygons_geojson = d3.json("data/polygons.geojson")// Get unique species for dropdown
unique_species = [...new Set(species_data.map(d => d.species))].sort()Species Dashboard
Select Species
viewof selected_species = Inputs.text(
{
label: "Species:",
placeholder: "Type to search species...",
value: unique_species[0],
datalist: unique_species
}
)Filtered Polygons
// Validate that selected species exists in the list
validated_species = unique_species.includes(selected_species)
? selected_species
: unique_species[0]filtered_data = species_data.filter(d => d.species === validated_species)
filtered_polygon_ids = filtered_data.map(d => d.polygons_id)Showing \({filtered_polygon_ids.length}** polygon(s) for **\){validated_species}
Map - Click on a polygon to view plot
// Create map container
map_container = html`<div id="map" style="height: 500px; width: 100%;"></div>`// Initialize map - map_container is the div element itself
// Create map centered on Belgium (where coordinates are)
map = {
const m = L.map(map_container).setView([51.15, 4.15], 11);
// Add base tile layer
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
attribution: '© OpenStreetMap contributors'
}).addTo(m);
return m;
}// Store layer group for polygons
polygonLayer = L.layerGroup().addTo(map)function slugify(str) {
const slug = str
.normalize('NFD')
.replace(/[\u0300-\u036f]/g, '') // strip combining diacritics
.toLowerCase()
.replace(/[^a-z0-9]+/g, '_')
.replace(/^_|_$/g, '');
return slug.length > 0 ? slug : 'unknown';
}
// Sanitise a polygon ID so it can be used safely in a file path.
// Keeps only alphanumeric chars, hyphens and underscores.
function sanitizeId(id) {
return String(id).replace(/[^a-zA-Z0-9_-]/g, '_');
}
// Function to create popup content with a pre-rendered PNG plot.
// Displays the image at assets/plots/<species_slug>__<polygon_id>.png.
// Shows "No plot available" when the image cannot be loaded.
function createPopupContent(polygon_id, species_name) {
const popupDiv = document.createElement('div');
popupDiv.style.minWidth = '300px';
// Title
const title = document.createElement('h4');
title.textContent = `${species_name} - ${polygon_id}`;
title.style.marginTop = '0';
popupDiv.appendChild(title);
// PNG image: assets/plots/<species_slug>__<sanitized_polygon_id>.png
const img = document.createElement('img');
img.src = `assets/plots/${slugify(species_name)}__${sanitizeId(polygon_id)}.png`;
img.alt = `Plot for ${species_name} in ${polygon_id}`;
img.style.maxWidth = '300px';
img.style.height = 'auto';
img.style.display = 'block';
img.onerror = () => {
img.replaceWith(Object.assign(document.createElement('p'), {
textContent: 'No plot available for this selection.'
}));
};
popupDiv.appendChild(img);
return popupDiv;
}// Reactive cell: Update polygons when filtered_polygon_ids changes
{
// Clear existing polygons
polygonLayer.clearLayers();
// Add all polygons with appropriate styling
polygons_geojson.features.forEach(feature => {
const polygon_id = feature.properties.polygon_id;
const is_filtered = filtered_polygon_ids.includes(polygon_id);
// Find all species for this polygon
const polygon_records = species_data.filter(d => d.polygons_id === polygon_id);
const all_species = polygon_records.map(d => d.species);
const species_count = all_species.length;
// Style based on whether it's in filtered set
const style = {
fillColor: is_filtered ? '#3388ff' : '#cccccc',
weight: 2,
opacity: 1,
color: is_filtered ? '#0066cc' : '#999999',
fillOpacity: is_filtered ? 0.6 : 0.3
};
const layer = L.geoJSON(feature, {
style: style,
onEachFeature: (feature, layer) => {
// Add tooltip showing all species in this polygon
const tooltip_text = all_species.length > 0
? `${polygon_id}: ${all_species.join(', ')} (${all_species.length} species)`
: `${polygon_id}: No species data`;
layer.bindTooltip(tooltip_text, {
permanent: false,
direction: 'center'
});
// Add click handler with popup
if (is_filtered) {
layer.on('click', () => {
// Use the currently validated species for the popup
const popupContent = createPopupContent(polygon_id, validated_species);
L.popup()
.setLatLng(layer.getBounds().getCenter())
.setContent(popupContent)
.openOn(map);
});
// Change cursor on hover
layer.on('mouseover', () => {
layer.setStyle({ fillOpacity: 0.8 });
});
layer.on('mouseout', () => {
layer.setStyle({ fillOpacity: 0.6 });
});
}
}
});
layer.addTo(polygonLayer);
});
// Fit map to filtered polygons if any
if (filtered_polygon_ids.length > 0) {
const bounds = polygonLayer.getBounds();
if (bounds.isValid()) {
map.fitBounds(bounds, { padding: [50, 50] });
}
}
}Instructions: 1. Type a species name in the text input on the left (autocomplete suggestions will appear) 2. The map will highlight polygons containing that species in blue 3. Click on any highlighted polygon to view its plot in a popup