GuardIAS Emerging Trends
  • Emerging Trends
  • Species Search
  • About data & workflow
d3 = require("d3@7")
L = require("leaflet@1.9.4")
Plotly = require("https://cdn.plot.ly/plotly-2.35.2.min.js")

// Add Leaflet CSS
html`<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css" />`
emerging_species_data = d3.csv("data/emerging_trends_ranking_list.csv")
appearing_species_data = d3.csv("data/appearing_species.csv")
reappearing_species_data = d3.csv("data/reappearing_species.csv")
lme_geojson  = d3.json("data/lme_polygons.geojson")
// Merge all species categories into a common schema used by this page.
species_data = [
  ...emerging_species_data.map(function(d) {
    return {
      species_key: d["specieskey"] || d["species_key"] || "",
      species_name: d["species_name"] || "",
      lme_id: d["lme_id"] || "",
      lme_name: d["lme_name"] || "",
      plot_prefix: "",
      first_year: null,
      reappearance_year: null,
      years_without_occurrences: null,
      weighted_em_status: d["weighted_em_status"] || "",
      model_used: d["model"] || ""
    };
  }),
  ...appearing_species_data.map(function(d) {
    return {
      species_key: d["specieskey"] || d["species_key"] || "",
      species_name: d["species"] || "",
      lme_id: d["lme_id"] || "",
      lme_name: d["lme_name"] || "",
      plot_prefix: "appearing_species_plots_ggplot2",
      first_year: +d["first_year"],
      reappearance_year: null,
      years_without_occurrences: null,
      weighted_em_status: null,
      model_used: null
    };
  }),
  ...reappearing_species_data.map(function(d) {
    return {
      species_key: d["specieskey"] || d["species_key"] || "",
      species_name: d["species"] || "",
      lme_id: d["lme_id"] || "",
      lme_name: d["lme_name"] || "",
      plot_prefix: "reappearing_species_plots_ggplot2",
      first_year: null,
      reappearance_year: +d["reappearance_year"],
      years_without_occurrences: +d["years_without_occurrences"],
      weighted_em_status: null,
      model_used: null
    };
  })
]
// Get unique species names for the autocomplete dropdown
unique_species = [...new Set(species_data.map(d => d.species_name))].sort()
viewof selected_species = Inputs.text({
  label: "Species:",
  placeholder: "Type to search species...",
  value: unique_species[0],
  datalist: unique_species
})
// Validate that the selected species exists in the list
validated_species = unique_species.includes(selected_species)
  ? selected_species
  : unique_species[0]
// Rows for the validated species, one per LME
filtered_rows = species_data.filter(d => d.species_name === validated_species)
// LME IDs where the validated species is present
filtered_lme_ids = [...new Set(filtered_rows.map(d => +d.lme_id))]
// Reset selected LME when species changes; auto-select first available LME
reset_lme = {
  validated_species;  // reactive dependency on species change
  if (filtered_lme_ids.length > 0) {
    mutable selected_lme_id = filtered_lme_ids[0];
  } else {
    mutable selected_lme_id = null;
  }
}
// The data row for the currently selected species + LME.
// Falls back to filtered_rows[0] so a row is always available if data exists.
current_row = {
  const rows = filtered_rows;
  if (rows.length === 0) return null;
  if (selected_lme_id !== null) {
    const match = rows.find(d => +d.lme_id === selected_lme_id);
    if (match) return match;
  }
  return rows[0];
}
// Build and render a descriptive sentence for the validated species and selected LME
{
  const row = current_row;

  if (row && row.species_key) {
    const speciesLink = `<a href="https://www.gbif.org/species/${row.species_key}">${validated_species}</a>`;
    const lmeName = row.lme_name || "";
    let htmlStr;

    if (row.plot_prefix === "appearing_species_plots_ggplot2") {
      htmlStr = `The species ${speciesLink} in the <strong>${lmeName}</strong> is appearing in <strong>${row.first_year}</strong>.`;
    } else if (row.plot_prefix === "reappearing_species_plots_ggplot2") {
      htmlStr = `The species ${speciesLink} in the <strong>${lmeName}</strong> is reappearing in <strong>${row.reappearance_year}</strong> after <strong>${row.years_without_occurrences}</strong> years.`;
    } else {
      const score = row.weighted_em_status;
      const modelName = row.model_used;
      htmlStr = `The species ${speciesLink} in the <strong>${lmeName}</strong> has a total emerging score of <strong>${score}</strong>. Model used: <strong>${modelName || "unknown"}</strong>.`;
    }

    const p = document.createElement("p");
    p.style.cssText = "text-align:center;font-size:1rem;";
    p.innerHTML = htmlStr;
    yield p;
  } else {
    yield html``;
  }
}
// Show inline plots for the selected LME and species
{
  const row = current_row;
  if (row) {
    const container = document.createElement("div");

    const title = document.createElement("h4");
    title.textContent = `${validated_species} – ${row.lme_name}`;
    title.style.marginTop = "12px";
    container.appendChild(title);

    const plotDiv1 = document.createElement("div");
    plotDiv1.style.cssText = "width:100%;height:320px;";
    container.appendChild(plotDiv1);

    const plotDiv2 = document.createElement("div");
    plotDiv2.style.cssText = "width:100%;height:320px;margin-top:8px;";
    container.appendChild(plotDiv2);

    renderPlotlyChart(plotDiv1, plotJsonUrl(row.lme_name, row.species_key, 1, row.plot_prefix),
      "No occurrences plot available.");
    renderPlotlyChart(plotDiv2, plotJsonUrl(row.lme_name, row.species_key, 2, row.plot_prefix),
      "No grid-cells plot available.");

    yield container;
  } else if (filtered_lme_ids.length > 0) {
    yield html`<p style="color:#999;padding:20px;text-align:center;">Click on a highlighted region on the map to view plots.</p>`;
  } else {
    yield html`<p style="color:#999;padding:20px;text-align:center;">No LME data available for this species.</p>`;
  }
}
// Create map container
map_container = html`<div id="map" style="height: 500px; width: 100%; margin-top: 2.7rem;"></div>`
// Initialise Leaflet map centred on European seas
map = {
  const m = L.map(map_container).setView([55.0, 15.0], 4);

  L.tileLayer("https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png", {
    attribution: "© OpenStreetMap contributors"
  }).addTo(m);

  return m;
}
// Feature group that holds the LME polygons (cleared and rebuilt reactively)
// Using featureGroup instead of layerGroup to support getBounds()
lme_layer = L.featureGroup().addTo(map)
// Feature group for the blue bounding-box highlight on the selected LME
highlight_layer = L.featureGroup().addTo(map)
// Base URL for pre-generated Plotly JSON files.
PLOTS_JSON_BASE = "data/indicators_plots_json/"
// Build the JSON URL for a given plot.
function plotJsonUrl(lme_name, species_key, index, plot_prefix = "") {
  const baseName = plot_prefix
    ? `${plot_prefix}_lme_${lme_name}_species_${species_key}`
    : `lme_${lme_name}_species_${species_key}`;
  const filename = `${baseName}_${index}.json`;
  return PLOTS_JSON_BASE + encodeURIComponent(filename);
}
// Render a single Plotly chart into a container div.
async function renderPlotlyChart(container, jsonUrl, fallbackText) {
  try {
    const resp = await fetch(jsonUrl);
    if (!resp.ok) throw new Error("not found");
    const plotData = await resp.json();
    await Plotly.newPlot(container, plotData.data || [], plotData.layout || {}, {
      responsive: true,
      displayModeBar: false
    });
  } catch (e) {
    container.innerHTML = `<p style="color:#999;text-align:center;">${fallbackText}</p>`;
  }
}
// Mutable to track which LME is selected on the map
mutable selected_lme_id = null
// Reactive cell: rebuild LME polygons whenever the selected species changes
update_map = {
  lme_layer.clearLayers();

  lme_geojson.features.forEach(feature => {
    const lme_id   = feature.properties.lme_id;
    const lme_name = feature.properties.lme_name;

    // Records for this LME (all species present here)
    const lme_records  = species_data.filter(d => +d.lme_id === lme_id);
    const all_species  = [...new Set(lme_records.map(d => d.species_name))];
    const is_filtered  = filtered_lme_ids.includes(lme_id);

    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,
      onEachFeature: (feat, lyr) => {
        // Tooltip: show the first 5 species present in this LME (with count)
        const TOOLTIP_MAX = 5;
        const shown   = all_species.slice(0, TOOLTIP_MAX).join(", ");
        const extra   = all_species.length > TOOLTIP_MAX
          ? ` … (+${all_species.length - TOOLTIP_MAX} more)`
          : "";
        const tooltip_text = all_species.length > 0
          ? `${lme_name}: ${shown}${extra} (${all_species.length} species)`
          : `${lme_name}: No species data`;
        lyr.bindTooltip(tooltip_text, { permanent: false, direction: "center" });

        // Click: select LME to show plots inline
        if (is_filtered) {
          lyr.on("click", () => {
            mutable selected_lme_id = lme_id;
          });

          lyr.on("mouseover", () => lyr.setStyle({ fillOpacity: 0.8 }));
          lyr.on("mouseout",  () => lyr.setStyle({ fillOpacity: 0.6 }));
        }
      }
    });

    layer.addTo(lme_layer);
  });

  // Zoom to highlighted regions when any are selected
  if (filtered_lme_ids.length > 0) {
    const bounds = lme_layer.getBounds();
    if (bounds.isValid()) map.fitBounds(bounds, { padding: [50, 50] });
  }
}
// Reactive cell: draw a blue bounding box around the selected LME
highlight_selected = {
  highlight_layer.clearLayers();

  if (selected_lme_id !== null) {
    const feature = lme_geojson.features.find(f => f.properties.lme_id === selected_lme_id);
    if (feature) {
      L.geoJSON(feature, {
        style: {
          color: "#0066cc",
          weight: 4,
          opacity: 1,
          fillOpacity: 0,
          dashArray: null
        },
        interactive: false
      }).addTo(highlight_layer);
    }
  }
}

Funding: This dashboard is being developed in the framework of the GuardIAS project. GuardIAS receives funding from the European Union’s Horizon Europe Research and Innovation Programme (ID No 101181413).