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>`

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