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``;
  }
}
Warning

Marine occurrence data are substantially less well represented in open biodiversity databases than terrestrial data. This means that species listed as newly appearing or emerging may in reality could have been present and spreading for considerably longer, with earlier records remaining unpublished or scattered across grey literature and personal datasets. Because of these data limitations, the current results should be interpreted with caution.

// 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).