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" />`
lme_geojson = d3.json("data/lme_polygons.geojson")

// Load species data from emtrends repository
appearing_species_data = d3.csv("https://raw.githubusercontent.com/guardias-eu/emtrends/main/data/output/appearing_species.csv")
reappearing_species_data = d3.csv("https://raw.githubusercontent.com/guardias-eu/emtrends/main/data/output/reappearing_species.csv")
emerging_species_data = d3.csv("https://raw.githubusercontent.com/guardias-eu/emtrends/main/data/output/emerging_trends_ranking_list.csv")
// Mutable selected LME state
mutable selected_lme_id = 13
// Base URL for pre-generated Plotly JSON files.
PLOTS_JSON_BASE = "data/indicators_plots_json/"
// 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>`;
  }
}
// Modal overlay for species plots (shared by appearing and reappearing tables)
modal_overlay = {
  const overlay = document.createElement("div");
  overlay.id = "species-plot-modal";
  overlay.style.cssText = `
    display:none; position:fixed; top:0; left:0; width:100%; height:100%;
    background:rgba(0,0,0,0.7); z-index:10000; justify-content:center;
    align-items:center; overflow:auto;
  `;
  overlay.addEventListener("click", e => {
    if (e.target === overlay) overlay.style.display = "none";
  });

  const box = document.createElement("div");
  box.id = "species-plot-modal-box";
  box.style.cssText = `
    background:#222; border-radius:8px; padding:24px; max-width:620px;
    width:95%; max-height:90vh; overflow:auto; position:relative;
  `;
  overlay.appendChild(box);

  document.body.appendChild(overlay);
  return overlay;
}
// Show species plot modal for appearing/reappearing species
function showSpeciesPlotModal(speciesKey, speciesName, lmeName, prefix) {
  const overlay = document.getElementById("species-plot-modal");
  const box = document.getElementById("species-plot-modal-box");
  box.innerHTML = "";

  const title = document.createElement("h4");
  title.textContent = `${speciesName} – ${lmeName}`;
  title.style.marginTop = "0";
  box.appendChild(title);

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

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

  overlay.style.display = "flex";

  // Build JSON URLs using the prefix (appearing/reappearing) naming
  const baseName = prefix ? `${prefix}_lme_${lmeName}_species_${speciesKey}` : `lme_${lmeName}_species_${speciesKey}`;
  const url1 = PLOTS_JSON_BASE + encodeURIComponent(`${baseName}_1.json`);
  const url2 = PLOTS_JSON_BASE + encodeURIComponent(`${baseName}_2.json`);

  renderPlotlyChart(plotDiv1, url1, "No occurrences plot available.");
  renderPlotlyChart(plotDiv2, url2, "No grid-cells plot available.");
}
// Create map container
map_container = html`<div id="trends-map" style="height: 350px; width: 100%;"></div>`
// Initialise Leaflet map centred on European seas (same center and zoom as homepage)
map = {
  const m = L.map(map_container).setView([55.0, 15.0], 2);

  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
lme_layer = L.featureGroup().addTo(map)
// Add LME polygons to the map
add_lme_polygons = {
  lme_layer.clearLayers();

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

    const style = {
      fillColor:   selected_lme_id === lme_id ? "#3388ff" : "#cccccc",
      weight:      2,
      opacity:     1,
      color:       selected_lme_id === lme_id ? "#0066cc" : "#999999",
      fillOpacity: selected_lme_id === lme_id ? 0.8 : 0.3
    };

    const layer = L.geoJSON(feature, {
      style,
      onEachFeature: (feat, lyr) => {
        // Tooltip showing LME name
        lyr.bindTooltip(lme_name, { permanent: false, direction: "center" });

        // Click handler to select LME
        lyr.on("click", () => {
          mutable selected_lme_id = lme_id;
          // Update styles for all layers
          lme_layer.eachLayer(l => {
            const fid = l.feature.properties.lme_id;
            l.setStyle({
              fillColor:   fid === lme_id ? "#3388ff" : "#cccccc",
              color:       fid === lme_id ? "#0066cc" : "#999999",
              fillOpacity: fid === lme_id ? 0.8 : 0.3
            });
          });
        });

        // Hover effects
        lyr.on("mouseover", () => {
          if (selected_lme_id !== lme_id) {
            lyr.setStyle({ fillOpacity: 0.5 });
          }
        });
        lyr.on("mouseout", () => {
          if (selected_lme_id !== lme_id) {
            lyr.setStyle({ fillOpacity: 0.3 });
          }
        });
      }
    });

    layer.addTo(lme_layer);
  });
}
filtered_appearing = selected_lme_id !== null
  ? appearing_species_data.filter(d => +d.lme_id === selected_lme_id)
  : []

filtered_reappearing = selected_lme_id !== null
  ? reappearing_species_data.filter(d => +d.lme_id === selected_lme_id)
  : []

filtered_emerging = selected_lme_id !== null
  ? emerging_species_data.filter(d => +d.lme_id === selected_lme_id)
  : []
// Get selected LME name
selected_lme_name = selected_lme_id !== null
  ? lme_geojson.features.find(f => f.properties.lme_id === selected_lme_id)?.properties.lme_name || "Unknown"
  : null
selected_lme_id !== null ? html`<h3 style="margin-top: 0;">${selected_lme_name}</h3>` : html`<p style="text-align: center; color: #999; padding: 20px;">Click on a region to view species trends</p>`
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.

Appearing Species

selected_lme_id !== null && filtered_appearing.length > 0
  ? html`<p><strong>${filtered_appearing.length}</strong> appearing species between 2022 and now.</p>`
  : html``
// Display appearing species table
{
  if (selected_lme_id !== null && filtered_appearing.length > 0) {
    const table = Inputs.table(filtered_appearing, {
      columns: ["specieskey", "species", "first_year"],
      header: {
        specieskey: "speciesKey",
        species: "Species",
        first_year: "First Year"
      },
      width: {
        specieskey: 150,
        species: 350,
        first_year: 150
      },
      format: {
        specieskey: x => htl.html`<a href="https://www.gbif.org/species/${x}" target="_blank">${x}</a>`,
        species: (value, idx) => {
          const row = filtered_appearing[idx];
          const link = document.createElement("a");
          link.href = "#";
          link.textContent = value;
          link.style.cssText = "cursor:pointer;text-decoration:underline;";
          link.onclick = e => {
            e.preventDefault();
            showSpeciesPlotModal(row.specieskey, value, selected_lme_name, "appearing_species_plots_ggplot2");
          };
          return link;
        }
      },
      sort: "first_year",
      reverse: true
    });
    // Wrap in a plain container so Observable does not treat it as an input with checkboxes
    const wrapper = document.createElement("div");
    wrapper.appendChild(table);
    yield wrapper;
  } else if (selected_lme_id !== null) {
    yield html`<p style="text-align: center; color: #999; padding: 20px;">No appearing species data available for this LME</p>`;
  }
}

Reappearing Species

selected_lme_id !== null && filtered_reappearing.length > 0
  ? html`<p><strong>${filtered_reappearing.length}</strong> reappearing species between 2022 and now.</p>`
  : html``
// Display reappearing species table
{
  if (selected_lme_id !== null && filtered_reappearing.length > 0) {
    const table = Inputs.table(filtered_reappearing, {
      columns: ["specieskey", "species", "reappearance_year", "years_without_occurrences"],
      header: {
        specieskey: "speciesKey",
        species: "Species",
        reappearance_year: "Reappearance Year",
        years_without_occurrences: "Years Without Occurrences"
      },
      width: {
        specieskey: 150,
        species: 250,
        reappearance_year: 150,
        years_without_occurrences: 200
      },
      format: {
        specieskey: x => htl.html`<a href="https://www.gbif.org/species/${x}" target="_blank">${x}</a>`,
        species: (value, idx) => {
          const row = filtered_reappearing[idx];
          const link = document.createElement("a");
          link.href = "#";
          link.textContent = value;
          link.style.cssText = "cursor:pointer;text-decoration:underline;";
          link.onclick = e => {
            e.preventDefault();
            showSpeciesPlotModal(row.specieskey, value, selected_lme_name, "reappearing_species_plots_ggplot2");
          };
          return link;
        }
      },
      sort: "reappearance_year",
      reverse: true
    });
    const wrapper = document.createElement("div");
    wrapper.appendChild(table);
    yield wrapper;
  } else if (selected_lme_id !== null) {
    yield html`<p style="text-align: center; color: #999; padding: 20px;">No reappearing species data available for this LME</p>`;
  }
}

Emerging Species

selected_lme_id !== null && filtered_emerging.length > 0
  ? html`<p><strong>${filtered_emerging.filter(d => +d.weighted_em_status === 31.5).length}</strong> species with the highest total emerging score (31.5). Period: 2022-2024.</p>`
  : html``
// Display emerging species table
{
  if (selected_lme_id !== null && filtered_emerging.length > 0) {
    const table = Inputs.table(filtered_emerging, {
      columns: ["specieskey", "species_name", "weighted_em_status", "model", "growth"],
      header: {
        specieskey: "speciesKey",
        species_name: "Species",
        weighted_em_status: "Emergence Score",
        model: "Model",
        growth: "Growth"
      },
      width: {
        specieskey: 120,
        species_name: 250,
        weighted_em_status: 140,
        model: 100,
        growth: 120
      },
      format: {
        specieskey: x => htl.html`<a href="https://www.gbif.org/species/${x}" target="_blank">${x}</a>`,
        species_name: (value, idx) => {
          const row = filtered_emerging[idx];
          const link = document.createElement("a");
          link.href = "#";
          link.textContent = value;
          link.style.cssText = "cursor:pointer;text-decoration:underline;";
          link.onclick = e => {
            e.preventDefault();
            showSpeciesPlotModal(row.specieskey, value, selected_lme_name, null);
          };
          return link;
        },
        growth: x => x ? (+x).toFixed(4) : "—"
      }
    });
    const wrapper = document.createElement("div");
    wrapper.appendChild(table);
    yield wrapper;
  } else if (selected_lme_id !== null) {
    yield html`<p style="text-align: center; color: #999; padding: 20px;">No emerging species data available for this LME</p>`;
  }
}

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