indicator = "Urban"
import { concat } from '@uwdata/arquero'
largestAbs = (a, b) => Math.max(Math.abs(a), Math.abs(b))
// rounding function
function roundToDecimal(number, decimals) {
  // Multiply the number by 10^decimals
  let factor = Math.pow(10, decimals);
  
  // Round to the nearest integer and then divide by the same factor
  return Math.round(number * factor) / factor;
}
function quantile(arr, q) {
  const nums = arr.filter(d => Number.isFinite(d)).slice();
  if (!nums.length) return undefined;
  nums.sort((a, b) => a - b);
  const pos = (nums.length - 1) * q;
  const base = Math.floor(pos);
  const rest = pos - base;
  if (nums[base + 1] !== undefined) {
    return nums[base] + rest * (nums[base + 1] - nums[base]);
  } else {
    return nums[base];
  }
}
function get_data_spread(plot_data, col_var) {

  // --- Extract numeric values, filter out invalids ---
  const numericValues = plot_data.features
    .map(f => f.properties[col_var])
    .filter(v => v != null && !isNaN(v));
  
  const sorted = numericValues.slice().sort((a,b)=>a-b);
  const min = sorted.length ? d3.quantile(sorted, 0.025) : 0;
  const max = sorted.length ? d3.quantile(sorted, 0.975) : 100;   /// this works for percentages - may need to change if using ha??

  const mid = 0;  
  const max_div = largestAbs(min, max); 
  
  return([min, max, max_div])
}
// return min and max of map datasets that have shared columns 

function getCombinedMinMax(data1, data2, column) {
  // Convert to plain arrays if they are Arquero tables
  const arr1 = typeof data1.objects === "function" ? data1.objects() : data1;
  const arr2 = typeof data2.objects === "function" ? data2.objects() : data2;

  // Combine and filter valid numeric values
  const allValues = [...arr1, ...arr2]
    .map(d => d[column])
    .filter(v => v != null && !isNaN(v));

  if (allValues.length === 0) return { min: 0, max: 0 };

  return {
    min: Math.min(...allValues),
    max: Math.max(...allValues)
  };
}
d3 = require("d3@7")  // need this for viridis

function map(plot_data, col_var, nz_outline, options = {}) {
  // defaults and merge with provided options
  const {
    min_max = null,
    legend_title = 'Landcover area (ha)',
    type = 'state',
    mapTitle = "",
    wetland = "FALSE",
    id = `map-${Math.random().toString(36).slice(2)}`  // unique map id
  } = options;

  // --- create the div and Leaflet map ---
  //const div = html`<div id="${id}" style="height:500px; width:100%; position:relative; background-color:#666363;"></div>`;
  const div = html`<div id="${id}" style="height:500px; width:100%; position:relative; background-color: #FFFFFF;"></div>`;
  const m = L.map(div, {
    zoomControl: true,
    attributionControl: false
  });

  // Add NZ outline as a dark background layer
  const nzOutlineLayer = L.geoJSON(nz_outline, {
    style: {
      fillColor: '#E8E8E8', //'#FFFFFF', // white fill //"#1D1C1C", // Dark fill // '#E8E8E8',// light grey
      fillOpacity: 1,
      color: '#E8E8E8', //"#000",         // Optional dark border
      weight: 0.5
    }
  }).addTo(m);



  // --- Title overlay ---
  if (mapTitle) {
    const titleDiv = document.createElement("div");
    titleDiv.innerText = mapTitle;
    titleDiv.style.position = "absolute";
    titleDiv.style.top = "8px";
    titleDiv.style.left = "50%";
    titleDiv.style.transform = "translateX(-50%)";
    titleDiv.style.background = "rgba(255,255,255,0.8)";
    titleDiv.style.padding = "2px 2px 2px 2px";
    titleDiv.style.font = "14px sans-serif";
    titleDiv.style.fontWeight = "bold";
    titleDiv.style.borderRadius = "4px";
    titleDiv.style.pointerEvents = "none";
    div.appendChild(titleDiv);
  }

  // --- unique gradient id for legend ---
  const gradientId = `legend-gradient-${id}`;


  // --- Extract numeric values, filter out invalids ---

  const numericValues = plot_data.features
    .map(f => f.properties[col_var])
    .filter(v => v != null && !isNaN(v));

  const sorted = numericValues.slice().sort((a,b)=>a-b);

  // Conditional logic based on input_max_div
  let min, max, max_div;

    min = sorted.length ? d3.quantile(sorted, 0.025) : 0;
    max = sorted.length ? d3.quantile(sorted, 0.975) : 100;
    max_div = largestAbs(min, max);

  const mid = 0;

  // adjust col_var to 95 quantiles for colour
  // Add an index into GeoJSON before table conversion
  plot_data.features.forEach((f, i) => f.properties.__id = i);

  const aq_plot_data = aq.from(plot_data.features)
      .derive({
        col_adjusted: aq.escape(d => {
          const v = Number(d.properties[col_var]);
          if (isNaN(v)) return null; // or keep original here if you prefer
          if (v > max) return max;
          if (v < min) return min;
          return v;
        })
      })
      
  // note this is wrapped so quarto knows it outputs a value    
  const adj_plot_data = ({
    ...plot_data,
    features: aq_plot_data.objects().map(row => {
      const i = row.properties.__id;
      return {
        ...plot_data.features[i],
        properties: {
          ...plot_data.features[i].properties,
          adj_col_var : row.col_adjusted
        }
      };
    })
  });
  

  // --- Polygon color scale ---

  // Set Bonnie's diverging colour scale (white centre in RColourBrewer Orange to Purple)
  const hexColors = [ "#7F0000", "#FF7F0E", "#F5F5F5", "#9B30FF","#3F0071"];
  // try vanimo high contrast
  //const hexColors = ["#E4B4D2", "#BC71A7", "#833C71", "#401D34", "#1F1816", "#2E351C", "#506734", "#7DA351", "#B3D189"];
  const customInterpolator = d3.piecewise(d3.interpolateRgb, hexColors);

  const baseScale = type === "state"
    ? d3.scaleSequential(d3.interpolateViridis).domain([min, max])
    : d3.scaleDiverging()
      .domain([-max_div, mid, max_div]) // Your data's range: low, neutral, high
      .interpolator(customInterpolator);

  const colorScale = v => (v == null || isNaN(v)) ? "#cccccc" : baseScale(v);

  // --- create polygons

  const polyLayer = L.geoJSON(adj_plot_data, {
    style: feature => {
      const val = feature.properties.adj_col_var;
      return {
        fillColor: val == null ? "#d3d3d3" : colorScale(val),
        color: val == null ? "#d3d3d3" : colorScale(val),  // colour outline to reduce white showing in rendering
        fillOpacity: 0.7,
        stroke: true,
        opacity: 0.7,
        weight: .5,
      };
    },
    onEachFeature: (feature, layer) => {
      layer.bindPopup(
        wetland === 'FALSE'
          ? type === 'state'
            ? `
              polygon ID: ${feature.properties.seqnum}<br>
              Landcover: ${feature.properties.landcover}<br>
              Area (ha): ${feature.properties.area_ha}<br>
              Percent area: ${feature.properties.percent_landcover}<br>
            `
            : `
              Polygon : ${feature.properties.seqnum}<br>
              Landcover: ${feature.properties.landcover}<br>
              Percent change: ${feature.properties.percent_change}<br>
              Change (ha): ${feature.properties.change_ha}<br>
            `
          : type === 'state'
            ? `
              polygon ID: ${feature.properties.seqnum}<br>
              Area (ha): ${feature.properties.wetland_area_ha}<br>
            `
            : `
              Polygon : ${feature.properties.seqnum}<br>
              Change (ha): ${feature.properties.change_ha}<br>
            `
      );
    }
  }).addTo(m);

  // --- Legend ---
  const legend = L.control({ position: "bottomright" });
  legend.onAdd = function () {
    const div = L.DomUtil.create("div", `info legend legend-${id}`);
    div.style.background = "none";
    div.style.padding = "0";
    div.style.margin = "0";
    div.style.marginBottom = "20px";
    div.style.marginRight = "10px";
    div.style.font = "11px/1.3 sans-serif";
    div.style.color = "#333";
    div.style.display = "flex";
    div.style.flexDirection = "column";
    div.style.alignItems = "flex-start";

    const barHeight = 180;

    // Title
    const title = document.createElement("div");
    title.innerHTML = legend_title;
    title.style.fontWeight = "bold";
    title.style.marginBottom = "4px";
    div.appendChild(title);

    // --- Gradient row ---
    const rowDiv = document.createElement("div");
    rowDiv.style.display = "flex";
    rowDiv.style.alignItems = "flex-start";

    // Gradient SVG
    const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
    svg.setAttribute("width", 20);
    svg.setAttribute("height", barHeight);

    const defs = document.createElementNS("http://www.w3.org/2000/svg", "defs");
    const linearGrad = document.createElementNS("http://www.w3.org/2000/svg", "linearGradient");
    linearGrad.setAttribute("id", gradientId);
    linearGrad.setAttribute("x1", "0%");
    linearGrad.setAttribute("y1", "100%");
    linearGrad.setAttribute("x2", "0%");
    linearGrad.setAttribute("y2", "0%");

    const nStops = 50;
    for (let i = 0; i <= nStops; i++) {
      const t = i / nStops;
      let val;
      if (type === 'state') {
        val = min + t * (max - min);
      } else {
        const scale = d3.scaleLinear().domain([0, 0.5, 1]).range([-max_div, mid, max_div]);
        val = scale(t);
      }
      const stop = document.createElementNS("http://www.w3.org/2000/svg", "stop");
      stop.setAttribute("offset", `${t*100}%`);
      stop.setAttribute("stop-color", colorScale(val));
      linearGrad.appendChild(stop);
    }
    defs.appendChild(linearGrad);
    svg.appendChild(defs);

    const rect = document.createElementNS("http://www.w3.org/2000/svg", "rect");
    rect.setAttribute("x", 0);
    rect.setAttribute("y", 0);
    rect.setAttribute("width", 20);
    rect.setAttribute("height", barHeight);
    rect.setAttribute("fill", `url(#${gradientId})`);
    rect.setAttribute("stroke", "black");
    rect.setAttribute("stroke-width", "0.3");
    svg.appendChild(rect);

    const line = document.createElementNS("http://www.w3.org/2000/svg", "line");
    line.setAttribute("x1", 0);
    line.setAttribute("y1", barHeight);
    line.setAttribute("x2", 20);
    line.setAttribute("y2", barHeight);
    line.setAttribute("stroke", "black");
    line.setAttribute("stroke-width", "0.6");
    svg.appendChild(line);

    rowDiv.appendChild(svg);

    // Labels
    const minCLabel = `< ${Math.round(-max_div)}`;
    const maxCLabel = `> ${Math.round(max_div)}`;
    const midCLabel = "0";
    const maxSLabel = `> ${Math.round(max_div)}`;

    const labelsDiv = document.createElement("div");
    labelsDiv.style.display = "flex";
    labelsDiv.style.flexDirection = "column";
    labelsDiv.style.height = `${barHeight}px`;
    labelsDiv.style.justifyContent = "space-between";
    labelsDiv.style.marginLeft = "4px";
    labelsDiv.innerHTML = type === "state"
      ? `
        <span>${maxSLabel}</span>
        <span>${Math.round((max+min)/2)}</span>
        <span>${Math.round(min)}</span>
      `
      : `
        <span>${maxCLabel}</span>
        <span>${midCLabel}</span>
        <span>${minCLabel}</span>
      `;
    rowDiv.appendChild(labelsDiv);

    div.appendChild(rowDiv);

    return div;
  };
  legend.addTo(m);

  // --- map sizing hacks ---
  
  const bounds = L.latLngBounds([
    [-46.0, 166.0],
    [-34.0, 179.0]
  ]);

  const resizeObserver = new ResizeObserver(() => {
    m.invalidateSize();
    m.fitBounds(bounds); // optional if you want to maintain framing
  });
  resizeObserver.observe(div);

  // Fit bounds once layout is ready
  setTimeout(() => {
    try {
      //m.fitBounds(polyLayer.getBounds());
      m.invalidateSize();
    } catch (err) {
      console.warn("fitBounds failed:", err);
    }
  }, 300); // allow Quarto's grid layout to finalize

  // --- cleanup on invalidation ---
  invalidation.then(() => {
    resizeObserver.disconnect();
    m.remove();
  });

  // --- Return ---
  return Object.assign(div, {
    value: m,
    invalidateSize: () => m.invalidateSize(),
    fitBounds: b => m.fitBounds(b),
    remove: () => m.remove(),
    legend,
    ready: Promise.resolve()
  });
}
// can add a swatch to the map graph to add a value for NA into the legend label etc.

swatch = function (maindiv){   const naDiv = document.createElement("div");
    naDiv.style.display = "flex";
    naDiv.style.alignItems = "center";
    naDiv.style.marginTop = "6px";

    const naBox = document.createElement("div");
    naBox.style.width = "20px";
    naBox.style.height = "20px";
    naBox.style.background = "#cccccc";
    naBox.style.border = "0.3px solid black";

    const naLabel = document.createElement("span");
    naLabel.style.fontSize = "10px";
    naLabel.style.marginLeft = "4px";
    naLabel.innerText = "NA";           // adjust label as needed

    naDiv.appendChild(naBox);
    naDiv.appendChild(naLabel);

    maindiv.appendChild(naDiv);
}
// Utility function to convert array of objects → CSV
function toCSV(data) {
  if (!data.length) return "";
  
  const headers = Object.keys(data[0]);
  const rows = data.map(row => 
    headers.map(h => JSON.stringify(row[h] ?? "")).join(",")
  );
  
  return [headers.join(","), ...rows].join("\n");
}
function joinGeoJSON(geojson, table, key = "seqnum") {
  // Build a lookup map from the table for fast joins
  let lookup = new Map(table.map(d => [String(d[key]), d]));

  return {
    type: "FeatureCollection",
    features: geojson.features
      .map(f => {
        let match = lookup.get(String(f.properties[key]));
        if (!match) return null;  // skip if no match
        return {
          ...f,
          properties: {
            ...f.properties,
            ...match
          }
        };
      })
      .filter(f => f !== null) // remove unmatched features
  };
}
// Function to sync maps
function syncMaps(mapA, mapB) {
  let syncing = false; // prevent infinite loop

  mapA.on('move', () => {
    if (syncing) return;
    syncing = true;
    const center = mapA.getCenter();
    const zoom = mapA.getZoom();
    mapB.setView(center, zoom);
    syncing = false;
  });

  mapB.on('move', () => {
    if (syncing) return;
    syncing = true;
    const center = mapB.getCenter();
    const zoom = mapB.getZoom();
    mapA.setView(center, zoom);
    syncing = false;
  });
}
// loads two maps in one container with spinner 

// --- wrapper function for your maps ---
function mapsWithLoading(maps) {
  const wrapper = html`<div style="position: relative">
    <!-- loading overlay -->
    <div class="loading-overlay" 
         style="position: absolute; inset: 0; 
                display: flex; align-items: center; justify-content: center;
                background: rgba(255,255,255,0.8); z-index: 10; font-weight: bold;">
      Loading maps...
    </div>

    <!-- grid of maps -->
    <div style="display: grid; grid-template-columns: 1fr 1fr; gap: 20px; margin-bottom: 15px">
      <div>${maps.map1}</div>
      <div>${maps.map2}</div>
    </div>
  </div>`;

  const overlay = wrapper.querySelector(".loading-overlay");

  let loadedCount = 0;
  const totalMaps = 2; // change if you add more maps later

  // Listen for map load events
  [maps.map1, maps.map2].forEach(m => {
    m.on("load", () => {
      loadedCount++;
      if (loadedCount === totalMaps) {
        overlay.style.display = "none"; // hide overlay when all are ready
      }
    });
  });

  return wrapper;
}
from_years  = ['1996', '2001', '2008', '2012', '2018']
years = ['1996', '2001', '2008', '2012', '2018', '2023']
// App specific values for dropdown menus
regions = ["New Zealand", "Northland", "Auckland", "Waikato", "Bay of Plenty", "Gisborne", "Hawke's Bay", "Taranaki",  "Manawat\u016B-Whanganui", "Wellington", "Tasman", "Nelson", "Marlborough", "West Coast", "Canterbury", "Otago", "Southland",  "Islands outside region"]  // removed "Chatham Islands", - just using area outside region
all_medium_class = ["Indigenous total", "Exotic total", "Urban area total", "Artificial bare surfaces", "Cropping/horticulture", "Exotic forest", "Exotic grassland", "Exotic scrub/shrubland", "Indigenous forest", "Indigenous scrub", "Natural bare/lightly vegetated surfaces",  "Other herbaceous vegetation", "Tussock grassland", "Urban area", "Water bodies"]  // removed "Not land", and "NA"


medium_class = ({
  Indigenous: ["All", "Indigenous forest", "Indigenous scrub/shrubland", 
               "Natural bare/lightly-vegetated surfaces", 
               "Other herbaceous vegetation", "Tussock grassland"],
  Exotic: ["All", "Cropping/horticulture", "Exotic forest", "Exotic grassland", "Exotic scrub/shrubland"],  // medium_class changed from Cropland to Cropping/horticulture, and Exotic scrub to Exotic scrub/shrubland in 2025
  Urban: ["All", "Artificial bare surfaces", "Urban area"], 
  Wetland: ["Artificial bare surfaces", "Cropping/horticulture", "Exotic forest", "Exotic grassland", "Exotic scrub/shrubland", "Indigenous forest", "Indigenous scrub/shrubland", "Natural bare/lightly-vegetated surfaces", "Other herbaceous vegetation", "Tussock grassland", "Urban area", "Water bodies"]
})[indicator] ?? ["All"]; 


// Define the options for the detailed_class based on medium_class
detailed_class = ({
  Indigenous: {
    "All": ["All"], 
    "Indigenous forest": ["All", "Broadleaved indigenous hardwoods", "Indigenous forest", "Indigenous forest - harvested"],
    "Indigenous scrub/shrubland": ["All", "Fernland", "M\u0101nuka and/or k\u0101nuka", "Sub alpine shrubland", "Matagouri or Grey scrub", "Mangrove", "Peat shrubland (Chatham Islands)", "Dune shrubland (Chatham Islands)", "Indigenous scrub - harvested"],   
    "Natural bare/lightly-vegetated surfaces": ["All", "Sand or gravel", "Landslide", "Permanent snow and ice", "Alpine grass/herbfield", "Gravel or rock"], 
    "Other herbaceous vegetation": ["All", "Herbaceous freshwater vegetation", "Herbaceous saline vegetation", "Flaxland"], 
    "Tussock grassland": ["All"]  // there is only one detailed class of tussock == 'Tall tussock grassland'. Removing for reduced memory load
  },
  
  Exotic: {
     "All": ["All"], 
     "Cropping/horticulture": ["All", "Orchard, vineyard or other perennial crop", "Short-rotation cropland"],  // medium_class changed from Cropland to Cropping/horticulture in 2025
     "Exotic forest": ["All", "Deciduous hardwoods", "Exotic forest", "Forest - harvested"],
     "Exotic grassland": ["All", "Depleted grassland", "High producing exotic grassland", "Low producing grassland"], 
     "Exotic scrub/shrubland": ["All", "Gorse and/or broom", "Mixed exotic shrubland"]
  },
  
  Urban: {
    "All": ["All"], 
    "Artificial bare surfaces": ["All", "Surface mine or dump", "Transport infrastructure"],
    "Urban area": ["All", "Built-up area (settlement)", "Urban parkland/open space"]
  }, 
  
  Wetland: {
    "All": ["All"],
    "Artificial bare surfaces": ["All", "Surface mine or dump", "Transport infrastructure"],
    "Cropping/horticulture": ["All", "Orchard, vineyard or other perennial crop", "Short-rotation cropland"],
    "Exotic forest": ["All", "Deciduous hardwoods", "Exotic forest", "Forest - harvested"],
    "Exotic grassland": ["All", "High producing exotic grassland", "Low producing grassland"],
    "Exotic scrub/shrubland": ["All", "Gorse and/or broom", "Mixed exotic shrubland"],
    "Indigenous forest": ["All", "Broadleaved indigenous hardwoods", "Indigenous forest"],
    "Indigenous scrub/shrubland": ["All", "Fernland", "Mangrove", "M\u0101nuka and/or k\u0101nuka", "Matagouri or grey scrub", "Sub alpine shrubland"],
    "Natural bare/lightly-vegetated surfaces": ["All", "Gravel or rock", "Sand or gravel"],
    "Other herbaceous vegetation": ["All", "Flaxland", "Herbaceous freshwater vegetation", "Herbaceous saline vegetation"],
    "Tussock grassland": ["All", "Tall tussock grassland"],
    "Urban area": ["All", "Built-up area (settlement)", "Urban parkland/open space"],
    "Water bodies": ["All", "Estuarine open water", "Lake or pond", "River"]
}
  
})[indicator] ?? ["All"];
lcdb_change =  FileAttachment("Urban_landcover_change.csv").csv({ typed: true })
metadata = FileAttachment("concordance_pal_descriptions.csv").csv({ typed: true})
import { aq, op, table, rename } from '@uwdata/arquero'
import {tsvParse} from "@observablehq/d3"
viewof medium_category = Inputs.select(medium_class, {value: "All", label: "Medium class: "})
viewof detailed_category = Inputs.select(detailed_class[medium_category], {value: detailed_class[medium_category][0], label: "Detailed class: "})
viewof from_year = Inputs.select(from_years, {value: '2018', label: "From year: "})
viewof to_year = Inputs.select(years.slice(years.indexOf(from_year) + 1, 6), {value: '2023', label: "To year: "})
// Combine them into a two-column layout
html`<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 20px; row-gap: 5px; margin-bottom: 15px">
  <div>${viewof medium_category}</div>
  <div>${viewof from_year}</div>
  <div>${viewof detailed_category}</div>
  <div>${viewof to_year}</div>
</div>`
which_category = detailed_category === "All"
        ? medium_category === "All"
          ? medium_class
          : medium_category
        : detailed_category

// select columnames
class_from = detailed_category === "All"
    ? "from_lcdb_medium_class"
    : "from_lcdb_detailed_class"

class_to = detailed_category === "All"
        ? "to_lcdb_medium_class"
        : "to_lcdb_detailed_class" 


from_array = aq.from(lcdb_change.filter(function(data) {
           const tmp = (which_category.includes(data[class_from])) &&     
           from_year.includes(data.year_from) && to_year.includes(data.year_to) &&
           !which_category.includes(data[class_to])  // exclude landcover which doesn't change category

    return(tmp)
  }))
  .groupby('region_name')
  .rollup({loss_ha: aq.op.sum('area_ha')})
  .orderby(aq.desc("region_name")) 
  .select('region_name', 'loss_ha')

  // permanent ice and snow only has losses - requires empty to_array
  blank_array = from_array
    .derive({ area_ha: d => 0 })
    .select('region_name', 'area_ha')
 
 // check if there are values in the to_ class column
 tmp_array = aq.from(lcdb_change.filter(function(data) {
           const tmp = (which_category.includes(data[class_to])) &&     
          from_year.includes(data.year_from) && to_year.includes(data.year_to)&&
           !which_category.includes(data[class_from])  // exclude landcover which doesn't change category
    return(tmp)
    
  })) 
  
  // permanent ice and snow only has losses - requires empty to_array (nothing goes to ice and snow)
  // replace any arrays of size zero with a blank array
  tmp2_array = tmp_array.size > 0 ? tmp_array : blank_array
  
  //tmp2_array

  to_array = tmp2_array
  // summarise data by region
  .groupby('region_name')
  .rollup({gain_ha: aq.op.sum('area_ha')})
  .orderby(aq.desc("region_name"))
  .select('region_name', 'gain_ha')
  
region_order = regions.slice(1) // remove NZ from list

  
plot_data = to_array
  .join_full(from_array, "region_name")
  .derive({ loss_ha: d => d.loss_ha || 0})    // replace missing values with 0 so net change calculates correctly
  .derive({ gain_ha: d => d.gain_ha || 0})
  .derive({ net_ha: d => d.gain_ha - d.loss_ha  })
  .derive({ loss_ha: d => - aq.op.round(d.loss_ha)})   // round final values for graphing
  .derive({ gain_ha: d => aq.op.round(d.gain_ha)})
  .derive({ net_ha: d => aq.op.round(d.net_ha)})
  .derive({ order: aq.escape(row => region_order.indexOf(row["region_name"])) })
  .orderby("order")      // sort by the numeric helper
  //.select(d => d, { drop: ["order"] }) // remove helper column if you want
  .objects()   // <-- convert final table to array of objects
  .map(d => ({
    ...d,
    tooltip: `<strong>${d.region_name}</strong><br>Gain: ${d.gain_ha}<br>Loss: ${d.loss_ha}<br>Net: ${d.net_ha}`
  }));
  • Graph
  • Table
  • Metadata
  • Download
tooltip = {
  const el = document.createElement("div");
  el.style.cssText = `
    position: absolute;
    pointer-events: none;
    background: white;
    border: 1px solid #ccc;
    padding: 6px 10px;
    border-radius: 4px;
    box-shadow: 0 2px 6px rgba(0,0,0,0.2);
    font-family: sans-serif;
    font-size: 12px;
    display: none;
  `;
  document.body.appendChild(el);
  return el;   // 👉 return it so other cells can use "tooltip"
}
plot_title = detailed_category === "All"
  ? medium_category === "All"
    ? `Change in ${indicator.toLowerCase()} land cover area, by region, ${from_year}\u2013${to_year}`
    : `Change in ${medium_category.toLowerCase()} land cover area, by region, ${from_year}\u2013${to_year}`
  : `Change in ${detailed_category.toLowerCase()} land cover area, by region, ${from_year}\u2013${to_year}`;

html`<p style="text-align:center; font-weight:bold; font-size:14px; margin:10px 0;">${plot_title}</p>`;
(() => {


  const xMin = Math.min(...plot_data.map(d => d.loss_ha));
  const xMax = Math.max(...plot_data.map(d => d.gain_ha));

  const p = Plot.plot({
    width: 800,
    marginRight: 10,
    marginLeft: 150,   // space to the left of the chart
    marginBottom: 30, // space below the chart (space before caption rendered)
    insetBottom: 20,  // space between the x-axis and the marks
    insetLeft: 20,    // space between the y-axis and the marks
    insetRight: 20,
    // styling options
    style: {
      //backgroundColor: "#7e9a9a",
      color: "black",
      fontFamily: "system-ui",
      fontSize: "14px",
      overflow: "visible"  // not clipped and rendered outside the box
    },
    x: { domain: [xMin, xMax], grid: true,  label: null, labelAnchor: "Center" },
    y: { type: "band", domain: plot_data.map(d => d.region_name), padding: 0.2, label: null },
    marks: [
      Plot.barX(plot_data, { y: "region_name", x: "gain_ha", fill: "#4B0082", title: "Gain", ariaLabel: d => d.tooltip  }),
      Plot.barX(plot_data, { y: "region_name", x: d => d.loss_ha, fill: "#B22222", title: "Loss", ariaLabel: d => d.tooltip  }),
      Plot.dot(plot_data, { y: "region_name", x: d => d.net_ha, fill: "black", r: 4, title: "Net change", ariaLabel: d => d.tooltip  }),
      Plot.ruleX([0]),
      Plot.frame({anchor: "left"}), 
      Plot.frame({anchor: "bottom"})
    ],
    color: { legend: true }
  });

  // Attach tooltip events
  p.querySelectorAll("rect, circle").forEach(el => {
    el.addEventListener("mousemove", event => {
      tooltip.style.display = "block";
      tooltip.innerHTML = el.getAttribute("aria-label");
      tooltip.style.left = (event.pageX + 10) + "px";
      tooltip.style.top = (event.pageY + 10) + "px";
    });
    el.addEventListener("mouseout", () => {
      tooltip.style.display = "none";
    });
  });
    
  // Add a manual centered x-axis label (no arrow)
  const xlabel = html`<p style="text-align:center; font-size:14px; margin:6px 0 0 0;">Hectares</p>`;

  
  // Add a custom legend below the plot
  const legend = html`
    <div style="
      display: flex;
      justify-content: center;
      align-items: center;
      gap: 20px;
      margin-top: 10px;
      font-family: system-ui;
      font-size: 14px;">
      <div style="display: flex; align-items: center; gap: 6px;">
        <div style="width: 15px; height: 15px; background-color: #4B0082;"></div> Gain
      </div>
      <div style="display: flex; align-items: center; gap: 6px;">
        <div style="width: 15px; height: 15px; background-color: #B22222;"></div> Loss
      </div>
      <div style="display: flex; align-items: center; gap: 6px;">
        <div style="width: 10px; height: 10px; border-radius: 50%; background-color: black;"></div> Net change
      </div>
    </div>
  `;

  // Combine chart and legend
  const container = html`<div style="text-align: center;"></div>`;
  container.append(p, xlabel, legend);
  return container;
})();
html`<p style="text-align:right; font-size:10px;">Source: Stats NZ using data from Bioeconomy Science Institute</p>`
Inputs.table(plot_data, {
  columns: [
    "region_name",
    "gain_ha",
    "loss_ha",
    "net_ha"
  ],
  header: {
    region_name: "Region", 
    gain_ha: "Gain (ha)",
    loss_ha: "Loss (ha)",
    net_ha: "Net change (ha)"
  }
});
display_meta = aq.from(metadata)
  .select("class_code", "medium_class", "detailed_class", "description")
  .rename({
    class_code: "Class code",
    medium_class: "Medium class",
    detailed_class: "Detailed class",
    description: "Description"
  })
  .objects()

Inputs.table(await display_meta)
filename = plot_title
  .replace(/\u2013/g, "-")   // replace en dash with hyphen
  .replace(/,/g, "")         // remove commas
  .replace(/\s+/g, "_")      // replace spaces with underscores
  .replace(/[^\w\-]+/g, "")  // strip non-word chars except underscore & hyphen
  + ".csv";

// Clean data (remove tooltip)
plot_data_clean = plot_data.map(({ tooltip, ...rest }) => rest);


// Download button
downloadCSV = {
  const button = html`<button>Download current selection as a csv</button>`;

  button.onclick = () => {
    // Convert to CSV
    const csv = toCSV(plot_data_clean);

    // Create blob
    const blob = new Blob([csv], { type: "text/csv" });
    const url = URL.createObjectURL(blob);

    // Create temporary <a> link
    const a = document.createElement("a");
    a.href = url;
    a.download = filename;   // ✅ use dynamic filename
    document.body.appendChild(a);
    a.click();
    document.body.removeChild(a);

    // Cleanup
    setTimeout(() => URL.revokeObjectURL(url), 10000);
  };

  return button;
}