The Africa Agriculture Adaptation Atlas The Africa Agriculture Adaptation Atlas
  • Documentation
  • Report an Issue
import { lang as Lang } from "/helpers/lang.js";
import { atlasHero, downloadButton } from "/helpers/uiComponents.ojs";
import { patchWindowsCache } from "/helpers/data.js";
import { atlasTOC } from "/helpers/toc.ojs";
import { renderGeoMap } from "/helpers/figures.ojs";

// Data imports
general_translations = await FileAttachment(
  "/data/shared/generalTranslations.json",
).json();
vulnerability_translations = await FileAttachment(
  "/data/vulnerability_notebook/translations.json",
).json();
hero_url = "./../../images/default_crop.webp";
// Hero section with dynamic title
nbTitle = _lang(vulnerability_translations.notebook_title);

atlasHero(nbTitle, hero_url);
atlasTOC({
  skip: ["notebook-title", "appendix", "source-code"],
  heading: `<b>${_lang(general_translations.toc)}</b>`,
});
mapWidth = typeof width !== 'undefined' ? Math.min(width, 625) : 625
mapHeight = 600

// Mutable state for cross-section reactivity - CENTRALIZED ADMIN SELECTIONS
mutable sharedAdmin0 = null
mutable sharedAdmin1 = null
mutable sharedAdmin2 = null

/**
 * Update all shared admin selections synchronously across all sections
 * 
 * @param {string|null} admin0 - Admin0 selection value
 * @param {string|null} admin1 - Admin1 selection value  
 * @param {string|null} admin2 - Admin2 selection value
 */
function updateSharedAdminSelections(admin0, admin1, admin2) {
  mutable sharedAdmin0 = admin0;
  mutable sharedAdmin1 = admin1;
  mutable sharedAdmin2 = admin2;
}
languages = [
  { key: "en", label: "English", locale: 'en-US' },
  { key: "fr", label: "Français", locale: 'fr-FR' }
]

defaultLangKey = {
  const name = "lang";
  const list = languages.map((d) => d.key);
  const defaultKey = "en";
  const queryParam = await Lang.getParamFromList({ name, list });
  return queryParam ?? defaultKey;
}

_lang = Lang.lg(masterLanguage.key)

globalSelection = {
  return {
    label: _lang({en: "Sub-Saharan Africa", fr: "Afrique subsaharienne"}),
    labelGeneral: _lang({en: "Africa", fr: "Afrique"}),
 
  }
}
adminRegions = {
  return {
    labels: {
      admin0: _lang(vulnerability_translations.country),
      admin1: _lang(vulnerability_translations.region),
      admin2: _lang(vulnerability_translations.subregion)
    }
  };
}

// Get current admin selections as reactive object - now using shared state
adminSelections = ({
  selectAdmin0: sharedAdmin0,
  selectAdmin1: sharedAdmin1,
  selectAdmin2: sharedAdmin2
})
mutable dataCache = new Map()

/**
 * Load data with caching to avoid redundant loads
 * 
 * @param {string} key - Unique cache key for this data
 * @param {Function} loader - Async function that returns the data to cache
 * @returns {Promise<*>} Cached data if available, otherwise loaded data
 */
async function loadDataWithCache(key, loader) {
  if (dataCache.has(key)) {
    return dataCache.get(key);
  }
  
  try {
    const startTime = performance.now();
    
    const data = await loader();
    
    const loadTime = performance.now() - startTime;
    
    dataCache.set(key, data);
    return data;
  } catch (error) {
    console.error(`Error loading ${key}:`, error);
    return null;
  }
}

/**
 * Create DuckDB connection with exposure data parquet file loaded as a view
 * Uses caching to avoid redundant connections
 * 
 * @returns {Promise<Object|null>} DuckDB client with exposure_data view, or null on failure
 */
async function createExposureDB() {
  return loadDataWithCache('exposureDB', async () => {
    try {
      const parquetUrl = await FileAttachment(
        "/data/vulnerability_notebook/notebook_exposure.parquet",
      ).url();
      const db = await DuckDBClient.of();
      await db.query(`
        CREATE OR REPLACE VIEW exposure_data AS
        SELECT * FROM read_parquet('${parquetUrl}')
      `);
      return db;
    } catch (error) {
      console.error("Failed to create DuckDB connection for exposure data:", error);
      return null;
    }
  });
}
import { getAdminBoundaries } from "/components/_atlasBoundaries.ojs";
// Cached boundary data loading
boundaries = getAdminBoundaries([0, 1, 2]);
dataAdmin0 = {
  const data = boundaries.admin0.features.map(d => d.properties)
  // add a blank value and sort alphabetically
  return [null, ...data.map(d => d.admin0_name)].map(d => {
    return {label: d == null ? globalSelection.label : d, value: d}
  }).sort((a, b) => {
    // Keep null/blank value at the top
    if (a.value === null) return -1;
    if (b.value === null) return 1;
    // Sort others alphabetically by label
    return a.label.localeCompare(b.label);
  })
}

dataAdmin1 = {
  // admin 1, filter by 0 - using shared state
  let data;
  if (sharedAdmin0 && sharedAdmin0 !== "SSA") {
    // Filter by selected country
    data = boundaries.admin1.features.map(d => d.properties)
      .filter(d => d.admin0_name == sharedAdmin0);
  } else {
    // If no country selected or SSA selected, show empty (just null option)
    data = [];
  }
  
  // add blank value and sort alphabetically
  return [null, ...data.map(d => d.admin1_name)].map(d => {
    return {label: d, value: d}
  }).sort((a, b) => {
    // Keep null/blank value at the top
    if (a.value === null) return -1;
    if (b.value === null) return 1;
    // Sort others alphabetically by label
    return a.label.localeCompare(b.label);
  })
}

dataAdmin2 = {
  let data;
  if (sharedAdmin0 && sharedAdmin0 !== "SSA" && sharedAdmin1) {
    // Filter by selected country and region
    data = boundaries.admin2.features.map(d => d.properties)
      .filter(d => d.admin0_name == sharedAdmin0 && d.admin1_name == sharedAdmin1);
  } else {
    // If no country or region selected, show empty (just null option)
    data = [];
  }
  
  // add blank value and sort alphabetically
  return [null, ...data.map(d => d.admin2_name)].map(d => {
    return {label: d, value: d}
  }).sort((a, b) => {
    // Keep null/blank value at the top
    if (a.value === null) return -1;
    if (b.value === null) return 1;
    // Sort others alphabetically by label
    return a.label.localeCompare(b.label);
  })
}
// Bidirectional synchronization: Update shared state when ANY section's selectors change
// This reactive cell monitors all section admin selectors and updates shared state when any change
// The underscore prefix ensures this cell doesn't display output in the notebook
// Updated to handle cascading: when admin0 changes, admin1 and admin2 reset; when admin1 changes, admin2 resets
_adminUpdate = {
  // Check exposure section selectors first (has priority)
  if (selectAdmin0?.value !== sharedAdmin0) {
    // Admin0 changed - reset admin1 and admin2
    updateSharedAdminSelections(selectAdmin0?.value || null, null, null);
  } else if (selectAdmin1?.value !== sharedAdmin1) {
    // Admin1 changed - reset admin2
    updateSharedAdminSelections(sharedAdmin0, selectAdmin1?.value || null, null);
  } else if (selectAdmin2?.value !== sharedAdmin2) {
    // Admin2 changed
    updateSharedAdminSelections(sharedAdmin0, sharedAdmin1, selectAdmin2?.value || null);
  }
  // Check sensitivity section selectors
  else if (sensitivityAdmin0?.value !== sharedAdmin0) {
    updateSharedAdminSelections(sensitivityAdmin0?.value || null, null, null);
  } else if (sensitivityAdmin1?.value !== sharedAdmin1) {
    updateSharedAdminSelections(sharedAdmin0, sensitivityAdmin1?.value || null, null);
  } else if (sensitivityAdmin2?.value !== sharedAdmin2) {
    updateSharedAdminSelections(sharedAdmin0, sharedAdmin1, sensitivityAdmin2?.value || null);
  }
  // Check adaptive capacity section selectors
  else if (adaptiveCapacityAdmin0?.value !== sharedAdmin0) {
    updateSharedAdminSelections(adaptiveCapacityAdmin0?.value || null, null, null);
  } else if (adaptiveCapacityAdmin1?.value !== sharedAdmin1) {
    updateSharedAdminSelections(sharedAdmin0, adaptiveCapacityAdmin1?.value || null, null);
  } else if (adaptiveCapacityAdmin2?.value !== sharedAdmin2) {
    updateSharedAdminSelections(sharedAdmin0, sharedAdmin1, adaptiveCapacityAdmin2?.value || null);
  }
  // Check composite section selectors
  else if (compositeAdmin0?.value !== sharedAdmin0) {
    updateSharedAdminSelections(compositeAdmin0?.value || null, null, null);
  } else if (compositeAdmin1?.value !== sharedAdmin1) {
    updateSharedAdminSelections(sharedAdmin0, compositeAdmin1?.value || null, null);
  } else if (compositeAdmin2?.value !== sharedAdmin2) {
    updateSharedAdminSelections(sharedAdmin0, sharedAdmin1, compositeAdmin2?.value || null);
  }
}
sensitivityDataAdmin0 = {
  const data = boundaries.admin0.features.map(d => d.properties)
  // add a blank value and sort alphabetically
  return [null, ...data.map(d => d.admin0_name)].map(d => {
    return {label: d == null ? globalSelection.label : d, value: d || "SSA"}
  }).sort((a, b) => {
    // Keep null/blank value at the top
    if (a.value === "SSA") return -1;
    if (b.value === "SSA") return 1;
    // Sort others alphabetically by label
    return a.label.localeCompare(b.label);
  })
}


sensitivityDataAdmin1 = {
  // admin 1, filter by 0 
  const data = boundaries.admin1.features.map(d => d.properties)
  .filter(d => d.admin0_name == sensitivityAdmin0?.value)
  // add blank value and sort alphabetically
  return [null, ...data.map(d => d.admin1_name)].map(d => {
    return {label: d, value: d}
  }).sort((a, b) => {
    // Keep null/blank value at the top
    if (a.value === null) return -1;
    if (b.value === null) return 1;
    // Sort others alphabetically by label
    return a.label.localeCompare(b.label);
  })
}


sensitivityDataAdmin2 = {
  const data = boundaries.admin2.features.map(d => d.properties)
  .filter(d => d.admin0_name == sensitivityAdmin0?.value && d.admin1_name == sensitivityAdmin1?.value)
  // add blank value and sort alphabetically
  return [null, ...data.map(d => d.admin2_name)].map(d => {
    return {label: d, value: d}
  }).sort((a, b) => {
    // Keep null/blank value at the top
    if (a.value === null) return -1;
    if (b.value === null) return 1;
    // Sort others alphabetically by label
    return a.label.localeCompare(b.label);
  })
}
compositeDataAdmin0 = {
  const data = boundaries.admin0.features.map(d => d.properties)
  // add a blank value and sort alphabetically
  return [null, ...data.map(d => d.admin0_name)].map(d => {
    return {label: d == null ? globalSelection.label : d, value: d || "SSA"}
  }).sort((a, b) => {
    // Keep null/blank value at the top
    if (a.value === "SSA") return -1;
    if (b.value === "SSA") return 1;
    // Sort others alphabetically by label
    return a.label.localeCompare(b.label);
  })
}


compositeDataAdmin1 = {
  // admin 1, filter by 0 
  const data = boundaries.admin1.features.map(d => d.properties)
  // Note: compositeAdmin0 will be available when this function is called from composite section
  // add blank value and sort alphabetically
  return [null, ...data.map(d => d.admin1_name)].map(d => {
    return {label: d, value: d}
  }).sort((a, b) => {
    // Keep null/blank value at the top
    if (a.value === null) return -1;
    if (b.value === null) return 1;
    // Sort others alphabetically by label
    return a.label.localeCompare(b.label);
  })
}


compositeDataAdmin2 = {
  const data = boundaries.admin2.features.map(d => d.properties)
  // Note: compositeAdmin0 and compositeAdmin1 will be available when this function is called from composite section
  // add blank value and sort alphabetically
  return [null, ...data.map(d => d.admin2_name)].map(d => {
    return {label: d, value: d}
  }).sort((a, b) => {
    // Keep null/blank value at the top
    if (a.value === null) return -1;
    if (b.value === null) return 1;
    // Sort others alphabetically by label
    return a.label.localeCompare(b.label);
  })
}
adaptiveCapacityDataAdmin0 = {
  const data = boundaries.admin0.features.map(d => d.properties)
  // add a blank value and sort alphabetically
  return [null, ...data.map(d => d.admin0_name)].map(d => {
   return {label: d == null ? globalSelection.label : d, value: d || "SSA"}
  }).sort((a, b) => {
    // Keep null/blank value at the top
    if (a.value === "SSA") return -1;
    if (b.value === "SSA") return 1;
    // Sort others alphabetically by label
    return a.label.localeCompare(b.label);
  })
}


adaptiveCapacityDataAdmin1 = {
  // admin 1, filter by 0 
  const data = boundaries.admin1.features.map(d => d.properties)
  .filter(d => d.admin0_name == adaptiveCapacityAdmin0?.value)
  // add blank value and sort alphabetically
  return [null, ...data.map(d => d.admin1_name)].map(d => {
    return {label: d, value: d}
  }).sort((a, b) => {
    // Keep null/blank value at the top
    if (a.value === null) return -1;
    if (b.value === null) return 1;
    // Sort others alphabetically by label
    return a.label.localeCompare(b.label);
  })
}


adaptiveCapacityDataAdmin2 = {
  const data = boundaries.admin2.features.map(d => d.properties)
  .filter(d => d.admin0_name == adaptiveCapacityAdmin0?.value && d.admin1_name == adaptiveCapacityAdmin1?.value)
  // add blank value and sort alphabetically
  return [null, ...data.map(d => d.admin2_name)].map(d => {
    return {label: d, value: d}
  }).sort((a, b) => {
    // Keep null/blank value at the top
    if (a.value === null) return -1;
    if (b.value === null) return 1;
    // Sort others alphabetically by label
    return a.label.localeCompare(b.label);
  })
}
function bindTabularToGeo({
  data = [],
  dataBindColumn = "dataBindColumn",
  geoData = [],
  geoDataBindColumn = "geoDataBindColumn",
}) {
  const index = new Map(data.map((d) => [d[dataBindColumn], d]));
  const geojson = JSON.parse(JSON.stringify(geoData));

  for (const f of geojson.features) {
    f.properties.data = index.get(f.properties[geoDataBindColumn]);
  }
  return geojson;
}

/**
 * Get the most granular admin selection from admin level selectors
 *
 * @param {Object} admin0 - Admin0 selector object with .value property
 * @param {Object} admin1 - Admin1 selector object with .value property
 * @param {Object} admin2 - Admin2 selector object with .value property
 * @param {string} globalLabel - Default label when no selection is made (default: "Sub-Saharan Africa")
 * @returns {string} The most specific admin level selected, or globalLabel if none selected
 *
 * @example
 * getAdminSelection(selectAdmin0, selectAdmin1, selectAdmin2)
 */
function getAdminSelection(
  admin0,
  admin1,
  admin2,
  globalLabel = "Sub-Saharan Africa",
) {
  const a0 = admin0?.value;
  const a1 = admin1?.value;
  const a2 = admin2?.value;

  return a2 ? a2 : a1 ? a1 : a0 ? a0 : globalLabel;
}

// Convenience wrappers for backward compatibility
function getExposureAdminSelection() {
  return getAdminSelection(selectAdmin0, selectAdmin1, selectAdmin2);
}

function getSensitivityAdminSelection() {
  return getAdminSelection(
    sensitivityAdmin0,
    sensitivityAdmin1,
    sensitivityAdmin2,
  );
}
// Formatting utilities
/**
 * Format number as compact currency (e.g., "$1.2M")
 */
formatNumCompactShort = new Intl.NumberFormat("en-US", {
  notation: "compact",
  compactDisplay: "short",
  style: "currency",
  currency: "USD",
}).format;
// Color scales and themes
/**
 * Color scale definitions for visualizations
 */
colorScales = {
  return {
    range: {
      green: ['#E4F5D0', '#015023'],
      blue: ['#E8F2FF', '#003E6B'],
      brown: ['#FFFDE5', '#A87B00'],
      yellowGreen: ['#216729', '#F7D732'], // Reversed: green (low/good) to yellow (high/bad)
      orangeRed: ['#F4BB21', '#EC5A47'],
      redOrange: ['#FEE0D2', '#CB4335'],
      vulnerability: ['#2E8B57', '#FFD700', '#FF6347'], // Low, Medium, High
      adaptive: ['#FF6B6B', '#4ECDC4', '#45B7D1'] // Poor, Fair, Good
    },
    unknown: "#f0f0f0",
    noData: "#f0f0f0"
  }
}
intDollarUnit = `${intDollarYear} USD`
intDollarYear = 2015

/**
 * Value of Production (VoP) note configuration
 */
vopNote = {
  return {
    caption: _lang(vulnerability_translations.vopNoteCaption),
    blurb: _lang(vulnerability_translations.vopNoteBlurb)
  }
}
function createLoadingState(message = null) {
  return htl.html`
    <div style="
      display: flex;
      align-items: center;
      justify-content: center;
      padding: 40px;
      color: #6b7280;
      font-style: italic;
    ">
      <div style="margin-right: 12px;">
        <div class="spinner" style="
          width: 20px;
          height: 20px;
          border: 2px solid #e5e7eb;
          border-top: 2px solid #3b82f6;
          border-radius: 50%;
          animation: spin 1s linear infinite;
        "></div>
      </div>
      ${message || _lang(vulnerability_translations.loading)}
    </div>
    <style>
      @keyframes spin {
        0% { transform: rotate(0deg); }
        100% { transform: rotate(360deg); }
      }
    </style>
  `;
}

/**
 * Create a "no data available" state UI component
 *
 * @param {string|null} message - Optional message (defaults to translation)
 * @returns {HTMLElement} HTML element with no-data display
 */
function createNoDataState(message = null) {
  return htl.html`
    <div style="
      display: flex;
      align-items: center;
      justify-content: center;
      padding: 40px;
      background: #f9fafb;
      border: 2px dashed #d1d5db;
      border-radius: 8px;
      color: #6b7280;
      text-align: center;
      margin-top: 16px;
    ">
      <div>
        <div style="margin-bottom: 8px; font-size: 18px;">📊</div>
        <div>${message || _lang(vulnerability_translations.no_data_available)}</div>
      </div>
    </div>
  `;
}
function generateInsight(template, replacements) {
  return Lang.reduceReplaceTemplateItems(template, replacements);
}

/**
 * Create a styled insight display component from text or HTML
 * Handles both plain text (with paragraph splitting) and HTML elements
 *
 * @param {string|HTMLElement} insight - Insight text or HTML element to display
 * @returns {HTMLElement} Styled HTML element with insight content
 */
function createInsightDisplay(insight) {
  // Handle case where insight is already an HTML element (from htl.html)
  if (typeof insight !== "string") {
    // If it's already an HTML element, just wrap it in the standard display
    return htl.html`
      <div style="
        background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
        color: white;
        padding: 20px;
        border-radius: 10px;
        margin: 16px 0;
        box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
      ">
        <h4 style="margin: 0 0 12px 0; font-size: 16px; font-weight: 600;">
          ${_lang(vulnerability_translations.quick_insights)}
        </h4>
        <div style="margin: 0; font-size: 18px; line-height: 1.5; opacity: 0.95;">
          ${insight}
        </div>
      </div>
    `;
  }

  // Split insight into paragraphs based on double newlines and process each
  const paragraphs = insight.split("\n\n").filter((p) => p.trim().length > 0);

  // Create HTML elements using htl.html for proper rendering
  const paragraphElements = paragraphs
    .map((paragraph) => {
      // Handle special formatting for section headers
      if (paragraph.startsWith("📍 Selected Group:")) {
        const content = paragraph.replace("📍 Selected Group:", "").trim();
        return htl.html`<div style="
        background: rgba(255, 255, 255, 0.15);
        padding: 12px;
        border-radius: 8px;
        margin-bottom: 16px;
        border-left: 4px solid #ffd700;
      ">
        <div style="font-weight: bold; margin-bottom: 8px;">📍 Selected Group</div>
        <div style="font-size: 16px;">${content}</div>
      </div>`;
      } else if (paragraph.startsWith("Overall Population Analysis:")) {
        const content = paragraph
          .replace("Overall Population Analysis:", "")
          .trim();
        return htl.html`<div style="
        border-top: 1px solid rgba(255, 255, 255, 0.3);
        padding-top: 16px;
        margin-bottom: 16px;
      ">
        <div style="font-weight: bold; margin-bottom: 12px;">Overall Population Analysis</div>
        <div style="font-size: 16px; line-height: 1.6;">${content}</div>
      </div>`;
      } else if (paragraph === "---") {
        // Skip separator lines
        return null;
      } else {
        // Regular paragraphs
        return htl.html`<p style="margin: 0 0 12px 0; font-size: 16px; line-height: 1.6; opacity: 0.95;">${paragraph}</p>`;
      }
    })
    .filter((p) => p !== null);

  return htl.html`
    <div style="
      background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
      color: white;
      padding: 20px;
      border-radius: 10px;
      margin: 16px 0;
      box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
    ">
      <h4 style="margin: 0 0 12px 0; font-size: 16px; font-weight: 600;">
        ${_lang(vulnerability_translations.quick_insights)}
      </h4>
      <div style="margin: 0;">
        ${paragraphElements}
      </div>
    </div>
  `;
}
overview = _lang(vulnerability_translations.notebook_overview);

compositeQuestion = _lang(vulnerability_translations.composite_question);

viewof compositeAdmin0 = Inputs.select(dataAdmin0, {
  label: adminRegions.labels.admin0, 
  format: x => x.label,
  value: dataAdmin0.find(d => d.value === sharedAdmin0) || (dataAdmin0.length > 0 ? dataAdmin0[0] : null)
})
viewof compositeAdmin1 = Inputs.select(dataAdmin1, {
  label: adminRegions.labels.admin1, 
  format: x => x.label,
  value: dataAdmin1.find(d => d.value === sharedAdmin1) || (dataAdmin1.length > 0 ? dataAdmin1[0] : null)
})  
viewof compositeAdmin2 = Inputs.select(dataAdmin2, {
  label: adminRegions.labels.admin2, 
  format: x => x.label,
  value: dataAdmin2.find(d => d.value === sharedAdmin2) || (dataAdmin2.length > 0 ? dataAdmin2[0] : null)
})
// Controls form with exact same HTML structure
compositeControlsForm = htl.html`
<div style="
  background: #f8fafc;
  border: 2px solid #e2e8f0;
  border-radius: 12px;
  padding: 24px;
  box-shadow: 0 4px 6px rgba(0, 0, 0, 0.07);
  margin: 16px 0;
">
  <h3 style="margin: 0 0 20px 0; color: #2d3748; font-size: 1.1rem; font-weight: 600;">
    ${_lang(vulnerability_translations.geographic_selection)}
  </h3>
  <div style="
    display: flex;
    align-items: flex-start;
    gap: 20px;
    flex-wrap: wrap;
    justify-content: space-between;
  " class="form-inputs-container">
    <div style="flex: 1; min-width: 180px; max-width: 100%;">${viewof compositeAdmin0}</div>
    <div style="flex: 1; min-width: 180px; max-width: 100%;">${viewof compositeAdmin1}</div>
    <div style="flex: 1; min-width: 180px; max-width: 100%;">${viewof compositeAdmin2}</div>
  </div>
</div>
`
// Keep metadata as JSON (small file, 4KB)
sensitivityMetadata = await loadDataWithCache(
  "compositeUrgencyMetadata",
  async () => {
    return await FileAttachment(
      "/data/vulnerability_notebook/urgency_metadata.json",
    ).json();
  },
);
// Reuse cached boundary data (already loaded in shared)
compositeMapBoundaries = boundaries;
// Get current admin selections using shared state
compositeAdminSelections = {
  return {
    selectAdmin0: compositeAdmin0?.value || sharedAdmin0 || null,
    selectAdmin1: compositeAdmin1?.value || sharedAdmin1 || null,
    selectAdmin2: compositeAdmin2?.value || sharedAdmin2 || null
  }
}
// Database connection for index calculations
index_db = DuckDBClient.of({
  exposure: FileAttachment(
    "/data/vulnerability_notebook/notebook_exposure.parquet",
  ),
  urgency: FileAttachment("/data/vulnerability_notebook/urgency_index.parquet"),
  enabling: FileAttachment(
    "/data/vulnerability_notebook/enabling_vars.parquet",
  ), // AKA adaptive capacity
});
function computeIndex(row) {
  const normValues = Object.entries(row)
    .filter(
      ([key, value]) =>
        key.endsWith("_norm") && typeof value === "number" && !isNaN(value),
    )
    .map(([, value]) => value);

  if (normValues.length === 0) return null;

  const sum = normValues.reduce((a, b) => a + b, 0);
  return sum / normValues.length;
}

/**
 * Generate SQL WHERE clause for admin level filtering
 * Returns clause to filter by admin0 (country level) or admin1 (region level) based on selection
 *
 * @param {string|null} country_sel - Selected country code (or "SSA" for all countries)
 * @param {string|null} admin1_sel - Selected admin1 region (currently unused but kept for consistency)
 * @returns {string} SQL WHERE clause fragment
 */
whereAdminNull = (country_sel, admin1_sel) => {
  // If all countries selected (SSA) or no selection, return country-level data (admin1_name IS NULL)
  // Otherwise, return region-level data (admin1_name IS NOT NULL)
  if (country_sel == "SSA" || !country_sel) {
    return `AND admin1_name IS NULL`;
  } else {
    return `AND admin1_name IS NOT NULL`;
  }
};
// Calculate exposure index
exposure_index = {
  // Check if index_db is available before querying
  if (!index_db) {
    console.error("index_db is not available");
    return [];
  }
  
  try {
    return await index_db.query(`
      WITH raw_exposure AS (
          SELECT 
            admin0_name, 
            admin1_name, 
            SUM(value) AS total_exposure
          FROM exposure
          WHERE admin2_name IS NULL
            ${whereAdminNull(compositeAdmin0.value, compositeAdmin1.value)}
          GROUP BY admin0_name, admin1_name
        ),
        stats AS (
          SELECT
            MIN(total_exposure) AS exposure_min,
            MAX(total_exposure) AS exposure_max
          FROM raw_exposure
          WHERE ISFINITE(total_exposure)
        )
        SELECT 
          r.admin0_name,
          r.admin1_name,
          r.total_exposure,
          -- Min-max normalize exposure
          (r.total_exposure - s.exposure_min) / NULLIF(s.exposure_max - s.exposure_min, 0) AS exposure_norm
        FROM raw_exposure r
        CROSS JOIN stats s
        WHERE ISFINITE(r.total_exposure)
        ORDER BY r.total_exposure DESC;
    `);
  } catch (error) {
    console.error("Failed to calculate exposure index:", error);
    return [];
  }
}
// Calculate enabling index (adaptive capacity)
enabling_index = {
  // Check if index_db is available before querying
  if (!index_db) {
    console.error("index_db is not available");
    return [];
  }
  
  try {
    let resp = await index_db.query(`
SELECT *
FROM enabling
WHERE admin0_name != 'SSA'
${whereAdminNull(compositeAdmin0.value, compositeAdmin1.value)}
`);

    return resp.map((row) => ({
      ...row,
      enabling_index: computeIndex(row)
    }));
  } catch (error) {
    console.error("Failed to calculate enabling index:", error);
    return [];
  }
}
// Calculate urgency index
// Index is based on exposure and sensitivity
urgency_index = {
  // Check if index_db is available before querying
  if (!index_db) {
    console.error("index_db is not available");
    return [];
  }
  
  try {
    let gender = "total"; //NOTE: Keeping this as an option in case future updates want gender specific index
    // sensitivity data
    let sensitivityData = await index_db.query(`
SELECT *
FROM urgency
WHERE admin0_name != 'SSA'
AND gender = '${gender}'
${whereAdminNull(compositeAdmin0.value, compositeAdmin1.value)}
`);

    // Check if exposure_index exists and is an array before using
    const exposureMap = (exposure_index && Array.isArray(exposure_index))
      ? Object.fromEntries(
          exposure_index.map((d) => {
            const a0 = d.admin0_name;
            const a1 = d.admin1_name;
            const key = a1 ? `${a0}||${a1}` : a0; // use both if admin1 exists, else just admin0
            return [key, d.exposure_norm];
          })
        )
      : {};

    // merge into urgencyData
    const merged = sensitivityData.map((d) => {
      const a0 = d.admin0_name;
      const a1 = d.admin1_name;
      const key = a1 ? `${a0}||${a1}` : a0;

      return {
        ...d,
        exposure_norm: exposureMap[key] ?? null
      };
    });

    return merged.map((row) => ({
      ...row,
      urgency_index: computeIndex(row)
    }));
  } catch (error) {
    console.error("Failed to calculate urgency index:", error);
    return [];
  }
}
// Calculate vulnerability index (AKA composite index with ND-gain Methodology)
vulnerabilityIndex = {
  const key = (r) => `${r.admin0_name}|${r.admin1_name}`;
  const toMap = (arr, k, v) => Object.fromEntries(arr.map((r) => [k(r), v(r)]));

  const uMap = toMap(urgency_index, key, (r) => r.urgency_index);
  const enMap = toMap(enabling_index, key, (r) => r.enabling_index);

  const allKeys = new Set([...Object.keys(enMap), ...Object.keys(uMap)]);

  const merged = [...allKeys].map((k) => {
    const [admin0_name, admin1_name] = k.split("|");
    const en = enMap[k],
      u = uMap[k];
    return {
      admin0_name,
      admin1_name,
      enabling_index: en ?? null,
      urgency_index: u ?? null,
      vulnerability:
        typeof en === "number" && typeof u === "number"
          ? 100 - (en - u + 1) * 50
          : null
    };
  });

  return merged.filter(d => d.vulnerability !== null);
}
// Updated vulnerability index calculation using the Observable approach
componentIndicesPerRegion = {
  // Check if vulnerabilityIndex exists and is valid before processing
  if (!vulnerabilityIndex || !Array.isArray(vulnerabilityIndex) || vulnerabilityIndex.length === 0) {
    return { scores: {}, adminLevel: null, regions: [] };
  }
  
  const selections = compositeAdminSelections;
  const isNull = (val) => val === null || val === undefined || val === "" || val === "\\N";
  
  // Determine admin level and get unique regions
  let adminLevel, regions;
  if (!selections.selectAdmin0 || selections.selectAdmin0 === "SSA") {
    // Show all countries (admin0 level)
    adminLevel = "admin0_name";
    regions = [...new Set(vulnerabilityIndex.map(d => d.admin0_name).filter(d => d))];
  } else if (!selections.selectAdmin1) {
    // Show admin1 within selected country
    adminLevel = "admin1_name";
    regions = [...new Set(vulnerabilityIndex
      .filter(d => d.admin0_name === selections.selectAdmin0 && !isNull(d.admin1_name))
      .map(d => d.admin1_name))];
  } else if (!selections.selectAdmin2) {
    // Show admin2 within selected admin1
    adminLevel = "admin2_name";
    regions = [...new Set(vulnerabilityIndex
      .filter(d => d.admin0_name === selections.selectAdmin0 && 
                   d.admin1_name === selections.selectAdmin1 && 
                   !isNull(d.admin2_name))
      .map(d => d.admin2_name))];
  } else {
    // Single admin2 selected
    adminLevel = "admin2_name";
    regions = [selections.selectAdmin2];
  }

  // Calculate scores for each region using the new vulnerability index
  const regionScores = {};

  regions.forEach(region => {
    // Find vulnerability data for this region
    const vulnData = vulnerabilityIndex.find(d => d[adminLevel] === region);
    
    if (vulnData) {
      // Convert vulnerability score (0-100) to 0-1 scale for consistency
      const compositeScore = vulnData.vulnerability ? vulnData.vulnerability / 100 : 0;
      const enablingScore = vulnData.enabling_index || 0;
      const urgencyScore = vulnData.urgency_index || 0;
      
      // Urgency index represents sensitivity (population density, education, poverty)
      // Exposure is separate and comes from exposure data
      const sensitivityScore = urgencyScore; // Urgency index IS sensitivity
      
      // Get exposure score from exposure data (with null check)
      const exposureData = (exposure_index && Array.isArray(exposure_index))
        ? exposure_index.find(d => d[adminLevel] === region)
        : null;
      const exposureScore = exposureData ? exposureData.exposure_norm : 0;
      
      // FIXED: Adaptive capacity should be HIGH when enabling is HIGH (not inverted)
      // Countries with high enabling factors have HIGH adaptive capacity (good thing)
      const adaptiveScore = enablingScore; // High enabling = high adaptive capacity
      
      regionScores[region] = {
        exposure: exposureScore,
        sensitivity: sensitivityScore,
        adaptive_capacity: adaptiveScore,
        composite: compositeScore,
        // Include raw values for reference
        enabling_index: enablingScore,
        urgency_index: urgencyScore,
        vulnerability_score: vulnData.vulnerability
      };
    } else {
      // No data available for this region
      regionScores[region] = { // All null as a default of 0 could be interpreted as a score rather than missing
        exposure: null,
        sensitivity: null,
        adaptive_capacity: null,
        composite: null,
        enabling_index: null,
        urgency_index: null,
        vulnerability_score: null
      };
    }
  });

  return { 
    scores: regionScores, 
    adminLevel: adminLevel,
    regions: regions
  };
}
// Small Multiples/Faceted Maps Visualization with proper loading/no-data states
compositeVulnerabilityMaps = {
  // Show loading spinner only if data is still loading (null/undefined)
  if ([exposure_index, enabling_index, urgency_index, sensitivityMetadata].some((d) => d === null || d === undefined)) {
    return createLoadingState(
      _lang({en: "Loading composite vulnerability data...", fr: "Chargement des données de vulnérabilité composite..."})
    );
  }

  // // If data is loaded but empty or processing failed, show no data state
  if (
    [exposure_index, enabling_index, urgency_index, sensitivityMetadata, componentIndicesPerRegion]
      .some((d) => !d) || !componentIndicesPerRegion?.scores
  ) {
    return createNoDataState(_lang(vulnerability_translations.no_data_available));
  }

  const { scores, adminLevel } = componentIndicesPerRegion;
  const selections = compositeAdminSelections;
  
  // Get the appropriate boundary level
  let boundaries;
  if (adminLevel === "admin0_name") {
    boundaries = compositeMapBoundaries.admin0;
  } else if (adminLevel === "admin1_name") {
    boundaries = {
      ...compositeMapBoundaries.admin1,
      features: compositeMapBoundaries.admin1.features.filter(
        d => d.properties.admin0_name === selections.selectAdmin0
      )
    };
  } else {
    boundaries = {
      ...compositeMapBoundaries.admin2,
      features: compositeMapBoundaries.admin2.features.filter(
        d => d.properties.admin0_name === selections.selectAdmin0 && 
            d.properties.admin1_name === selections.selectAdmin1
      )
    };
  }

  // Define the components to show (always all components)
  const components = [
    { key: "composite", label: _lang({en: "Composite", fr: "Composite"}), scheme: "Reds", isComposite: true },
    { key: "exposure", label: _lang({en: "Exposure", fr: "Exposition"}), scheme: "Oranges", isComposite: false },
    { key: "sensitivity", label: _lang({en: "Sensitivity", fr: "Sensibilité"}), scheme: "Purples", isComposite: false },
    { key: "adaptive_capacity", label: _lang({en: "Adaptive Capacity", fr: "Capacité d'Adaptation"}), scheme: "Greens", isComposite: false }
  ];

  // Calculate subplot dimensions - use full width when available
  const totalWidth = typeof width === "number" ? Math.min(1200, width) : 1000;
  const totalHeight = 600;
  const layoutPadding = 8;
  const componentMapGap = 12;
  const legendAllowance = 40;
  const compositeColumnRatio = 2.2;
  const componentColumnRatio = 1;
  const columnTotal = compositeColumnRatio + componentColumnRatio;
  const compositeMapWidth = Math.floor(totalWidth * (compositeColumnRatio / columnTotal));
  const componentMapWidth = Math.floor(totalWidth * (componentColumnRatio / columnTotal));
  const componentMapHeight = Math.floor((totalHeight - (componentMapGap * 2)) / 3);
  const compositeMapHeight = (componentMapHeight * 3) + (componentMapGap * 2) + (legendAllowance * 2);
  
  const componentDataByKey = new Map();

  const buildComponentFeatures = (component) => {
    return boundaries.features.map(feature => {
      // Get the correct region name based on admin level
      let regionName;
      if (adminLevel === "admin0_name") {
        regionName = feature.properties.admin0_name;
      } else if (adminLevel === "admin1_name") {
        regionName = feature.properties.admin1_name;
      } else if (adminLevel === "admin2_name") {
        regionName = feature.properties.admin2_name;
      } else {
        regionName = feature.properties[adminLevel];
      }

      const regionScores = scores[regionName] || { 
        exposure: 0, sensitivity: 0, adaptive_capacity: 0, composite: 0 
      };

      return {
        ...feature,
        properties: {
          ...feature.properties,
          vulnerability: regionScores[component.key] || 0,
          regionName: regionName,
          // Add individual scores as properties for easier tooltip access
          exposureScore: regionScores.exposure || 0,
          sensitivityScore: regionScores.sensitivity || 0,
          adaptiveCapacityScore: regionScores.adaptive_capacity || 0,
          compositeScore: regionScores.composite || 0
        }
      };
    });
  };

  components.forEach(component => {
    componentDataByKey.set(component.key, {
      component,
      features: buildComponentFeatures(component)
    });
  });

  const createMapFor = (componentKey, mapWidth, mapHeight) => {
    const entry = componentDataByKey.get(componentKey);
    if (!entry) {
      return createNoDataState(_lang(vulnerability_translations.no_data_available));
    }

    const { component, features: componentFeatures } = entry;

    // Color scheme based on component
    let colorRange;
    if (component.key === "adaptive_capacity") {
      colorRange = ["#F7D732", "#216729"]; // Red (low/bad) to Green (high/good) - high adaptive capacity is good
    } else {
      colorRange = ["#F4BB21", "#EC5A47"]; // Yellow (low) to Red (high) for other components
    }

    return renderGeoMap({
      features: componentFeatures,
      width: mapWidth,
      height: mapHeight,
      valueAccessor: d => d.properties.vulnerability,
      color: {
        type: "linear",
        range: colorRange,
        legend: true,
        label: component.key === "adaptive_capacity" ? 
          _lang({en: "Adaptive Capacity (Higher = Better)", fr: "Capacité d'Adaptation"}) : 
          component.key === "exposure" ? 
            _lang({en: "Climate Exposure (Higher = Worse)", fr: "Exposition Climatique"}) :
          component.key === "sensitivity" ? 
            _lang({en: "Population Sensitivity (Higher = Worse)", fr: "Sensibilité de la Population"}) :
            _lang({en: "Composite Vulnerability (Higher = Worse)", fr: "Vulnérabilité Composite"})
      },
      tooltip: {
        channels: {
          country: {
            label: _lang({en: "Country", fr: "Pays"}),
            value: (d) => d.properties.admin0_name || "Unknown"
          },
          region: {
            label: _lang({en: "Region", fr: "Région"}),
            value: (d) => {
              if (d.properties.admin2_name) {
                return `${d.properties.admin2_name}, ${d.properties.admin1_name}`;
              } else if (d.properties.admin1_name) {
                return d.properties.admin1_name;
              } else {
                return d.properties.admin0_name;
              }
            }
          },
          component: {
            label: _lang({en: "Component", fr: "Composant"}),
            value: component.label
          },
          score: {
            label: _lang({en: "Score", fr: "Score"}),
            value: (d) => d.properties.vulnerability !== null && d.properties.vulnerability !== undefined ? d.properties.vulnerability.toFixed(3) : "No data"
          },
          ...(component.key === "composite" ? {
            exposure: {
              label: _lang({en: "Exposure", fr: "Exposition"}),
              value: (d) => d.properties.exposureScore !== null && d.properties.exposureScore !== undefined ? d.properties.exposureScore.toFixed(3) : "No data"
            },
            sensitivity: {
              label: _lang({en: "Sensitivity", fr: "Sensibilité"}),
              value: (d) => d.properties.sensitivityScore !== null && d.properties.sensitivityScore !== undefined ? d.properties.sensitivityScore.toFixed(3) : "No data"
            },
            adaptiveCapacity: {
              label: _lang({en: "Adaptive Capacity", fr: "Capacité d'Adaptation"}),
              value: (d) => d.properties.adaptiveCapacityScore !== null && d.properties.adaptiveCapacityScore !== undefined ? d.properties.adaptiveCapacityScore.toFixed(3) : "No data"
            }
          } : {})
        }
      }
    });
  };

  const createMapContainer = (componentKey, mapWidth, mapHeight, isFeatured) => {
    const map = createMapFor(componentKey, mapWidth, mapHeight);
    const mapContainer = document.createElement("div");
    mapContainer.className = isFeatured ? "map featured" : "map";
    mapContainer.dataset.componentKey = componentKey;
    mapContainer.style.display = "flex";
    mapContainer.style.flexDirection = "column";
    mapContainer.style.alignItems = "stretch";
    mapContainer.style.width = "100%";

    const mapWrapper = document.createElement("div");
    mapWrapper.style.overflow = "visible";
    mapWrapper.append(map);

    const mapSvg = map.querySelector ? map.querySelector('svg:nth-of-type(2)') : null; // Map is the second svg of the figure. Might change
    if (mapSvg) mapSvg.setAttribute("overflow", "visible");

    mapContainer.append(mapWrapper);
    return mapContainer;
  };

  // Individual legends are now integrated with each map

  const compositeMapContainer = createMapContainer(
    "composite",
    compositeMapWidth,
    compositeMapHeight,
    true
  );
  const componentMapContainers = components
    .filter(component => !component.isComposite)
    .map(component => createMapContainer(component.key, componentMapWidth, componentMapHeight, false));

  // Return maps in a custom layout with labels and external legend
  const container = htl.html`
    <div style="
      display: flex;
      flex-direction: column;
      align-items: stretch;
      gap: 20px;
      width: 100%;
      max-width: 100%;
      padding: ${layoutPadding}px;
      background: white;
      border-radius: 8px;
    ">
      <!-- Individual legends are now integrated with each map -->
      
      <!-- Two-column layout: composite map left, components stacked right -->
      <div style="
        display: grid;
        grid-template-columns: minmax(0, ${compositeColumnRatio}fr) minmax(0, ${componentColumnRatio}fr);
        gap: ${componentMapGap}px;
        width: 100%;
        overflow: visible;
      ">
        <div style="
          display: flex;
          flex-direction: column;
          align-items: stretch;
          width: 100%;
        " data-role="featured">
          ${compositeMapContainer}
        </div>

        <div style="
          display: flex;
          flex-direction: column;
          align-items: stretch;
          gap: ${componentMapGap}px;
          width: 100%;
        " data-role="secondary">
          ${componentMapContainers.map((mapContainer) => mapContainer)}
        </div>
      </div>
    </div>
  `;

  // Enable double-click swapping between featured and secondary columns
  setTimeout(() => {
    const featuredColumn = container.querySelector('[data-role="featured"]');
    const secondaryColumn = container.querySelector('[data-role="secondary"]');
    if (!featuredColumn || !secondaryColumn) return;

    const bindMapEvents = () => {
      const maps = container.querySelectorAll('.map');
      maps.forEach((map) => {
        if (map.dataset.bound === "true") return;
        map.dataset.bound = "true";

        map.addEventListener('dblclick', () => {
          if (map.classList.contains('featured')) return;

          const currentFeatured = container.querySelector('.map.featured');
          if (!currentFeatured) return;

          const featuredKey = currentFeatured.dataset.componentKey;
          const clickedKey = map.dataset.componentKey;
          if (!featuredKey || !clickedKey) return;

          const newFeatured = createMapContainer(
            clickedKey,
            compositeMapWidth,
            compositeMapHeight,
            true
          );
          const newSecondary = createMapContainer(
            featuredKey,
            componentMapWidth,
            componentMapHeight,
            false
          );

          featuredColumn.innerHTML = "";
          featuredColumn.append(newFeatured);
          secondaryColumn.replaceChild(newSecondary, map);

          bindMapEvents();
        });
      });
    };

    bindMapEvents();
  }, 0);

  return container;
}
// Dynamic Insights following wireframe template
compositeVulnerabilityInsights = {
  if (!componentIndicesPerRegion || !componentIndicesPerRegion.scores) {
    return createNoDataState();
  }

  const selections = compositeAdminSelections;
  const { scores, adminLevel } = componentIndicesPerRegion;
  
  // Get region name for insight
  let regionName;
  if (!selections.selectAdmin0 || selections.selectAdmin0 === "SSA") {
    regionName = "Sub-Saharan Africa";
  } else if (!selections.selectAdmin1) {
    regionName = selections.selectAdmin0;
  } else if (!selections.selectAdmin2) {
    regionName = selections.selectAdmin1;
  } else {
    regionName = selections.selectAdmin2;
  }
  
  // Calculate average scores across all regions for this level
  const regions = Object.keys(scores);
  const avgScores = {
    composite: d3.mean(regions, r => scores[r].composite) || 0,
    exposure: d3.mean(regions, r => scores[r].exposure) || 0,
    sensitivity: d3.mean(regions, r => scores[r].sensitivity) || 0,
    adaptive_capacity: d3.mean(regions, r => scores[r].adaptive_capacity) || 0
  };
  
  // Determine vulnerability level
  const vulnLevel = avgScores.composite > 0.7 ? 
    _lang({en: "high", fr: "élevé"}) : 
    avgScores.composite > 0.4 ? 
    _lang({en: "moderate", fr: "modéré"}) : 
    _lang({en: "low", fr: "faible"});
  
  // Identify main drivers
  const drivers = [];
  if (avgScores.exposure > 0.6) drivers.push(_lang({en: "high exposure", fr: "exposition élevée"}));
  if (avgScores.sensitivity > 0.6) drivers.push(_lang({en: "high sensitivity", fr: "sensibilité élevée"}));  
  if (avgScores.adaptive_capacity < 0.4) drivers.push(_lang({en: "limited adaptive capacity", fr: "capacité d'adaptation limitée"})); // LOW adaptive capacity is bad
  
  // Recommendations based on highest scoring components
  const recommendations = [];
  if (avgScores.exposure === Math.max(avgScores.exposure, avgScores.sensitivity, avgScores.adaptive_capacity)) {
    recommendations.push(
      _lang({en: "climate hazard preparedness", fr: "préparation aux dangers climatiques"}),
      _lang({en: "resilient infrastructure", fr: "infrastructure résiliente"})
    );
  }
  if (avgScores.sensitivity > 0.5) {
    recommendations.push(
      _lang({en: "education access", fr: "accès à l'éducation"}),
      _lang({en: "poverty reduction programs", fr: "programmes de réduction de la pauvreté"})
    );
  }
  if (avgScores.adaptive_capacity < 0.5) { // LOW adaptive capacity needs improvement
    recommendations.push(
      _lang({en: "connectivity improvements", fr: "améliorations de la connectivité"}),
      _lang({en: "institutional strengthening", fr: "renforcement institutionnel"})
    );
  }

  const driversText = drivers.length > 0 ? 
    drivers.join(_lang({en: " combined with ", fr: " combiné avec "})) : 
    _lang({en: "multiple factors", fr: "facteurs multiples"});
    
  const recommendationsText = recommendations.slice(0, 2).join(_lang({en: " and ", fr: " et "})) || 
    _lang({en: "comprehensive interventions", fr: "interventions complètes"});

  // Create component scores as list items
  const componentScores = [
    htl.html`<li><strong>${_lang({en: "Exposure", fr: "Exposition"})}</strong>: ${(avgScores.exposure || 0).toFixed(2)}</li>`,
    htl.html`<li><strong>${_lang({en: "Sensitivity", fr: "Sensibilité"})}</strong>: ${(avgScores.sensitivity || 0).toFixed(2)}</li>`,
    htl.html`<li><strong>${_lang({en: "Adaptive Capacity", fr: "Capacité d'adaptation"})}</strong>: ${(avgScores.adaptive_capacity || 0).toFixed(2)}</li>`
  ];

  const insight = htl.html`<div>
    <p>${_lang({
      en: `${regionName} has a ${vulnLevel} vulnerability score of ${(avgScores.composite || 0).toFixed(2)}.`,
      fr: `${regionName} a un score de vulnérabilité ${vulnLevel} de ${(avgScores.composite || 0).toFixed(2)}.`
    })}</p>
    
    <ul>${componentScores}</ul>
    
    <p>${_lang({
      en: `${driversText} driving vulnerability. Strengthening ${recommendationsText} could reduce future risks.`,
      fr: `${driversText} génèrent la vulnérabilité. Renforcer ${recommendationsText} pourrait réduire les risques futurs.`
    })}</p>
  </div>`;
  
  return createInsightDisplay(insight);
}
// Create download dataset with all composite index and sub-indices
compositeDownloadData = {
  if (!componentIndicesPerRegion || !componentIndicesPerRegion.scores) return [];
  
  const { scores, adminLevel } = componentIndicesPerRegion;
  const regions = Object.keys(scores);
  
  return regions.map(region => {
    const regionScores = scores[region];
    return {
      [_lang({en: "Region", fr: "Région"})]: region,
      [_lang({en: "Composite Vulnerability Score", fr: "Score de Vulnérabilité Composite"})]: regionScores.composite ? regionScores.composite.toFixed(3) : "N/A",
      [_lang({en: "Exposure Score", fr: "Score d'Exposition"})]: regionScores.exposure ? regionScores.exposure.toFixed(3) : "N/A",
      [_lang({en: "Sensitivity Score", fr: "Score de Sensibilité"})]: regionScores.sensitivity ? regionScores.sensitivity.toFixed(3) : "N/A",
      [_lang({en: "Adaptive Capacity Score", fr: "Score de Capacité d'Adaptation"})]: regionScores.adaptive_capacity ? regionScores.adaptive_capacity.toFixed(3) : "N/A",
      [_lang({en: "Enabling Index", fr: "Indice d'Enabling"})]: regionScores.enabling_index ? regionScores.enabling_index.toFixed(3) : "N/A",
      [_lang({en: "Urgency Index", fr: "Indice d'Urgence"})]: regionScores.urgency_index ? regionScores.urgency_index.toFixed(3) : "N/A",
      [_lang({en: "Raw Vulnerability Score", fr: "Score de Vulnérabilité Brut"})]: regionScores.vulnerability_score || "N/A"
    };
  });
}
// Download button for composite index data
viewof compositeDownloadButton = {
  if (!compositeDownloadData || compositeDownloadData.length === 0) {
    return htl.html`<p>${_lang({en: "No data available for download", fr: "Aucune donnée disponible pour le téléchargement"})}</p>`;
  }
  
  const region = _lang({en: "All_Regions", fr: "Toutes_Les_Régions"});
  
  return downloadButton(
    compositeDownloadData, 
    `composite_vulnerability_data_${region}`,
    _lang({en: "Download Composite Index Data", fr: "Télécharger les Données de l'Indice Composite"})
  );
}

exposureQuestion = _lang(vulnerability_translations.exposure_question);

viewof selectAdmin0 = Inputs.select(dataAdmin0, {
  label: adminRegions.labels.admin0, 
  format: x => x.label,
  value: dataAdmin0.find(d => d.value === sharedAdmin0) || (dataAdmin0.length > 0 ? dataAdmin0[0] : null)
})
viewof selectAdmin1 = Inputs.select(dataAdmin1, {
  label: adminRegions.labels.admin1, 
  format: x => x.label,
  value: dataAdmin1.find(d => d.value === sharedAdmin1) || (dataAdmin1.length > 0 ? dataAdmin1[0] : null)
})
viewof selectAdmin2 = Inputs.select(dataAdmin2, {
  label: adminRegions.labels.admin2, 
  format: x => x.label,
  value: dataAdmin2.find(d => d.value === sharedAdmin2) || (dataAdmin2.length > 0 ? dataAdmin2[0] : null)
})
exposureControlsForm = htl.html`
<div style="
  background: #f8fafc;
  border: 2px solid #e2e8f0;
  border-radius: 12px;
  padding: 24px;
  box-shadow: 0 4px 6px rgba(0, 0, 0, 0.07);
  margin: 16px 0;
">
  <h3 style="margin: 0 0 20px 0; color: #2d3748; font-size: 1.1rem; font-weight: 600;">
    ${_lang(vulnerability_translations.geographic_selection)}
  </h3>
  <div style="
    display: flex;
    align-items: flex-start;
    gap: 20px;
    flex-wrap: wrap;
    justify-content: space-between;
  " class="form-inputs-container">
    <div style="flex: 1; min-width: 180px; max-width: 100%;">${viewof selectAdmin0}</div>
    <div style="flex: 1; min-width: 180px; max-width: 100%;">${viewof selectAdmin1}</div>
    <div style="flex: 1; min-width: 180px; max-width: 100%;">${viewof selectAdmin2}</div>
  </div>
</div>
`
exposureDb = await createExposureDB() // Use cached loading


exposureData = {
  if (!exposureDb) return [];
  
  try {
    const query = `
      SELECT *
      FROM exposure_data
      WHERE hazard = 'any'
    `;
    
    return await exposureDb.query(query);
  } catch (error) {
    return [];
  }
}
// grab tabular data for choropleth
dataGeoImpact = {
  // Check if exposureData exists and is an array before processing
  if (!exposureData || !Array.isArray(exposureData)) {
    return [];
  }
  
  // get data for choropleth map based on choice
  const dataSource = exposureData;

  let filteredData;
  
  // select different data based on admin selections using shared state
  if (sharedAdmin1) {
    filteredData = dataSource.filter((d) => {
      return d.admin0_name == sharedAdmin0
        && d.admin1_name == sharedAdmin1
        && d.admin2_name // always non-null
    });
  } else if (sharedAdmin0) {
    // admin0 is selected, with no admin1
    // get all admin1 data for selected admin0
    filteredData = dataSource.filter((d) => {
      return d.admin0_name == sharedAdmin0
        && d.admin1_name // always non-null 
        && !d.admin2_name // always null
    });
  } else {
    // end case: admin0 is not selected
    // get all admin0 data
    filteredData = dataSource.filter((d) => {
      return !d.admin1_name // always null 
        && !d.admin2_name // always null
    });
  }
  
  // Aggregate data by summing values for each administrative region
  if (filteredData.length === 0) return [];
  
  // Determine grouping key based on admin level
  // Handle null values explicitly to avoid "null" strings in keys
  let groupKey;
  if (sharedAdmin1) {
    // Group by admin2
    groupKey = d => `${d.admin0_name || 'NULL'}|${d.admin1_name || 'NULL'}|${d.admin2_name || 'NULL'}`;
  } else if (sharedAdmin0) {
    // Group by admin1
    groupKey = d => `${d.admin0_name || 'NULL'}|${d.admin1_name || 'NULL'}`;
  } else {
    // Group by admin0
    groupKey = d => d.admin0_name || 'NULL';
  }
  
  const groupedData = d3.group(filteredData, groupKey);
  
  return Array.from(groupedData.entries()).map(([key, values]) => {
    // Parse the key back to get admin names
    let admin0_name, admin1_name, admin2_name;
    
    if (sharedAdmin1) {
      [admin0_name, admin1_name, admin2_name] = key.split("|");
    } else if (sharedAdmin0) {
      [admin0_name, admin1_name] = key.split("|");
    } else {
      admin0_name = key;
    }
    
    // Sum up all exposure values for this region
    const totalValue = d3.sum(values, d => d.value || 0);
    
    return {
      admin0_name,
      admin1_name: admin1_name || null,
      admin2_name: admin2_name || null,
      value: totalValue
    };
  });
}
// Geographic data binding
exposureMapData = {
  if (!dataGeoImpact || dataGeoImpact.length === 0) {
    return { features: [] };
  }

  // Check if boundaries exist before accessing properties
  if (!boundaries) {
    return { features: [] };
  }

  // Determine which admin level to show
  if (!sharedAdmin0) {
    // Show admin0 level
    if (!boundaries.admin0 || !boundaries.admin0.features) {
      return { features: [] };
    }
    return bindTabularToGeo({
      data: dataGeoImpact,
      dataBindColumn: "admin0_name",
      geoData: boundaries.admin0,
      geoDataBindColumn: "admin0_name"
    });
  } else if (!sharedAdmin1) {
    // Show admin1 level within selected admin0
    if (!boundaries.admin1 || !boundaries.admin1.features) {
      return { features: [] };
    }
    const data = dataGeoImpact.map(d => ({
      ...d,
      a1_a0: [d.admin1_name, d.admin0_name].join("_")
    }));
    
    const geoData = {
      ...boundaries.admin1,
      features: boundaries.admin1.features.filter(
        d => d.properties.admin0_name === sharedAdmin0
      )
    };

    return bindTabularToGeo({
      data: data,
      dataBindColumn: "admin1_name",
      geoData: geoData,
      geoDataBindColumn: "admin1_name"
    });
  } else {
    // Show admin2 level within selected admin1
    if (!boundaries.admin2 || !boundaries.admin2.features) {
      return { features: [] };
    }
    const data = dataGeoImpact.map(d => ({
      ...d,
      a2_a1_a0: [d.admin2_name, d.admin1_name, d.admin0_name].join("_")
    }));
    
    const geoData = {
      ...boundaries.admin2,
      features: boundaries.admin2.features.filter(d => 
        d.properties.admin1_name === sharedAdmin1 &&
        d.properties.admin0_name === sharedAdmin0
      )
    };



    return bindTabularToGeo({
      data: data,
      dataBindColumn: "admin2_name",
      geoData: geoData,
      geoDataBindColumn: "admin2_name"
    });
  }
}
// Visualization with proper loading/no-data states
exposureChoroplethMap = {
  // Show loading spinner only if data is still loading (null/undefined)
  if (exposureData === null || exposureData === undefined) {
    return createLoadingState(_lang({en: "Loading exposure data...", fr: "Chargement des données d'exposition..."}));
  }
  
  // If data is loaded but empty, or map data is empty, show no data state
  if (!exposureData || exposureData.length === 0 || !exposureMapData || !exposureMapData.features || exposureMapData.features.length === 0) {
    return createNoDataState(_lang(vulnerability_translations.no_data_available));
  }

  const data = exposureMapData;

  
  const plot = renderGeoMap({
    features: data.features,
    width: mapWidth,
    height: mapHeight,
    caption: vopNote.caption,
    projection: {
      type: "azimuthal-equal-area",
      domain: data
    },
    color: {
      legend: true,
      label: `${_lang({en: "VoP", fr: "VoP"})} (${intDollarUnit})`,
      range: colorScales.range.yellowGreen,
      unknown: colorScales.unknown,
      tickFormat: formatNumCompactShort
    },
    valueAccessor: (d) => {
      const dataColumn = "value";
      const fillValue = d.properties.data ? d.properties.data[dataColumn] : null;
      return fillValue;
    },
    tooltip: {
      channels: {
        country: {
          label: "Country",
          value: (d) => d.properties.admin0_name
        },
        name: {
          label: "Region",
          value: (d) => {
            if (d.properties.admin2_name) {
              return `${d.properties.admin2_name}, ${d.properties.admin1_name}`;
            } else if (d.properties.admin1_name) {
              return d.properties.admin1_name;
            } else {
              return d.properties.admin0_name;
            }
          }
        },
        data: {
          label: "VoP",
          value: (d) => {
            const dataColumn = "value";
            const data = d.properties.data ? d.properties.data[dataColumn] : undefined;
            return data;
          }
        }
      },
      format: {
        country: true,
        name: true,
        data: (d) => d ? formatNumCompactShort(d) : "No data"
      }
    },
    extraMarks: [
      Plot.geo(
        sharedAdmin2
          ? data.features.filter(d => d.properties.admin2_name === sharedAdmin2)
          : [],
        {
          fill: null,
          stroke: "#333",
          strokeWidth: 1.5
        }
      )
    ]
  });

  return htl.html`
  <!--   <div style=" -->
  <!--     border: 1px solid #e5e7eb; -->
  <!--     border-radius: 8px; -->
  <!--   padding: 20px; -->
  <!--   background-color: #fff; -->
  <!--   box-shadow: 0 2px 4px rgba(0,0,0,0.1); -->
  <!--     margin: 16px 0; -->
  <!-- "> -->
    ${plot}
    <!-- </div> -->
  `;
}
// Download functionality
exposureDownloadButton = {
  if (!dataGeoImpact || dataGeoImpact.length === 0) {
    return htl.html``;
  }
  return downloadButton(
    dataGeoImpact, 
    `exposure_data_${getExposureAdminSelection().replace(/\s+/g, '_')}_all_hazard_types`,
    _lang(vulnerability_translations.download_data)
  );
}
// Dynamic insights
exposureInsights = {
  if (!dataGeoImpact || dataGeoImpact.length === 0) {
    return createNoDataState();
  }

  // Calculate total exposure value
  const totalValue = d3.sum(dataGeoImpact, d => d.value || 0);
  const region = getExposureAdminSelection();
  const hazardLabel = "Any";
  
  // Get top 3 most exposed commodities
  let topCommodities = [];
  if (exposureData && exposureData.length > 0) {
    // Filter data based on current admin selection
    let filteredCommodityData = exposureData;
    
    if (sharedAdmin1) {
      filteredCommodityData = exposureData.filter(d => 
        d.admin0_name === sharedAdmin0 && 
        d.admin1_name === sharedAdmin1 &&
        d.admin2_name
      );
    } else if (sharedAdmin0) {
      filteredCommodityData = exposureData.filter(d => 
        d.admin0_name === sharedAdmin0 &&
        d.admin1_name &&
        !d.admin2_name
      );
    } else {
      filteredCommodityData = exposureData.filter(d => 
        !d.admin1_name && !d.admin2_name
      );
    }
    
    // Group by commodity and sum values
    const commodityGroups = d3.group(filteredCommodityData, d => d.crop);
    const commodityTotals = Array.from(commodityGroups.entries()).map(([crop, values]) => ({
      crop: crop,
      totalValue: d3.sum(values, d => d.value || 0)
    }));
    
    // Sort by total value and get top 3
    topCommodities = commodityTotals
      .sort((a, b) => b.totalValue - a.totalValue)
      .slice(0, 3)
      .filter(d => d.totalValue > 0);
  }
  
  // Format currency without the "2015 USD" prefix
  const formatCleanCurrency = (number) => {
    return new Intl.NumberFormat("en-US", {
      notation: "compact",
      compactDisplay: "short",
      style: "currency",
      currency: "USD",
    }).format(number);
  };
  
  const template = _lang(vulnerability_translations.exposure_insight_template);
  const replacements = [
    { name: "region", value: region },
    { name: "amount", value: formatCleanCurrency(totalValue) }
  ];
  
  const insight = generateInsight(template, replacements);
  
  // Add top commodities insight if available
  let commoditiesInsight = "";
  if (topCommodities.length > 0) {
    // Create dynamic text with specific commodity names
    const commodityNames = topCommodities
      .map(d => d.crop.replace(/-/g, ' '))
      .join(", ");
    
    const commoditiesText = _lang({
      en: ` The top 3 most exposed commodities are:`,
      fr: ` Les 3 produits les plus exposés sont:`
    });
    
    // Combine insight text with commodities list as HTML using proper htl.html syntax
    const fullInsight = htl.html`
      ${insight}${commoditiesText}
      <ol>
        ${topCommodities.map(d => htl.html`<li>${d.crop.replace(/-/g, ' ')} (${formatCleanCurrency(d.totalValue)})</li>`)}
      </ol>
    `;
    return createInsightDisplay(fullInsight);
  }
  
  return createInsightDisplay(insight);
}
sensitivityQuestion = _lang(vulnerability_translations.sensitivity_question);

viewof sensitivityAdmin0 = Inputs.select(dataAdmin0, {
  label: adminRegions.labels.admin0, 
  format: x => x.label,
  value: dataAdmin0.find(d => d.value === sharedAdmin0) || (dataAdmin0.length > 0 ? dataAdmin0[0] : null)
})
viewof sensitivityAdmin1 = Inputs.select(dataAdmin1, {
  label: adminRegions.labels.admin1, 
  format: x => x.label,
  value: dataAdmin1.find(d => d.value === sharedAdmin1) || (dataAdmin1.length > 0 ? dataAdmin1[0] : null)
})
viewof sensitivityAdmin2 = Inputs.select(dataAdmin2, {
  label: adminRegions.labels.admin2, 
  format: x => x.label,
  value: dataAdmin2.find(d => d.value === sharedAdmin2) || (dataAdmin2.length > 0 ? dataAdmin2[0] : null)
})
sensitivityControlsForm = htl.html`
<div style="
  background: #f8fafc;
  border: 2px solid #e2e8f0;
  border-radius: 12px;
  padding: 24px;
  box-shadow: 0 4px 6px rgba(0, 0, 0, 0.07);
  margin: 16px 0;
">
  <h3 style="margin: 0 0 20px 0; color: #2d3748; font-size: 1.1rem; font-weight: 600;">
    ${_lang(vulnerability_translations.geographic_selection)}
  </h3>
  <div style="
    display: flex;
    align-items: flex-start;
    gap: 20px;
    flex-wrap: wrap;
    justify-content: space-between;
  " class="form-inputs-container">
    <div style="flex: 1; min-width: 180px; max-width: 100%;">${viewof sensitivityAdmin0}</div>
    <div style="flex: 1; min-width: 180px; max-width: 100%;">${viewof sensitivityAdmin1}</div>
    <div style="flex: 1; min-width: 180px; max-width: 100%;">${viewof sensitivityAdmin2}</div>
  </div>
</div>
`
// Load icicle metadata and data with caching
icicleKeys = await loadDataWithCache("icicleKeys", async () => {
  return await FileAttachment(
    "/data/vulnerability_notebook/vulnerability_iciclekeys.json",
  ).json();
});
// Load data with caching - using parquet via DuckDB
csv_raw = await loadDataWithCache("sensitivityData", async () => {
  const db = await DuckDBClient.of({
    vulnerability_icicle: FileAttachment(
      "/data/vulnerability_notebook/vulnerability_icicledata_updated.parquet",
    ),
  });
  const data = await db.query("SELECT * FROM vulnerability_icicle");

  // Convert to the expected format: array of arrays [admin0, admin1, admin2, path, value]
  // The parquet should have columns: admin0_name, admin1_name, admin2_name, path, value
  return data
    .filter((d) => d.path != null && d.path !== "")
    .map((d) => {
      // Convert null values to "NA" string to match CSV format
      const admin0 = d.admin0_name || "SSA";
      const admin1 = d.admin1_name || "NA";
      const admin2 = d.admin2_name || "NA";

      return [admin0, admin1, admin2, d.path, d.value || 0];
    });
});
breadcrumbWidth = 200;
breadcrumbHeight = 35;
targetHeight = 425;
height = narrow ? narrowHeight : targetHeight;
narrowHeight = 600;
narrow = width <= 0;
segmentX = (d) => (narrow ? d.y0 : d.x0);
segmentY = (d) => (narrow ? d.x0 : d.y0);
segmentWidth = (d) => (narrow ? d.y1 - d.y0 : d.x1 - d.x0);
segmentHeight = (d) => (narrow ? d.x1 - d.x0 : d.y1 - d.y0);
partition = (data, order = null) =>
  d3
    .partition()
    .padding(1)
    .size(narrow ? [height, width] : [width, height])(
    d3
      .hierarchy(data)
      .sum((d) => d.value)
      .sort((a, b) => {
        if (order) {
          // Look up indices, fallback to 999 if not defined
          return (order[a.data.name] ?? 999) - (order[b.data.name] ?? 999);
        }
        return b.value - a.value;
      }),
  );
function buildHierarchy(csv) {
  // Helper function that transforms the given CSV into a hierarchical format.
  // Skip the population level and start directly with demographic categories
  const root = { name: "root", children: [] };
  for (let i = 0; i < csv.length; i++) {
    const sequence = csv[i][0];
    const size = +csv[i][1];
    // Skip invalid rows
    if (!sequence || isNaN(size)) {
      continue;
    }
    const parts = sequence.split("_");
    let currentNode = root;
    // Skip the first part (population) and start from the second part
    for (let j = 1; j < parts.length; j++) {
      const children = currentNode["children"];
      const nodeName = parts[j];
      let childNode = null;
      let foundChild = false;
      // Search for existing child with the same name
      for (let k = 0; k < children.length; k++) {
        if (children[k]["name"] === nodeName) {
          childNode = children[k];
          foundChild = true;
          break;
        }
      }
      // If not found, create a new child node
      if (!foundChild) {
        childNode = { name: nodeName, children: [] };
        children.push(childNode);
      }
      currentNode = childNode;
      // If it's the last part of the sequence, create a leaf node
      if (j === parts.length - 1) {
        childNode.value = size;
      }
    }
  }
  return root;
}
csv = {
  // Check if csv_raw exists and is an array before processing
  if (!csv_raw || !Array.isArray(csv_raw) || csv_raw.length === 0) {
    return [];
  }
  
  if (!sensitivityAdmin0.value) {
    // No admin selection - use SSA data
    return csv_raw
      .filter(
        (item) => item && item[0] === "SSA" && item[1] === "NA" && item[2] === "NA" && item[3]
      )
      .map((item) => [item[3], item[4]]);
  } else if (!sensitivityAdmin1.value) {
    return csv_raw
      .filter(
        (item) => item && item[0] === sensitivityAdmin0.value && item[1] === "NA" && item[2] === "NA" && item[3]
      )
      .map((item) => [item[3], item[4]]);
  } else if (sensitivityAdmin1.value && !sensitivityAdmin2.value) {
    return csv_raw
      .filter(
        (item) => item && item[0] === sensitivityAdmin0.value && item[1] === sensitivityAdmin1.value && item[2] === "NA" && item[3]
      )
      .map((item) => [item[3], item[4]]);
  } else if (sensitivityAdmin1.value !== null && sensitivityAdmin2.value !== null) {
    return csv_raw
      .filter(
        (item) =>
          item && item[0] === sensitivityAdmin0.value && item[1] === sensitivityAdmin1.value && item[2] === sensitivityAdmin2.value && item[3]
      )
      .map((item) => [item[3], item[4]]);
  }
  return [];
}
data = buildHierarchy(csv)

// Create percentage table for insights
icicle_pct_table = {
  if (!csv || csv.length === 0) return [];
  
  // A little function to basically calculate the same thing as the breadcrumb but for all the admin region.
  // This is what users should see if going to a tabular view and also what they should download. It is much easier to understand and work with.
  const summarize_icicle = (csv, icicle_keys = null) => {
    const rows = csv.map(([key, val]) => ({
      parts: key.split("_").slice(1),
      value: +val
    }));
    const total = d3.sum(rows, (d) => d.value);

    const maxDepth = Math.max(...rows.map((r) => r.parts.length));
    const levelNames = icicle_keys
      ? Object.keys(icicle_keys)
      : [...Array(maxDepth).keys()].map((i) => `level${i + 1}`);

    const lookupName = (dim, key) => {
      if (!icicle_keys) return key;
      return icicle_keys[dim]?.[key]?.[0] ?? key;
    };

    const genderTotals = {};
    rows.forEach((r) => {
      const gender = r.parts[0];
      genderTotals[gender] = (genderTotals[gender] || 0) + r.value;
    });

    // Recursive helper
    const recurse = (rows, depth = 0, prefix = []) => {
      if (!rows.length) return [];
      if (depth >= maxDepth) return [];

      return d3
        .rollups(
          rows,
          (v) => d3.sum(v, (d) => d.value),
          (d) => d.parts[depth]
        )
        .flatMap(([k, sum]) => {
          const current = [...prefix, k];
          const rowObj = {};
          levelNames.forEach((col, i) => {
            rowObj[col] = current[i] ? lookupName(col, current[i]) : null;
          });
          rowObj.pct_total = (100 * sum) / total;
          // pct relative to gender
          const genderKey = prefix[0];
          if (genderKey && genderTotals[genderKey]) {
            rowObj.pct_of_gender = (100 * sum) / genderTotals[genderKey];
          }
          return [
            rowObj,
            ...recurse(
              rows.filter((r) => r.parts[depth] === k),
              depth + 1,
              current
            )
          ];
        });
    };

    return recurse(rows);
  };

  return summarize_icicle(csv, icicleKeys);
}

// Create insight data structure
icicle_insight = {
  if (!icicle_pct_table || icicle_pct_table.length === 0) return {};
  
  const round = 1;
  
  // Only sum leaf-level rows (most specific combinations) to avoid double-counting
  const leafRows = icicle_pct_table.filter(d => d.gender && d.poverty && d.education);
  
  const sum = (filter) =>
    +d3
      .sum(leafRows.filter(filter), (d) => d.pct_of_gender) // Total as % of gender not total pop
      .toFixed(round);

  // Helper function to find single value
  const find = (filter) =>
    +(icicle_pct_table.find(filter)?.pct_total || 0).toFixed(round);
  
  return {
    // Total population by gender
    male: +d3.sum(leafRows.filter(d => d.gender === "male"), d => d.pct_total).toFixed(round),
    female: +d3.sum(leafRows.filter(d => d.gender === "female"), d => d.pct_total).toFixed(round),
    
    // Education levels by gender (using pct_of_gender)
    sec_edu_m: sum((d) => d.gender === "male" && d.education === "primary"),
    sec_edu_f: sum((d) => d.gender === "female" && d.education === "primary"),
    no_edu_m: sum((d) => d.gender === "male" && d.education === "noprimary"),
    no_edu_f: sum((d) => d.gender === "female" && d.education === "noprimary"),
    
    // Poverty levels by gender (using pct_of_gender)
    high_poverty_m: sum((d) => d.gender === "male" && d.poverty === "high"),
    high_poverty_f: sum((d) => d.gender === "female" && d.poverty === "high"),
    low_poverty_m: sum((d) => d.gender === "male" && d.poverty === "low"),
    low_poverty_f: sum((d) => d.gender === "female" && d.poverty === "low"),
    
    // Most vulnerable group (high poverty + no education)
    most_vuln_m: find(
      (d) =>
        d.gender === "male" &&
        d.poverty === "high" &&
        d.education === "noprimary"
    ),
    most_vuln_f: find(
      (d) =>
        d.gender === "female" &&
        d.poverty === "high" &&
        d.education === "noprimary"
    )
  };
}
// Generate a string that describes the points of a breadcrumb SVG polygon.
function breadcrumbPoints(d, i) {
  const tipWidth = 10;
  const points = [];
  points.push("0,0");
  points.push(`${breadcrumbWidth},0`);
  points.push(`${breadcrumbWidth + tipWidth},${breadcrumbHeight / 2}`);
  points.push(`${breadcrumbWidth},${breadcrumbHeight}`);
  points.push(`0,${breadcrumbHeight}`);
  if (i > 0) {
    // Leftmost breadcrumb; don't include 6th vertex.
    points.push(`${tipWidth},${breadcrumbHeight / 2}`);
  }
  return points.join(" ");
}
alias = {
  const flat_alias = Object.fromEntries(
    Object.values(icicleKeys).flatMap((obj) =>
      Object.entries(obj).map(([k, v]) => [k, v[0]])
    )
  );
  flat_alias.population = "Total Population";
  return flat_alias;
}
icicle_dict = new Object({
  gender0: { en: "Male", fr: "" },
  gender1: { en: "Female", fr: "" },
  poverty1: { en: "Low Poverty", fr: "" },
  poverty2: { en: "Moderate Poverty", fr: "" },
  poverty3: { en: "High Poverty", fr: "" },
  education3: { en: "Secondary Education", fr: "" },
  education2: { en: "Primary Education", fr: "" },
  education1: { en: "No Primary Education", fr: "" },
});

icicle_color = d3
  .scaleOrdinal()
  .domain([
    "population",
    "gender1", // female
    "gender0", // male

    "poverty1", // low poverty = good // TODO: Check the directionality of this in preprocess code
    "poverty2", // mid. poverty = mid
    "poverty3", // high poverty = bad

    "education3", // high edu. = good
    "education2", // mid. edu. = mid
    "education1", // low edu = bad
  ])
  .range([
    "#a4a4a4", // population - removed
    "#59CD90", // female - #59CD90
    "#A491D3", // male

    "#f4bb21", // low poverty = good
    "#fc8a34", // mid. poverty = mid
    "#ec5a47", // high poverty = bad

    "#f4bb21", // high edu = good
    "#fc8a34", // mid. edu = mid
    "#ec5a47", // low edu = bad
  ]);
breadcrumb = {
  // Only show breadcrumb if there's a sequence to display
  // if (!icicle.sequence || icicle.sequence.length === 0) {
  //   return htl.html`<div style="height: ${breadcrumbHeight}px;"></div>`;
  // }

  const renderBreadcrumb = () => {
    const svg = d3
      .create("svg")
      .attr("viewBox", `0 0 ${breadcrumbWidth * 4.75} ${breadcrumbHeight}`)
      .style("font", "13px sans-serif")
      .style("margin", "2px")
      .style("display", "block");

    const g = svg
      .selectAll("g")
      .data(icicle.sequence)
      .join("g")
      .attr("transform", (d, i) => `translate(${i * breadcrumbWidth}, 0)`);

    g.append("polygon")
      .attr("points", breadcrumbPoints)
      .attr("fill", (d) => {
        return icicle_color(d.data.name)
        })
      .attr("stroke", "white");

    g.append("text")
      .attr("x", (breadcrumbWidth + 10) / 2)
      .attr("y", 15)
      .attr("dy", "0.35em")
      .attr("text-anchor", "middle")
      .attr("fill", "white")
      .attr("font-size", "12px")
      .text((d) => {
        const name = d.data.name;
        return _lang(icicle_dict[name]);
        // Default fallback - capitalize and replace underscores
        // return name.charAt(0).toUpperCase() + name.slice(1).replace(/_/g, ' ');
      });

    // Always show percentage at the end of breadcrumb when there's a sequence
    if (icicle.sequence && icicle.sequence.length > 0) {
      const percentage = Number(icicle.percentage) || 0;
      svg
        .append("text")
        .text(percentage > 0 ? percentage.toFixed(1) + "%" : "")
        .attr("x", (icicle.sequence.length + 0.25) * breadcrumbWidth)
        .attr("y", breadcrumbHeight / 2)
        .attr("dy", "0.35em")
        .attr("text-anchor", "middle")
        .attr("font-size", "12px")
        .attr("fill", "black")
        .attr("font-weight", "bold");
    }
    return svg.node();
  };

  return htl.html`
<div style="height: ${breadcrumbHeight + 3}px; overflow: hidden;">
    ${icicle.sequence && icicle.sequence.length > 0 
      ? renderBreadcrumb(icicle.sequence)
      : ""}
  </div>
`;
}
viewof icicle = {
  // Early return for loading state
  if (csv_raw === null || csv_raw === undefined || icicleKeys === null || icicleKeys === undefined) {
    const loading = createLoadingState(_lang({
      en: "Loading sensitivity data...",
      fr: "Chargement des données de sensibilité..."
    }));
    const element = document.createElement('div');
    element.innerHTML = loading.outerHTML;
    element.value = { sequence: [], percentage: 0.0 };
    return element;
  }
  
  // Early return for empty data
  if (!csv_raw || csv_raw.length === 0 || !icicleKeys) {
    const noData = createNoDataState(_lang(vulnerability_translations.no_data_available));
    const element = document.createElement('div');
    element.innerHTML = noData.outerHTML;
    element.value = { sequence: [], percentage: 0.0 };
    return element;
  }

  // Configuration constants
  const config = {
    gutter: 80,
    heightMultiplier: 1.5, // Increase plot height by 50%
    layerLabels: ["Gender", "Poverty", "Education"],
    order: {
      poverty1: 2,
      poverty2: 1,
      poverty3: 0,
      education1: 0,
      education2: 1,
      education3: 2
    },
    legend: {
      x: 1,
      y: 5,
      width: 300,
      height: 30,
      cornerRadius: 10,
      itemSpacing: 100,
      itemOffset: 25
    }
  };

  const adjustedHeight = height * config.heightMultiplier;
  
  // Create adjusted partition function that uses the taller height
  const adjustedPartition = (data, order) => {
    // Call the original partition but we'll scale the y-values
    const originalRoot = partition(data, order);
    
    // Scale all y-values by the height multiplier
    const scaleNode = (node) => {
      node.y0 *= config.heightMultiplier;
      node.y1 *= config.heightMultiplier;
      if (node.children) {
        node.children.forEach(scaleNode);
      }
    };
    scaleNode(originalRoot);
    
    return originalRoot;
  };
  
  const root = adjustedPartition(data, config.order);
  
  // State management
  let frozen = false;
  let frozenSequence = [];

  // Create SVG element
  const svg = d3.create("svg")
    .attr("viewBox", `0 0 ${width + config.gutter} ${adjustedHeight}`)
    .style("font", "12px sans-serif");

  const element = svg.node();
  element.value = { sequence: [], percentage: 0.0 };

  // Main plot group
  const plot = svg.append("g")
    .attr("transform", `translate(${config.gutter},0)`);

  // Background rect
  plot.append("rect")
    .attr("width", width)
    .attr("height", adjustedHeight)
    .attr("fill", "none");

  // Helper function to determine text color based on background
  const getTextColor = (bgColor) => {
    return ["#A491D3", "#ec5a47", "#fc8a34"].includes(bgColor) ? "white" : "black";
  };

  // Helper function to update element value
  const updateElementValue = (sequence, percentage) => {
    element.value = { sequence, percentage };
    element.dispatchEvent(new CustomEvent("input"));
  };

  // Helper function to update segment opacity
  const updateSegmentOpacity = (targetSequence) => {
    segment.attr("fill-opacity", node => 
      targetSequence.indexOf(node) >= 0 ? 1.0 : 0.3
    );
  };

  // Create segments
  const segment = plot.append("g")
    .attr("transform", narrow 
      ? `translate(${-root.y1}, 40)` 
      : `translate(0, ${-root.y1 + 40})`)
    .selectAll("rect")
    .data(root.descendants().filter(d => d.depth))
    .join("rect")
    .attr("fill", d => icicle_color(d.data.name))
    .attr("x", segmentX)
    .attr("y", segmentY)
    .attr("width", segmentWidth)
    .attr("height", segmentHeight)
    .on("mouseenter", (event, d) => {
      if (frozen) return;
      const sequence = d.ancestors().reverse().slice(1);
      updateSegmentOpacity(sequence);
      const percentage = (100 * (d.value / root.value)).toPrecision(3);
      updateElementValue(sequence, percentage);
    })
    .on("click", (event, d) => {
      if (frozen) {
        frozen = false;
        return;
      }
      frozen = true;
      frozenSequence = d.ancestors().reverse().slice(1);
      updateSegmentOpacity(frozenSequence);
      const percentage = (100 * (d.value / root.value)).toPrecision(3);
      updateElementValue(frozenSequence, percentage);
      event.stopPropagation();
    });

  // Add layer labels
  const depthNodes = d3.rollups(
    root.descendants().filter(d => d.depth),
    v => v[0],
    d => d.depth
  ).sort((a, b) => d3.ascending(a[0], b[0]));

  plot.append("g")
    .attr("transform", narrow 
      ? `translate(${-root.y1}, 40)` 
      : `translate(0, ${-root.y1 + 40})`)
    .selectAll("text.layer-label")
    .data(depthNodes.map(([depth, rep]) => ({
      depth,
      y: segmentY(rep) + segmentHeight(rep) / 2
    })))
    .join("text")
    .attr("class", "layer-label")
    .attr("x", -12)
    .attr("y", d => d.y)
    .attr("dy", "0.35em")
    .attr("text-anchor", "end")
    .attr("font-size", "12px")
    .attr("font-weight", "600")
    .text(d => config.layerLabels[d.depth - 1] ?? `Layer ${d.depth}`);

  // Add percentage labels
  plot.append("g")
    .attr("transform", narrow 
      ? `translate(${-root.y1}, 40)` 
      : `translate(0, ${-root.y1 + 40})`)
    .selectAll("text")
    .data(root.descendants().filter(d => d.depth && d.data.name !== "population"))
    .join("text")
    .attr("x", d => segmentX(d) + segmentWidth(d) / 2)
    .attr("y", d => segmentY(d) + segmentHeight(d) / 2)
    .attr("dy", "0.35em")
    .attr("text-anchor", "middle")
    .attr("font-size", "12px")
    .attr("font-weight", "bold")
    .attr("fill", d => getTextColor(icicle_color(d.data.name)))
    .attr("pointer-events", "none")
    .text(d => {
      const dataObj = d?.data;
      const genderName = dataObj.name.startsWith("gender") 
        ? _lang(icicle_dict[dataObj.name]) 
        : null;
      const genderStr = genderName ? `${genderName} - ` : "";
      const percentage = (100 * (d.value / root.value)).toFixed(1);
      return segmentWidth(d) > 30 && segmentHeight(d) > 15 
        ? `${genderStr}${percentage}%` 
        : "";
    });

  // Event handlers
  const handleMouseLeave = () => {
    if (frozen) return;
    segment.attr("fill-opacity", 1);
    updateElementValue([], 0.0);
  };

  const handleClick = () => {
    if (frozen) {
      frozen = false;
      frozenSequence = [];
      segment.attr("fill-opacity", 1);
      updateElementValue([], 0.0);
    }
  };

  plot.on("mouseleave", handleMouseLeave);
  plot.on("click", handleClick);

  // Cleanup mechanism
  if (element.dispose) element.dispose();
  element.dispose = () => {
    plot.on("mouseleave", null).on("click", null);
    segment.on("mouseenter", null).on("click", null);
  };

  // Add legend
  const colorScale = d3.scaleOrdinal()
    .domain(["Better", "Moderate", "Worse"])
    .range(["#F4BB21", "#FC8A34", "#EC5A47"]);

  svg.append("rect")
    .attr("x", config.legend.x)
    .attr("y", config.legend.y)
    .attr("rx", config.legend.cornerRadius)
    .attr("ry", config.legend.cornerRadius)
    .attr("width", config.legend.width)
    .attr("height", config.legend.height)
    .attr("fill", "#fff");

  const legend = svg.selectAll(".legend")
    .data(colorScale.domain())
    .enter()
    .append("g")
    .attr("class", "legend")
    .attr("transform", (d, i) => 
      `translate(${i * config.legend.itemSpacing + config.legend.itemOffset}, 11)`);

  legend.append("rect")
    .attr("x", 0)
    .attr("width", 18)
    .attr("height", 18)
    .style("fill", colorScale);

  legend.append("text")
    .attr("x", 24)
    .attr("y", 9)
    .attr("dy", ".35em")
    .text(d => d);

  return element;
}
// Download functionality
sensitivityDownloadButton = {
  if (!icicle_pct_table || icicle_pct_table.length === 0) {
    return htl.html``;
  }
  
  return downloadButton(
    icicle_pct_table, 
    `sensitivity_data_${getSensitivityAdminSelection().replace(/\s+/g, '_')}`,
    _lang(vulnerability_translations.download_data)
  );
}
// Dynamic insights - reactive to icicle selection
sensitivityInsights = {
  if (!data || data.length === 0 || !csv || csv.length === 0 || !icicle_insight) {
    return createNoDataState();
  }

  const region = getSensitivityAdminSelection();
  const insight = icicle_insight;
  
  // Generate default insights (always shown)
  const totalMale = insight.male;
  const totalFemale = insight.female;
  const mostVulnTotal = (insight.most_vuln_m + insight.most_vuln_f).toFixed(1);
  const femalePct = insight.most_vuln_f.toFixed(1);
  const malePct = insight.most_vuln_m.toFixed(1);
  
  // Education comparison
  const noEduMale = insight.no_edu_m.toFixed(1);
  const noEduFemale = insight.no_edu_f.toFixed(1);
  
  // Poverty comparison  
  const highPovMale = insight.high_poverty_m.toFixed(1);
  const highPovFemale = insight.high_poverty_f.toFixed(1);
  
  // Structure the default insights as separate parts for better HTML rendering
  const defaultInsightParts = [
    `In ${region}, men represent ${totalMale}% and women represent ${totalFemale}% of the population.`,
    `Among the most vulnerable group (high poverty + no primary education), ${femalePct}% are female and ${malePct}% are male.`,
    `Education gap: ${noEduFemale}% of women vs ${noEduMale}% of men have no primary education.`,
    `Poverty gap: ${highPovFemale}% of women vs ${highPovMale}% of men live in high poverty areas.`
  ];
  const defaultInsightText = defaultInsightParts.join('\n\n');
  return createInsightDisplay(defaultInsightText);
}
adaptiveCapacityQuestion = _lang(
  vulnerability_translations.adaptive_capacity_question,
);

viewof adaptiveCapacityAdmin0 = Inputs.select(dataAdmin0, {
  label: adminRegions.labels.admin0, 
  format: x => x.label,
  value: dataAdmin0.find(d => d.value === sharedAdmin0) || (dataAdmin0.length > 0 ? dataAdmin0[0] : null)
})
viewof adaptiveCapacityAdmin1 = Inputs.select(dataAdmin1, {
  label: adminRegions.labels.admin1, 
  format: x => x.label,
  value: dataAdmin1.find(d => d.value === sharedAdmin1) || (dataAdmin1.length > 0 ? dataAdmin1[0] : null)
})
viewof adaptiveCapacityAdmin2 = Inputs.select(dataAdmin2, {
  label: adminRegions.labels.admin2, 
  format: x => x.label,
  value: dataAdmin2.find(d => d.value === sharedAdmin2) || (dataAdmin2.length > 0 ? dataAdmin2[0] : null)
})
adaptiveCapacityControlsForm = htl.html`
<div style="
  background: #f8fafc;
  border: 2px solid #e2e8f0;
  border-radius: 12px;
  padding: 24px;
  box-shadow: 0 4px 6px rgba(0, 0, 0, 0.07);
  margin: 16px 0;
">
  <h3 style="margin: 0 0 20px 0; color: #2d3748; font-size: 1.1rem; font-weight: 600;">
    ${_lang(vulnerability_translations.geographic_selection)}
  </h3>
  <div style="
    display: flex;
    align-items: flex-start;
    gap: 20px;
    flex-wrap: wrap;
    justify-content: space-between;
  " class="form-inputs-container">
    <div style="flex: 1; min-width: 180px; max-width: 100%;">${viewof adaptiveCapacityAdmin0}</div>
    <div style="flex: 1; min-width: 180px; max-width: 100%;">${viewof adaptiveCapacityAdmin1}</div>
    <div style="flex: 1; min-width: 180px; max-width: 100%;">${viewof adaptiveCapacityAdmin2}</div>
  </div>
</div>
`
// Load data with caching - using parquet via DuckDB
adaptiveCapacityRawData = await loadDataWithCache(
  "adaptiveCapacityData",
  async () => {
    const db = await DuckDBClient.of({
      enabling_vars: FileAttachment(
        "/data/vulnerability_notebook/enabling_vars.parquet",
      ),
    });
    const data = await db.query("SELECT * FROM enabling_vars");

    return data;
  },
);
// Load metadata with caching
adaptiveCapacityMetadata = await loadDataWithCache(
  "adaptiveCapacityMetadata",
  async () => {
    return await FileAttachment(
      "/data/vulnerability_notebook/enabling_vars_metadata.json",
    ).json();
  },
);
// Process the new normalized data structure
adaptiveCapacityData = {
  if (!adaptiveCapacityRawData || !Array.isArray(adaptiveCapacityRawData)) {
    console.error("adaptiveCapacityRawData is not valid:", adaptiveCapacityRawData);
    return [];
  }
  
  return adaptiveCapacityRawData.map(d => ({
    ...d,
    // Use the new normalized columns directly (0-1 scale, high = good)
    pct_electric_norm: d.pct_electric_norm,
    pct_piped_water_norm: d.pct_piped_water_norm,
    min_to_cities_norm: d.min_to_cities_norm,
    conflict_density_norm: d.conflict_density_norm,
    pct_cellphone_norm: d.pct_cellphone_norm,
    // Keep original values for tooltips
    pct_electric: d.pct_electric,
    pct_piped_water: d.pct_piped_water,
    min_to_cities: d.min_to_cities,
    conflict_density: d.conflict_density,
    pct_cellphone: d.pct_cellphone
  }))
}
// Get current admin selections (same variable names as sensitivity section)
adaptiveCapacityAdminSelections = {
  return {
    selectAdmin0: adaptiveCapacityAdmin0?.value || null,
    selectAdmin1: adaptiveCapacityAdmin1?.value || null,
    selectAdmin2: adaptiveCapacityAdmin2?.value || null
  }
}
// Filter data based on admin selections (same pattern as sensitivity)
filteredAdaptiveCapacityData = {
  const selections = adaptiveCapacityAdminSelections;
  
  if (!adaptiveCapacityData || adaptiveCapacityData.length === 0) {
    console.error("adaptiveCapacityData is empty or undefined");
    return [];
  }
  
  let data = adaptiveCapacityData;

  if (!selections.selectAdmin0) {
      data =  data.filter((d) => (d.admin0_name === "SSA" || !d.admin0_name)) // Select just SSA data & do not show regional avg
  } else if (!selections.selectAdmin1) {
      data =  data.filter((d) => (d.admin0_name === selections.selectAdmin0))
  } else if (selections.selectAdmin1  && !selections.selectAdmin2) {
      data =  data.filter((d)=> (d.admin0_name === selections.selectAdmin0 && d.admin1_name === selections.selectAdmin1))
  } else if (selections.selectAdmin1 && selections.selectAdmin2) {
      data =  data.filter((d) => (d.admin0_name === selections.selectAdmin0 && d.admin1_name === selections.selectAdmin1 && d.admin2_name === selections.selectAdmin2))
  }

 return data
}
// Helper function to get current region name (uses shared utility)
getAdaptiveCapacityRegion = () => {
  return getAdminSelection(
    adaptiveCapacityAdmin0,
    adaptiveCapacityAdmin1,
    adaptiveCapacityAdmin2,
  );
};
{
  // Check if adaptiveCapacityRawData exists and is an array before processing
  if (!adaptiveCapacityRawData || !Array.isArray(adaptiveCapacityRawData)) {
    return createNoDataState();
  }
  
  let data = adaptiveCapacityRawData;
  const selections = adaptiveCapacityAdminSelections;
  const isSSA = selections.selectAdmin0 === "SSA" || !selections.selectAdmin0;
  // Data for the blue bar - the lowest selected region
  let region_data = data.filter(
    (d) =>
      d.admin0_name === (selections.selectAdmin0 || "SSA") &&
      d.admin1_name === selections.selectAdmin1 &&
      d.admin2_name === selections.selectAdmin2,
  );
  
  let region_name = getAdaptiveCapacityRegion();

  // Data for the grey bar which is the parent
  // eg. region = AGO so parent = SSA or region = nairobi so parent = kenya
  let parent_data = [];
  let parent_name = "";

  if (!selections.selectAdmin0) {
    // Top-level (SSA selected so no parent data is returned)
    parent_data = [];
  } else if (!selections.selectAdmin1) {
    // admin0 selected but no admin1 so parent = SSA
    parent_data = data.filter((d) => d.admin0_name === "SSA");
    parent_name = "SSA";
  } else if (!selections.selectAdmin2) {
    // admin1 selected but no admin2 so parent = admin0 selection
    parent_data = data.filter(
      (d) => d.admin0_name === selections.selectAdmin0 && !d.admin1_name,
    );
    parent_name = selections.selectAdmin0;
  } else {
    // admin2 selected so parent = admin1 selected
    parent_data = data.filter(
      (d) =>
        d.admin0_name === selections.selectAdmin0 &&
        d.admin1_name === selections.selectAdmin1 &&
        !d.admin2_name,
    );
    parent_name = selections.selectAdmin1;
  }

  // Define the indicators to display using new normalized data structure
  const indicators = [
    {
      key: "pct_electric_norm",
      rawKey: "pct_electric",
      label: _lang({ en: "Electricity Access", fr: "Accès à l'électricité" }),
      unit: "%",
    },
    {
      key: "pct_piped_water_norm",
      rawKey: "pct_piped_water",
      label: _lang({ en: "Piped Water Access", fr: "Accès à l'eau courante" }),
      unit: "%",
    },
    {
      key: "min_to_cities_norm",
      rawKey: "min_to_cities",
      label: _lang({
        en: "Distance to Market",
        fr: "Temps de trajet vers les villes",
      }),
      unit: " min.",
    },
    {
      key: "pct_cellphone_norm",
      rawKey: "pct_cellphone",
      label: _lang({
        en: "Cellphone Access",
        fr: "Accès au téléphone portable",
      }),
      unit: "%",
    },
    {
      key: "conflict_density_norm",
      rawKey: "conflict_density",
      label: _lang({ en: "Lack of Conflict", fr: "Densité de conflit" }),
      unit: " count/km²",
    },
  ];

  const chartData = indicators.map((indicator) => {
    // Get normalized value (0-1 scale, high = good)
    // Check if region_data has elements before accessing [0]
    const region_normalizedValue = region_data.length > 0 
      ? (region_data[0]?.[indicator.key] || null)
      : null;
    const parent_normalizedValue = parent_data.length > 0
      ? (parent_data[0]?.[indicator.key] || null)
      : null;
    // Get original value for tooltips
    const region_rawValue = region_data.length > 0
      ? (region_data[0]?.[indicator.rawKey] || null)
      : null;
    const parent_rawValue = parent_data.length > 0
      ? (parent_data[0]?.[indicator.rawKey] || null)
      : null;

    return {
      indicator: indicator.label,
      region_normalizedValue, // Use normalized value for chart
      region_rawValue, // Keep original for tooltips
      parent_normalizedValue,
      parent_rawValue,
      unit: indicator.unit,
    };
  });

  // const sortedData = chartData.sort((a, b) => b.value - a.value);
  // sort alphabetically so consistent between selections to allow easy comparisons
  const sortedData = chartData.sort((a, b) => a.indicator.localeCompare(b.indicator));

  const maxValue = 1.0; // Technically some values pass this, but in general this should be max
  
  const plot_channels = {
    [_lang({en: `${region_name} Value`, fr: "Valeur de la région sélectionnée"})]:
      d => d.region_rawValue == null
        ? null // 0 is meaningful so this stays null
        : Number(d.region_rawValue).toFixed(1) + (d.unit || "")
  };

  if (!isSSA) {
    plot_channels[_lang({en: `${parent_name} Value`, fr: "Moyenne régionale"})] =
      d => d.parent_rawValue == null
        ? null // 0 is meaningful so this stays null
        : Number(d.parent_rawValue).toFixed(1) + (d.unit || "")
  };

  const plot =  Plot.plot({
    width: 1000, // Full width
    height: 400,
    marginLeft: 160, // Reduced for better space utilization
    marginRight: 40, // Reduced for better space utilization
    marginTop: 10, // Reduced white space
    marginBottom: 40, // Reduced white space
    color: {
      legend: !isSSA, // Only show legend if not SSA
      domain: [
        _lang({en: `${region_name} Value`, fr: `${region_name} Value`}),
        _lang({en: `${parent_name} Value`, fr: "Moyenne régionale"})
      ],
      range: ["#4a90e2", "#e5e5e5"]
    },
    x: {
      label: _lang({en: "Ability to Adapt", fr: "Niveau de performance"}),
      labelAnchor: "center",
      labelOffset: 30,
      domain: [0, maxValue], // Normalized scale 0-1
      tickFormat: (d) => (d === 0 ? _lang({en: "Low", fr: "Faible"}) : d === 1 ? _lang({en: "High", fr: "Élevé"}) : ""),
      ticks: [0, 0.5, 1.0],
      tickSize: 4,
      labelFontSize: 12,
      labelFontWeight: "600"
    },
    y: {
      label: null, // Remove y-axis label to save space
      tickSize: 0,
      grid: true,
      // domain: sortedData.map(d => d.indicator) // Only show indicators with data
    },

    marks: [
      // Regional average bars (light gray background) - full width
      Plot.barX(sortedData, {
        x: "parent_normalizedValue",
        y: "indicator",
        fill: "#e5e5e5",
        opacity: 0.7
      }),
      
      // Actual value bars (blue bars) - narrower using inset
      
      Plot.barX(sortedData, {
        x: "region_normalizedValue", 
        y: "indicator",
        fill: "#4a90e2",
        insetTop: 15, // Make bars narrower
        insetBottom: 15, 
        channels: plot_channels,
        tip: {
          anchor: "top", // Position above the blue bar
          dy: -10, // Offset upward
          format: {
            x: false
          }
        }
      })
    ]
  });

  return plot;
}
// Download functionality
adaptiveCapacityDownloadButton = {
  if (!filteredAdaptiveCapacityData || filteredAdaptiveCapacityData.length === 0) {
    return htl.html``;
  }
  
  const region = getAdaptiveCapacityRegion();
  return downloadButton(
    filteredAdaptiveCapacityData, 
    `adaptive_capacity_data_${region.replace(/\s+/g, '_')}`,
    _lang(vulnerability_translations.download_data)
  );
}
// Dynamic Insights following wireframe template
adaptiveCapacityInsights = {
  if (!filteredAdaptiveCapacityData || filteredAdaptiveCapacityData.length === 0) {
    return createNoDataState(); // Use same function as exposure
  }
  
  const data = filteredAdaptiveCapacityData[0];
  const region = getAdaptiveCapacityRegion();
    
  // Helper function to determine threshold level
  const getThresholdLevel = (value, thresholds, isInverse = false) => {
    if (isInverse) {
      // For inverse indicators (lower is better)
      if (value <= thresholds.low[0]) return _lang({en: "high", fr: "élevé"});
      if (value <= thresholds.mid[0]) return _lang({en: "moderate", fr: "modéré"});
      return _lang({en: "low", fr: "faible"});
    } else {
      // For normal indicators (higher is better)
      if (value >= thresholds.high[0]) return _lang({en: "high", fr: "élevé"});
      if (value >= thresholds.mid[0]) return _lang({en: "moderate", fr: "modéré"});
      return _lang({en: "low", fr: "faible"});
    }
  };

  // Create indicators using new normalized data structure
  const indicators = [
    {
      key: "pct_electric_norm",
      originalKey: "pct_electric",
      label: _lang({en: "Electricity Access", fr: "Accès à l'électricité"}),
      originalValue: data.pct_electric,
      normalizedValue: data.pct_electric_norm,
      unit: "%"
    },
    {
      key: "pct_piped_water_norm",
      originalKey: "pct_piped_water", 
      label: _lang({en: "Piped Water Access", fr: "Accès à l'eau courante"}),
      originalValue: data.pct_piped_water,
      normalizedValue: data.pct_piped_water_norm,
      unit: "%"
    },
    {
      key: "min_to_cities_norm",
      originalKey: "min_to_cities",
      label: _lang({en: "Travel Time to Cities", fr: "Temps de trajet vers les villes"}),
      originalValue: data.min_to_cities,
      normalizedValue: data.min_to_cities_norm,
      unit: " min"
    },
    {
      key: "pct_cellphone_norm",
      originalKey: "pct_cellphone",
      label: _lang({en: "Cellphone Access", fr: "Accès au téléphone portable"}),
      originalValue: data.pct_cellphone,
      normalizedValue: data.pct_cellphone_norm,
      unit: "%"
    },
    {
      key: "conflict_density_norm",
      originalKey: "conflict_density",
      label: _lang({en: "Conflict Density", fr: "Densité de conflit"}),
      originalValue: data.conflict_density,
      normalizedValue: data.conflict_density_norm,
      unit: " count/km²"
    }
  ];

  // Format indicators with performance levels based on normalized values
  const thresholdIndicators = indicators.map(indicator => {
    const normalizedValue = typeof indicator.normalizedValue === 'number' ? indicator.normalizedValue : 0;
    const originalValue = typeof indicator.originalValue === 'number' ? indicator.originalValue : 0;
    
    // Determine performance level based on normalized value (0-1 scale)
    let level;
    if (normalizedValue >= 0.8) {
      level = _lang({en: "high", fr: "élevé"});
    } else if (normalizedValue >= 0.5) {
      level = _lang({en: "moderate", fr: "modéré"});
    } else {
      level = _lang({en: "low", fr: "faible"});
    }
    
    const numValue = Number(originalValue) || 0;
    const formattedValue = numValue < 1 ? numValue.toFixed(3) : numValue.toFixed(1);
    return `${indicator.label}: ${formattedValue}${indicator.unit} (${level})`;
  });
    
  // Identify limiting factors based on the exact format specified
  const limitingFactors = [];
    
  // Check each indicator for limiting factors using normalized values
  indicators.forEach(indicator => {
    const normalizedValue = typeof indicator.normalizedValue === 'number' ? indicator.normalizedValue : 0;
    const originalValue = typeof indicator.originalValue === 'number' ? indicator.originalValue : 0;
    
    // Determine if this is a limiting factor (low performance)
    if (normalizedValue < 0.5) {
      const numValue = Number(originalValue) || 0;
      let formattedValue;
      if (indicator.originalKey === "pct_electric") {
        formattedValue = `${numValue.toFixed(1)}%`;
      } else if (indicator.originalKey === "min_to_cities") {
        formattedValue = `${numValue.toFixed(0)} min`;
      } else if (indicator.originalKey === "inet_d_kbps") {
        formattedValue = `${(numValue/1000).toFixed(1)} Mbps`;
      } else if (indicator.originalKey === "pct_piped_water") {
        formattedValue = `${numValue.toFixed(0)}%`;
      } else if (indicator.originalKey === "pct_cellphone") {
        formattedValue = `${numValue.toFixed(0)}%`;
      } else if (indicator.originalKey === "conflict_density") {
        formattedValue = `${(numValue * 1000).toFixed(1)} per 1000 km²`;
      } else {
        formattedValue = `${numValue.toFixed(1)}${indicator.unit}`;
      }
      
      limitingFactors.push(`${indicator.label} (${formattedValue})`);
    }
  });

  
  // Generate insight text in the exact format specified
  const limitationLevel = limitingFactors.length > 2 ? 
    _lang({en: "significantly limited", fr: "significativement limitée"}) :
    limitingFactors.length > 0 ? 
    _lang({en: "limited", fr: "limitée"}) :
    _lang({en: "strong", fr: "forte"});
    
  // Format factors as HTML list
  const factorsList = limitingFactors.length > 0 ? 
    limitingFactors.map(factor => htl.html`<li>${factor}</li>`) :
    [htl.html`<li>${_lang({en: "Strong infrastructure and services", fr: "Infrastructure et services solides"})}</li>`];
    
  const recommendation = limitingFactors.length > 0 ?
    _lang({en: "Addressing these can improve resilience.", fr: "Traiter ces problèmes peut améliorer la résilience."}) :
    _lang({en: "Continue strengthening these systems.", fr: "Continuer à renforcer ces systèmes."});
    
  const insight = htl.html`<div>
    <p>${_lang({
      en: `In ${region}, adaptive capacity is ${limitationLevel} by:`,
      fr: `En ${region}, la capacité d'adaptation est ${limitationLevel} par :`
    })}</p>
    <ul>${factorsList}</ul>
    <p>${recommendation}</p>
  </div>`;
    
  return createInsightDisplay(insight);

}

// About this notebook
aboutSection = htl.html`
  <div>
    <h3 style="margin: 0 0 16px 0; color: #2d3748;">
      ${_lang({ en: "About This Notebook", fr: "À propos de ce carnet" })}
    </h3>
    <p style="margin: 0 0 12px 0; line-height: 1.6;">
      ${_lang({
        en: "This vulnerability assessment notebook helps you understand climate vulnerability through three key dimensions: exposure to climate hazards, sensitivity of populations, and adaptive capacity of communities.",
        fr: "Ce carnet d'évaluation de la vulnérabilité vous aide à comprendre la vulnérabilité climatique à travers trois dimensions clés : l'exposition aux dangers climatiques, la sensibilité des populations et la capacité d'adaptation des communautés.",
      })}
    </p>
    <p style="margin: 0 0 12px 0; line-height: 1.6;">
      ${_lang({
        en: "The notebook uses interactive visualizations to explore vulnerability patterns across Sub-Saharan Africa, with data that can be filtered by administrative regions and demographic characteristics.",
        fr: "Le carnet utilise des visualisations interactives pour explorer les schémas de vulnérabilité à travers l'Afrique subsaharienne, avec des données qui peuvent être filtrées par régions administratives et caractéristiques démographiques.",
      })}
    </p>
    <p style="margin: 0; line-height: 1.6; font-size: 14px; color: #6b7280;">
      ${_lang({
        en: "For questions or feedback about this notebook, please contact the Adaptation Atlas team.",
        fr: "Pour des questions ou commentaires sur ce carnet, veuillez contacter l'équipe de l'Atlas d'adaptation.",
      })}
    </p>
  </div>
`;

Source code

function NavbarLangSelector(language_obj, masterLanguage) {
  let navEnd = document.querySelector(".navbar-nav.ms-auto .nav-item.compact");
  if (navEnd) {
    let existingLangSelector = document.getElementById("nav-lang-selector");
    if (!existingLangSelector) {
      let lang_sel = Inputs.bind(
        Inputs.radio(language_obj, {
          label: "",
          format: (d) => d.label
        }),
        viewof masterLanguage
      );
      lang_sel.id = "nav-lang-selector";

      // Style the language selector for navbar integration
      lang_sel.style.display = "flex";
      lang_sel.style.alignItems = "center";
      lang_sel.style.marginLeft = "10px";
      let lang_div = lang_sel.querySelector("div");
      lang_div.style.display = "flex";
      lang_div.style.flexDirection = "column";

      navEnd.parentNode.appendChild(lang_sel);
    }
  }
}

NavbarLangSelector(languages, masterLanguage)
appendix = _lang(general_translations.appendix)

// Master language selector
viewof masterLanguage = Inputs.radio(languages, {
  label: "Main language toggle",
  format: (d) => d.key,
  value: languages.find((x) => x.key === defaultLangKey),
})