Code
// Step 1: Import DuckDB properly for Observable (no httpfs)
DuckDB = {
const duckdb = await import("https://cdn.jsdelivr.net/npm/@duckdb/duckdb-wasm@latest/+esm");
const JSDELIVR_BUNDLES = duckdb.getJsDelivrBundles();
const bundle = await duckdb.selectBundle(JSDELIVR_BUNDLES);
const worker_url = URL.createObjectURL(
new Blob([`importScripts("${bundle.mainWorker}");`], { type: "text/javascript" })
);
const worker = new Worker(worker_url);
const logger = { log: console.log };
const db = new duckdb.AsyncDuckDB(logger, worker);
await db.instantiate(bundle.mainModule, bundle.pthreadWorker);
const conn = await db.connect();
// Only load spatial, skip httpfs
await conn.query("INSTALL spatial; LOAD spatial;");
console.log("DuckDB-WASM initialized.");
return { db, conn };
}Code
queryCache = new Map()
runQuery = async (query, params = {}) => {
const cacheKey = JSON.stringify({query, params});
if (queryCache.has(cacheKey)) {
console.log("Loading from cache:", cacheKey);
return queryCache.get(cacheKey);
}
let finalQuery = query;
Object.entries(params).forEach(([key, value]) => {
finalQuery = finalQuery.replace(new RegExp(`\\{${key}\\}`, 'g'), value);
});
try {
const start = performance.now(); // Benchmark start
const result = await DuckDB.conn.query(finalQuery);
const end = performance.now(); // Benchmark end
console.log(`Query execution time: ${(end - start).toFixed(2)}ms`);
const data = result.toArray().map(row => Object.fromEntries(row));
queryCache.set(cacheKey, data);
console.log("Cached new data:", cacheKey);
return data;
} catch (error) {
console.error("Query error:", error);
return [];
}
}Code
// Step 3: Define file URLs
files = ({
exposure: `https://digital-atlas.s3.amazonaws.com/domain%3Dexposure/type%3Dcombined/source%3Dglw4%2Bspam2020v1r2_ssa/region%3Dssa/processing%3Daggregated/gaul24_adm0-1-2_exposure.parquet`,
hazard_historic: `https://digital-atlas.s3.amazonaws.com/domain=hazard_exposure/source=atlas_cmip6/region=ssa/processing=hazard-risk-exposure/variable={variable}/period={period}/model=historic/severity={severity}/interaction.parquet`,
hazard_ensemble: `https://digital-atlas.s3.amazonaws.com/domain=hazard_exposure/source=atlas_cmip6/region=ssa/processing=hazard-risk-exposure/variable={variable}/period={period}/model=ENSEMBLE/severity={severity}/interaction.parquet`,
boundaries_admin1: `https://digital-atlas.s3.amazonaws.com/domain=boundaries/type=admin/source=gaul2024/region=africa/processing=simplified/level=adm1/atlas_gaul24_a1_africa_simple-lowres.parquet`,
boundaries_admin2: `https://digital-atlas.s3.amazonaws.com/domain=boundaries/type=admin/source=gaul2024/region=africa/processing=simplified/level=adm2/atlas_gaul24_a2_africa_simple-lowres.parquet`
})Code
Code
ms_codes_data = d3.csv("https://raw.githubusercontent.com/AdaptationAtlas/hazards_prototype/main/metadata/SpamCodes.csv")
ms_codes_processed = ms_codes_data
.filter(d => d.compound === "no" && d.Code)
.map(d => ({
Code: d.Code.toLowerCase(),
Fullname: d.Fullname.replace(/ /g, "-"),
cereal: d.cereal === "TRUE",
legume: d.legume === "TRUE",
root_tuber: d.root_tuber === "TRUE"
}))
crops = ms_codes_processed.map(d => d.Fullname)
cereals = ms_codes_processed.filter(d => d.cereal).map(d => d.Fullname)
legumes = ms_codes_processed.filter(d => d.legume).map(d => d.Fullname)
root_tubers = ms_codes_processed.filter(d => d.root_tuber).map(d => d.Fullname)
livestock = [
"cattle-highland", "cattle-tropical", "goats-highland", "goats-tropical",
"pigs-highland", "pigs-tropical", "poultry-highland", "poultry-tropical",
"sheep-highland", "sheep-tropical"
]
all_commodities = [...crops, ...livestock]
commodity_groups = ({
crops: crops,
cereals: cereals,
legumes: legumes,
root_tubers: root_tubers,
livestock: livestock,
all: all_commodities
})Code
available_countries = runQuery(`SELECT DISTINCT admin0_name FROM read_parquet('${files.exposure}') ORDER BY admin0_name`)
available_commodities = runQuery(`SELECT DISTINCT crop FROM read_parquet('${files.exposure}') WHERE crop IS NOT NULL ORDER BY crop`)
admin1_boundaries = runQuery(`SELECT DISTINCT admin0_name, admin1_name FROM read_parquet('${files.boundaries_admin1}') WHERE admin1_name IS NOT NULL ORDER BY admin0_name, admin1_name`)
admin2_boundaries = runQuery(`SELECT DISTINCT admin0_name, admin1_name, admin2_name FROM read_parquet('${files.boundaries_admin2}') WHERE admin2_name IS NOT NULL ORDER BY admin0_name, admin1_name, admin2_name`)Code
Code
metadata = ({
available_countries: available_countries.map(d => d.admin0_name),
available_commodities: available_commodities.map(d => d.crop),
data_loading_complete: available_countries.length > 0 && available_commodities.length > 0,
commodity_groups: commodity_groups,
default_inputs: default_inputs
})
data_loading_complete = metadata.data_loading_completeCode
formatNumber = (number, options = {}) => {
return new Intl.NumberFormat('en-US', {
minimumFractionDigits: 0,
maximumFractionDigits: 2,
...options
}).format(number);
}
formatCurrency = (amount, currency = 'USD') => {
return new Intl.NumberFormat('en-US', {
style: 'currency',
currency: currency,
minimumFractionDigits: 0,
maximumFractionDigits: 0
}).format(amount);
}
formatPercentage = (value, decimals = 1) => {
return new Intl.NumberFormat('en-US', {
style: 'percent',
minimumFractionDigits: decimals,
maximumFractionDigits: decimals
}).format(value / 100);
}Code
convertToCSV = (data) => {
if (!Array.isArray(data) || data.length === 0) {
return '';
}
const headers = Object.keys(data[0]);
const csvContent = [
headers.join(','),
...data.map(row =>
headers.map(header => {
const value = row[header];
if (typeof value === 'string' && (value.includes(',') || value.includes('"'))) {
return `"${value.replace(/"/g, '""')}"`;
}
return value ?? '';
}).join(',')
)
].join('\n');
return csvContent;
}
downloadData = (data, filename, format = 'csv') => {
let content, mimeType, extension;
switch (format.toLowerCase()) {
case 'json':
content = JSON.stringify(data, null, 2);
mimeType = 'application/json';
extension = '.json';
break;
case 'csv':
default:
content = convertToCSV(data);
mimeType = 'text/csv';
extension = '.csv';
break;
}
if (!filename.endsWith(extension)) {
filename += extension;
}
const blob = new Blob([content], { type: mimeType });
const url = URL.createObjectURL(blob);
const link = document.createElement('a');
link.href = url;
link.download = filename;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
URL.revokeObjectURL(url);
}
// Update downloadButton to include format chooser via prompt
downloadButton = (data, filename, label = "Download") => {
const buttonId = `download-btn-${Math.random().toString(36).substr(2, 9)}`;
const buttonHTML = htl.html`
<button id="${buttonId}" class="atlas-download-btn" title="${label} ${filename}">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/>
<polyline points="7,10 12,15 17,10"/>
<line x1="12" y1="15" x2="12" y2="3"/>
</svg>
<span class="btn-text">${label}</span>
</button>
`;
setTimeout(() => {
const button = document.getElementById(buttonId);
if (button) {
button.addEventListener('click', () => {
try {
const format = prompt("Enter format (csv or json):", "csv").toLowerCase();
if (format === 'csv' || format === 'json') {
downloadData(data, filename, format);
} else {
alert("Invalid format. Please choose csv or json.");
}
} catch (error) {
console.error('Download failed:', error);
}
});
}
}, 100);
return buttonHTML;
}Code
// CSS styles
styles = htl.html`
<style>
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 'Ubuntu', 'Cantarell', 'Open Sans', 'Helvetica Neue', sans-serif;
line-height: 1.6;
color: #343a40;
background-color: #ffffff;
}
h1, h2, h3, h4, h5, h6 {
font-weight: 600;
line-height: 1.3;
margin-top: 2rem;
margin-bottom: 1rem;
}
h1 {
font-size: 2.5rem;
color: #1e3a8a;
border-bottom: 3px solid #007acc;
padding-bottom: 0.5rem;
}
h2 {
font-size: 2rem;
color: #1e40af;
}
h3 {
font-size: 1.5rem;
color: #2563eb;
font-weight: 500;
}
.hero-section {
background: linear-gradient(135deg, #1e3a8a 0%, #3b82f6 100%);
color: white;
padding: 3rem 2rem;
margin: -2rem -20px 3rem -20px;
text-align: center;
border-radius: 0 0 16px 16px;
}
.hero-title {
font-size: 3rem;
font-weight: 700;
margin-bottom: 1rem;
text-shadow: 2px 2px 4px rgba(0,0,0,0.3);
}
.hero-subtitle {
font-size: 1.25rem;
opacity: 0.95;
max-width: 800px;
margin: 0 auto;
}
.controls-section {
background: #f8f9fa;
border: 1px solid #e9ecef;
border-radius: 12px;
padding: 2rem;
margin: 2rem 0;
box-shadow: 0 2px 8px rgba(0,0,0,0.05);
}
.visualization-container {
background: white;
border: 1px solid #e9ecef;
border-radius: 8px;
padding: 1.5rem;
margin: 2rem 0;
box-shadow: 0 2px 8px rgba(0,0,0,0.05);
min-height: 400px;
position: relative;
}
.loading-message {
text-align: center;
padding: 40px;
color: #666;
font-style: italic;
}
.error-message {
background: #f8d7da;
color: #721c24;
border: 1px solid #f5c6cb;
border-radius: 6px;
padding: 1rem;
margin: 1rem 0;
}
.success-message {
background: #d4edda;
color: #155724;
border: 1px solid #c3e6cb;
border-radius: 6px;
padding: 1rem;
margin: 1rem 0;
}
.atlas-download-btn {
display: inline-flex;
align-items: center;
gap: 6px;
background: #007acc;
color: white;
border: none;
border-radius: 6px;
padding: 8px 16px;
font-size: 14px;
font-weight: 500;
cursor: pointer;
transition: all 0.2s ease;
text-decoration: none;
font-family: inherit;
margin: 4px;
}
.atlas-download-btn:hover {
background: #0056b3;
transform: translateY(-1px);
box-shadow: 0 4px 12px rgba(0, 122, 204, 0.3);
}
</style>
`Climate Hazard Exposure Analysis
Advanced Controls
Code
viewof data_loading_status = {
if (!data_loading_complete || !metadata || metadata.available_countries.length === 0 || metadata.available_commodities.length === 0) {
return htl.html`<div class="loading-message">Loading countries and commodities...</div>`;
}
return htl.html`<div class="success-message">Metadata loaded: ${metadata.available_countries.length} countries, ${metadata.available_commodities.length} commodities available</div>`;
}Code
viewof selected_admin0 = {
if (!metadata || metadata.available_countries.length === 0) {
return htl.html`<div class="loading-message">Loading countries...</div>`;
}
const container = html`<div>
<label for="country-select">Select Country (${metadata.available_countries.length} available):</label>
<select id="country-select" autocomplete="off">
${metadata.available_countries.map(c => html`<option value="${c}" ?selected=${c === (default_inputs.country || "Kenya")}>${c}</option>`)}
</select>
</div>`;
// Initialize Tom Select
new TomSelect(container.querySelector("#country-select"), {
create: false,
sortField: { field: "text", direction: "asc" },
dropdownParent: 'body',
maxItems: 1
});
// Keep value in sync
Object.defineProperty(container, "value", {
get: () => container.querySelector("#country-select").value
});
return container;
}Code
viewof selected_admin1 = {
if (!metadata || metadata.available_countries.length === 0 || !admin1_boundaries || admin1_boundaries.length === 0) {
return htl.html`<div class="loading-message">Loading Admin 1...</div>`; // Updated loading message
}
const country = selected_admin0 || default_inputs.country || "Kenya";
const admin1_options = admin1_boundaries
.filter(row => row.admin0_name === country && row.admin1_name)
.map(row => row.admin1_name);
const unique_admin1 = [...new Set(admin1_options)].sort();
const container = html`<div>
<label for="admin1-select">Select Admin 1 (${unique_admin1.length} available):</label>
<select id="admin1-select" autocomplete="off">
${unique_admin1.map(admin => html`<option value="${admin}" ?selected=${default_inputs.admin1 && default_inputs.admin1.includes(admin)}>${admin}</option>`)}
</select>
</div>`;
// Initialize Tom Select
new TomSelect(container.querySelector("#admin1-select"), {
create: false,
sortField: { field: "text", direction: "asc" },
dropdownParent: 'body',
maxItems: 5000 // Adjust the maxItems according to your needs
});
// Keep value in sync
Object.defineProperty(container, "value", {
get: () => Array.from(container.querySelector("#admin1-select").selectedOptions).map(option => option.value)
});
return container;
}Code
viewof selected_admin2 = {
if (!metadata || metadata.available_countries.length === 0 || !admin2_boundaries || admin2_boundaries.length === 0) {
return htl.html`<div class="loading-message">Loading Admin 2...</div>`;
}
const country = selected_admin0 || default_inputs.country || "Kenya";
const admin1_list = selected_admin1 || [];
const admin2_options = admin2_boundaries
.filter(row => row.admin0_name === country && row.admin2_name &&
(admin1_list.length === 0 || admin1_list.includes(row.admin1_name)))
.map(row => row.admin2_name);
const unique_admin2 = [...new Set(admin2_options)].sort();
const container = html`<div>
<label for="admin2-select">Select Admin 2 (${unique_admin2.length} available):</label>
<select id="admin2-select" autocomplete="off" multiple>
${unique_admin2.map(admin => html`<option value="${admin}">${admin}</option>`)}
</select>
</div>`;
// Initialize Tom Select (no default selection)
new TomSelect(container.querySelector("#admin2-select"), {
create: false,
sortField: { field: "text", direction: "asc" },
dropdownParent: 'body',
maxItems: 5000
});
// Keep value in sync
Object.defineProperty(container, "value", {
get: () => Array.from(container.querySelector("#admin2-select").selectedOptions).map(option => option.value)
});
return container;
}Code
viewof admin_level = {
const options = ["admin0_name", "admin1_name", "admin2_name"];
const container = html`<div>
<label for="admin-level-select">🗺️ Admin Level to Display:</label>
<select id="admin-level-select" autocomplete="off">
${options.map(option => html`<option value="${option}" ?selected=${option === "admin1_name"}>${option.replace("_name", "").replace("admin", "Admin ")}</option>`)}
</select>
</div>`;
// Initialize Tom Select
new TomSelect(container.querySelector("#admin-level-select"), {
create: false,
sortField: { field: "text", direction: "asc" },
dropdownParent: 'body',
maxItems: 1
});
// Keep value in sync
Object.defineProperty(container, "value", {
get: () => container.querySelector("#admin-level-select").value
});
return container;
}Code
viewof selected_commodities = {
if (!metadata || metadata.available_commodities.length === 0) {
return htl.html`<div class="loading-message">Loading commodities...</div>`;
}
const groupOptions = ["all", "crops", "cereals", "legumes", "root_tubers", "livestock"];
const individualOptions = metadata.available_commodities;
const allOptions = [...groupOptions, "---", ...individualOptions];
const container = html`<div>
<label for="commodities-select">Select Commodities (${metadata.available_commodities.length} available):</label>
<select id="commodities-select" autocomplete="off" multiple>
${allOptions.map(option => html`<option value="${option}" ?selected=${default_inputs.commodities && default_inputs.commodities.includes(option)} ${option === "---" ? 'disabled' : ''}>${option}</option>`)}
</select>
</div>`;
// Initialize Tom Select
const tomSelectInstance = new TomSelect(container.querySelector("#commodities-select"), {
create: false,
sortField: { field: "text", direction: "asc" },
dropdownParent: 'body',
maxItems: 5, // Adjust according to your needs
placeholder: 'Select commodities',
hideSelected: true
});
// Keep value in sync
Object.defineProperty(container, "value", {
get: () => Array.from(container.querySelector("#commodities-select").selectedOptions).map(option => option.value)
});
// Set initial selection for Tom Select
tomSelectInstance.setValue(default_inputs.commodities);
return container;
}Code
// REPLACE the whole analysis_options cell with this block
viewof analysis_options = {
const f = Inputs.form({
severity: Inputs.select(["moderate", "severe", "extreme"], {
label: "Hazard Severity:",
value: "severe"
}),
scenario1: Inputs.select(scen_x_time, {
label: "Scenario 1 (left):",
value: "historic"
}),
scenario2: Inputs.select(scen_x_time, {
label: "Scenario 2 (right):",
value: "ssp585_2041-2060"
}),
variable: Inputs.select(["vop_usd15", "vop_intld15"], {
label: "Exposure Variable:",
value: "vop_usd15"
}),
period: Inputs.select(["annual", "jagermeyr"], {
label: "Calculation Window:",
value: "annual"
})
});
// Apply grid styling (max 4 controls per row) — do not let this be the returned value
Object.assign(f.style, {
display: "grid",
gridTemplateColumns: "repeat(4, minmax(0, 1fr))",
gap: "1rem",
alignItems: "start"
});
// Return the form element so the cell renders the control (not the style object)
return f;
}Code
htl.html`
<div class="controls-section">
<h3>📋 Selection Status</h3>
${data_loading_status}
<div style="margin-top: 1rem; padding: 1rem; background: #f8f9fa; border-radius: 8px;">
<h4>Current Selections:</h4>
<p><strong>Country:</strong> ${selected_admin0 || "None selected"}</p>
<p><strong>Admin 1:</strong> ${selected_admin1?.length > 0 ? selected_admin1.join(", ") : "All areas (none specifically selected)"}</p>
<p><strong>Admin 2:</strong> ${selected_admin2?.length > 0 ? selected_admin2.join(", ") : "All areas (none specifically selected)"}</p>
<p><strong>Commodities:</strong> ${selected_commodities?.length > 0 ? selected_commodities.slice(0, 3).join(", ") + (selected_commodities.length > 3 ? "..." : "") : "None selected"}</p>
<p><strong>Display Level:</strong> ${admin_level.replace("_name", "").replace("admin", "Admin ")}</p>
</div>
</div>
`Overview
This notebook supports strategic decision-making for climate adaptation planning. It enables users to identify which crops and livestock are most exposed to climate hazards under various future scenarios. The tool is designed for grant applicants, program officers, and regional planners to prioritize investments and justify interventions based on risk exposure.
Code
notebookControlsAndMethods = html`<div style="font-family: sans-serif; line-height: 1.5;">
<h3 style="color:#003366; margin-top:0;">Notebook Controls & Essentials</h3>
<p>Description of controls and what they enable you to do:</p>
<ul>
<li>Compound vs non-compound hazards</li>
<li>Relative vs absolute comparisons</li>
<li>Side-by-side vs difference</li>
<li>Annual (all crops and livestock) vs GCCMI crop calendar (annual crops only?)</li>
<li>Scenarios: SSPs x Future Timeframe</li>
<li>Exposure variable (VoP IntD, VoP USD, Harvested Area)</li>
<li>Hazards (Fixed vs Variable)</li>
<li>Severity (Moderate, Severe, Extreme)</li>
<li>Commodities</li>
<li>Geographic selectors</li>
</ul>
<h3 style="color:#003366;">Methods & Data Sources</h3>
<p>
This analysis estimates the exposure of crops and livestock to climate hazards using value of production (VoP) as an economic risk indicator.
Production data is sourced from MapSPAM Add version for crops adjusted with FAOstat international prices (2015 International or US Dollars).
Livestock VoP comes from Herrero et al. (2013) and is adjusted to year using GLW4.
</p>
<p>
Hazard risks are calculated using climate datasets (CHIRPS, AgERA5, CMIP6 projections) and aligned with agricultural seasons via the GGCMI Phase 3 crop calendar.
The probability of hazards occurrence is estimated across years, with compound hazards identified where multiple risks (e.g., dry, heat, wet) coincide.
Add link to methods section for selected hazards.
</p>
<p>
The integration of hazard risk with VoP data quantifies the economic value of production exposed, providing insights for adaptation planning.
Full details on hazard definitions, include hazard classification thresholds and methodology can be found in the full Methods Section.
</p>
<h3 style="color:#003366;">Note on Static Exposure Data</h3>
<p>
The exposure data presented provides a static representation of agricultural land use and crop distribution.
While this means it does not account for real-time changes in farming practices—such as shifts in crop selection or adaptive strategies in response to seasonal variability—it still offers valuable insights.
By analyzing historical exposure patterns, we can identify regions and commodities that have consistently faced high climate risks, helping to inform long-term planning and investment decisions.
</p>
<p>
Despite its limitations, this dataset remains a powerful tool for understanding broad trends in hazard exposure.
It provides a foundation for identifying where adaptation strategies may be most needed and serves as a starting point for deeper analyses incorporating additional, dynamic data sources in the future.
</p>
</div>`Code
scenario_config = {
const parseScenario = (scenarioStr) => {
if (scenarioStr === "historic") {
return { scenario: "historic", timeframe: "historic" };
}
const [scenario, timeframe] = scenarioStr.split("_");
return { scenario, timeframe };
};
return {
scenario1: parseScenario(analysis_options.scenario1),
scenario2: parseScenario(analysis_options.scenario2),
unit: analysis_options.variable.split("_")[1] || "usd15",
variable: analysis_options.variable,
period: analysis_options.period
};
}Code
// In data_loaded, define paths correctly
data_loaded = {
if (!data_loading_complete || !selected_admin0 || !selected_commodities || selected_commodities.length === 0) {
return {
exposure: [],
historic: [],
ensemble: [],
boundaries: [],
loaded: false
};
}
const commodities_filter = selected_commodities.filter(c => c !== "---").join("','");
const admin1_filter = selected_admin1?.length > 0 ? `AND admin1_name IN ('${selected_admin1.join("','")}')` : '';
const admin2_filter = selected_admin2?.length > 0 ? `AND admin2_name IN ('${selected_admin2.join("','")}')` : '';
const scenario1 = scenario_config.scenario1;
const scenario2 = scenario_config.scenario2;
const severity = analysis_options.severity;
const variable = analysis_options.variable;
const period = analysis_options.period;
const admin_level_filter = admin_level === "admin2_name" ? files.boundaries_admin2 : files.boundaries_admin1;
const hazard_historic_path = files.hazard_historic.replace("{variable}", variable).replace("{period}", period).replace("{severity}", severity);
const hazard_ensemble_path = files.hazard_ensemble.replace("{variable}", variable).replace("{period}", period).replace("{severity}", severity);
const [exposure_array, hazard_s1, hazard_s2, boundaries_array] = await Promise.all([
runQuery(`
SELECT admin0_name, admin1_name, admin2_name, crop, value
FROM read_parquet('${files.exposure}')
WHERE admin0_name = '${selected_admin0}'
AND crop IN ('${commodities_filter}')
AND (tech = 'all' OR tech IS NULL)
${admin1_filter}
${admin2_filter}
`),
runQuery(`
SELECT admin0_name, admin1_name, admin2_name, crop, scenario, timeframe, hazard, value, 0 AS value_sd
FROM read_parquet('${scenario1.scenario === "historic" ? hazard_historic_path : hazard_ensemble_path}')
WHERE admin0_name = '${selected_admin0}'
AND crop IN ('${commodities_filter}')
AND scenario = '${scenario1.scenario}'
AND timeframe = '${scenario1.timeframe}'
AND severity = '${severity}'
AND period = '${period}'
${admin1_filter}
${admin2_filter}
`),
runQuery(`
SELECT admin0_name, admin1_name, admin2_name, crop, scenario, timeframe, hazard, value, COALESCE(value_sd, 0) AS value_sd
FROM read_parquet('${scenario2.scenario === "historic" ? hazard_historic_path : hazard_ensemble_path}')
WHERE admin0_name = '${selected_admin0}'
AND crop IN ('${commodities_filter}')
AND scenario = '${scenario2.scenario}'
AND timeframe = '${scenario2.timeframe}'
AND severity = '${severity}'
AND period = '${period}'
${admin1_filter}
${admin2_filter}
`),
runQuery(`
SELECT admin0_name, admin1_name, admin2_name, ST_AsText(geometry) AS wkt_geom
FROM read_parquet('${admin_level_filter}')
WHERE admin0_name = '${selected_admin0}'
${admin1_filter}
${admin2_filter}
`)
]);
return {
exposure: exposure_array,
historic: hazard_s1,
ensemble: hazard_s2,
boundaries: boundaries_array,
loaded: true
};
}Code
boundaries_ready = {
if (!data_loaded || !data_loaded.loaded || !data_loaded.boundaries || data_loaded.boundaries.length === 0) {
return { ready: false, data: [] };
}
return {
ready: true,
data: data_loaded.boundaries.map(d => ({
admin0_name: d.admin0_name,
admin1_name: d.admin1_name,
admin2_name: d.admin2_name,
wkt_geom: d.wkt_geom
}))
};
}Code
// Process and filter data based on user selections with modular processing
processed_data = {
try {
if (!data_loading_complete || !data_loaded.loaded) {
return {
exposure_filtered: [],
hazard_s1: [],
hazard_s2: [],
merged_data: [],
enhanced_merged_data: []
};
}
const exposure_data_array = data_loaded.exposure || [];
const hazard_s1_array = data_loaded.historic || [];
const hazard_s2_array = data_loaded.ensemble || [];
// Expand commodity groups based on user selection
const safe_selected_commodities = Array.isArray(selected_commodities) ? selected_commodities : [];
let selected_commodities_expanded = [];
// Check if user selected a group or individual commodities
if (safe_selected_commodities.includes("all")) {
selected_commodities_expanded = all_commodities;
} else if (safe_selected_commodities.includes("crops")) {
selected_commodities_expanded = [...new Set([...crops, ...safe_selected_commodities.filter(c => !["crops", "all"].includes(c))])];
} else if (safe_selected_commodities.includes("cereals")) {
selected_commodities_expanded = [...new Set([...cereals, ...safe_selected_commodities.filter(c => !["cereals", "all"].includes(c))])];
} else if (safe_selected_commodities.includes("legumes")) {
selected_commodities_expanded = [...new Set([...legumes, ...safe_selected_commodities.filter(c => !["legumes", "all"].includes(c))])];
} else if (safe_selected_commodities.includes("root_tubers")) {
selected_commodities_expanded = [...new Set([...root_tubers, ...safe_selected_commodities.filter(c => !["root_tubers", "all"].includes(c))])];
} else if (safe_selected_commodities.includes("livestock")) {
selected_commodities_expanded = [...new Set([...livestock, ...safe_selected_commodities.filter(c => !["livestock", "all"].includes(c))])];
} else {
selected_commodities_expanded = safe_selected_commodities;
}
// Ensure selected_admin0 is treated as an array
const admin0_array = selected_admin0 ? [selected_admin0] : [];
const exposure_filtered = exposure_data_array.filter(d => {
return admin0_array.includes(d.admin0_name) &&
selected_commodities_expanded.includes(d.crop) &&
(!selected_admin1 || selected_admin1.length === 0 || selected_admin1.includes(d.admin1_name)) &&
(!selected_admin2 || selected_admin2.length === 0 || selected_admin2.includes(d.admin2_name));
});
const hazard_s1 = hazard_s1_array;
const hazard_s2 = hazard_s2_array;
// Modular merge function
const merge_data = (exposure, s1, s2) => {
const exposure_map = new Map();
exposure.forEach(d => {
const key = `${d.admin0_name}|${d.admin1_name || ''}|${d.admin2_name || ''}|${d.crop}`;
exposure_map.set(key, isNaN(d.value) ? 0 : d.value || 0);
});
const s1_map = new Map();
s1.forEach(d => {
const key = `${d.admin0_name}|${d.admin1_name || ''}|${d.admin2_name || ''}|${d.crop}|${d.hazard}`;
s1_map.set(key, isNaN(d.value) ? 0 : d.value || 0);
});
const merged = s2.map(d2 => {
const key = `${d2.admin0_name}|${d2.admin1_name || ''}|${d2.admin2_name || ''}|${d2.crop}|${d2.hazard}`;
const exp_key = `${d2.admin0_name}|${d2.admin1_name || ''}|${d2.admin2_name || ''}|${d2.crop}`;
const value1 = isNaN(s1_map.get(key)) ? 0 : s1_map.get(key) || 0;
const value2 = isNaN(d2.value) ? 0 : d2.value || 0;
const value_tot = isNaN(exposure_map.get(exp_key)) ? 0 : exposure_map.get(exp_key) || 0;
const diff = value2 - value1;
return {
admin0_name: d2.admin0_name,
admin1_name: d2.admin1_name,
admin2_name: d2.admin2_name,
crop: d2.crop,
hazard: d2.hazard,
severity: d2.severity,
value1,
value2,
value_tot,
diff,
perc1: value_tot > 0 ? (value1 / value_tot) * 100 : 0,
perc2: value_tot > 0 ? (value2 / value_tot) * 100 : 0,
perc_diff: value_tot > 0 ? (diff / value_tot) * 100 : 0
};
});
return merged;
};
// Process compound hazards function
const process_compound_hazards = (merged_data) => {
const enhanced_data = [...merged_data];
// Group by location and crop
const locationGroups = d3.groups(merged_data,
d => `${d.admin0_name}|${d.admin1_name || ''}|${d.admin2_name || ''}|${d.crop}`
);
locationGroups.forEach(([key, records]) => {
const [admin0, admin1, admin2, crop] = key.split('|');
// Find base hazards
const dry_record = records.find(r => r.hazard === "dry");
const heat_record = records.find(r => r.hazard === "heat");
const wet_record = records.find(r => r.hazard === "wet");
const any_record = records.find(r => r.hazard === "any");
// Get values with defaults, coercing NaN to 0
const dry = dry_record || {value1: 0, value2: 0, value_tot: 0};
dry.value1 = isNaN(dry.value1) ? 0 : dry.value1;
dry.value2 = isNaN(dry.value2) ? 0 : dry.value2;
dry.value_tot = isNaN(dry.value_tot) ? 0 : dry.value_tot;
const heat = heat_record || {value1: 0, value2: 0, value_tot: 0};
heat.value1 = isNaN(heat.value1) ? 0 : heat.value1;
heat.value2 = isNaN(heat.value2) ? 0 : heat.value2;
heat.value_tot = isNaN(heat.value_tot) ? 0 : heat.value_tot;
const wet = wet_record || {value1: 0, value2: 0, value_tot: 0};
wet.value1 = isNaN(wet.value1) ? 0 : wet.value1;
wet.value2 = isNaN(wet.value2) ? 0 : wet.value2;
wet.value_tot = isNaN(wet.value_tot) ? 0 : wet.value_tot;
const any = any_record || {value1: 0, value2: 0, value_tot: 0};
any.value1 = isNaN(any.value1) ? 0 : any.value1;
any.value2 = isNaN(any.value2) ? 0 : any.value2;
any.value_tot = isNaN(any.value_tot) ? 0 : any.value_tot;
// Calculate total exposure for this location/crop using real summed values
const total_exposure = any.value_tot || d3.sum(records, r => isNaN(r.value2) ? 0 : r.value2);
// Calculate "no hazard"
const no_hazard_value1 = Math.max(0, total_exposure - any.value1);
const no_hazard_value2 = Math.max(0, total_exposure - any.value2);
enhanced_data.push({
admin0_name: admin0,
admin1_name: admin1,
admin2_name: admin2,
crop: crop,
hazard: "no hazard",
severity: analysis_options?.severity,
value1: no_hazard_value1,
value2: no_hazard_value2,
value_tot: total_exposure,
diff: no_hazard_value2 - no_hazard_value1,
perc1: total_exposure > 0 ? (no_hazard_value1 / total_exposure) * 100 : 0,
perc2: total_exposure > 0 ? (no_hazard_value2 / total_exposure) * 100 : 0,
perc_diff: total_exposure > 0 ? ((no_hazard_value2 - no_hazard_value1) / total_exposure) * 100 : 0
});
});
return enhanced_data;
};
const merged_data = merge_data(exposure_filtered, hazard_s1, hazard_s2);
const enhanced_merged_data = d3 ? process_compound_hazards(merged_data) : merged_data;
return {
exposure_filtered,
hazard_s1,
hazard_s2,
merged_data,
enhanced_merged_data
};
} catch (error) {
console.error("Error in processed_data:", error);
return {
exposure_filtered: [],
hazard_s1: [],
hazard_s2: [],
merged_data: [],
enhanced_merged_data: []
};
}
}Code
// Aggregate data for different analyses with error handling
aggregated_data = {
try {
if (!data_loading_complete) {
return {
by_hazard: [],
by_crop: [],
by_geography: [],
total_summary: {
total_value1: 0,
total_value2: 0,
total_diff: 0,
total_exposure: 0,
crop_count: 0,
hazard_count: 0
}
};
}
const data = analysis_options.show_compound ? processed_data.enhanced_merged_data : processed_data.merged_data;
if (!data || data.length === 0) {
console.warn("No merged data available for aggregation");
return {
by_hazard: [],
by_crop: [],
by_geography: [],
total_summary: {
total_value1: 0,
total_value2: 0,
total_diff: 0,
total_exposure: 0,
crop_count: 0,
hazard_count: 0
}
};
}
console.log("Aggregating data from", data.length, "records");
// Check if d3 is available
if (!d3 || !d3.rollups) {
console.warn("d3 not available, using manual aggregation");
// Manual aggregation as fallback
const aggregateBy = (data, groupKeyFn, keyName, valueFields = ['value1', 'value2', 'value_tot', 'diff']) => {
const groups = {};
data.forEach(d => {
const key = groupKeyFn(d);
if (!groups[key]) {
groups[key] = {
count: 0,
value1: 0,
value2: 0,
value_tot: 0,
diff: 0
};
}
groups[key].count++;
valueFields.forEach(field => {
groups[key][field] += isNaN(d[field]) ? 0 : (d[field] || 0);
});
});
return Object.entries(groups).map(([key, values]) => ({
[keyName]: key,
...values,
perc1: values.value_tot > 0 ? (values.value1 / values.value_tot) * 100 : 0,
perc2: values.value_tot > 0 ? (values.value2 / values.value_tot) * 100 : 0,
perc_diff: values.value_tot > 0 ? (values.diff / values.value_tot) * 100 : 0
}));
};
const by_hazard = aggregateBy(data, d => d.hazard, 'hazard');
const by_crop = aggregateBy(
data.filter(d => d.hazard === "any" || !d.hazard.includes("+")),
d => d.crop,
'crop'
).sort((a, b) => b.value2 - a.value2);
const by_geography = aggregateBy(
data.filter(d => d.hazard === "any" || !d.hazard.includes("+")),
d => d[admin_level] || d.admin0_name,
'region'
).sort((a, b) => b.value2 - a.value2);
return {
by_hazard,
by_crop,
by_geography,
total_summary: {
total_value1: data.reduce((sum, d) => sum + (isNaN(d.value1) ? 0 : d.value1 || 0), 0),
total_value2: data.reduce((sum, d) => sum + (isNaN(d.value2) ? 0 : d.value2 || 0), 0),
total_diff: data.reduce((sum, d) => sum + (isNaN(d.diff) ? 0 : d.diff || 0), 0),
total_exposure: data.reduce((sum, d) => sum + (isNaN(d.value_tot) ? 0 : d.value_tot || 0), 0),
crop_count: new Set(data.map(d => d.crop)).size,
hazard_count: new Set(data.map(d => d.hazard)).size
}
};
}
// Use d3 aggregation if available
const by_hazard = d3.rollups(
data,
group => ({
value1: d3.sum(group, d => isNaN(d.value1) ? 0 : d.value1),
value2: d3.sum(group, d => isNaN(d.value2) ? 0 : d.value2),
value_tot: d3.sum(group, d => isNaN(d.value_tot) ? 0 : d.value_tot),
diff: d3.sum(group, d => isNaN(d.diff) ? 0 : d.diff),
count: group.length
}),
d => d.hazard === "any" ? "any hazard" : d.hazard
).map(([hazard, values]) => ({
hazard,
...values,
perc1: values.value_tot > 0 ? (values.value1 / values.value_tot) * 100 : 0,
perc2: values.value_tot > 0 ? (values.value2 / values.value_tot) * 100 : 0,
perc_diff: values.value_tot > 0 ? (values.diff / values.value_tot) * 100 : 0
})).filter(d => !analysis_options.exclude_no_hazard || d.hazard !== "no hazard");
const by_crop = d3.rollups(
data.filter(d => (analysis_options.show_compound ? true : !d.hazard.includes("+")) && (analysis_options.exclude_no_hazard ? d.hazard !== "no hazard" : true)),
group => ({
value1: d3.sum(group, d => isNaN(d.value1) ? 0 : d.value1),
value2: d3.sum(group, d => isNaN(d.value2) ? 0 : d.value2),
value_tot: d3.sum(group, d => isNaN(d.value_tot) ? 0 : d.value_tot),
diff: d3.sum(group, d => isNaN(d.diff) ? 0 : d.diff),
count: group.length
}),
d => d.crop
).map(([crop, values]) => ({
crop,
...values,
perc1: values.value_tot > 0 ? (values.value1 / values.value_tot) * 100 : 0,
perc2: values.value_tot > 0 ? (values.value2 / values.value_tot) * 100 : 0,
perc_diff: values.value_tot > 0 ? (values.diff / values.value_tot) * 100 : 0
})).sort((a, b) => b.value2 - a.value2);
const by_geography = d3.rollups(
data.filter(d => (analysis_options.show_compound ? true : !d.hazard.includes("+")) && (analysis_options.exclude_no_hazard ? d.hazard !== "no hazard" : true)),
group => ({
value1: d3.sum(group, d => isNaN(d.value1) ? 0 : d.value1),
value2: d3.sum(group, d => isNaN(d.value2) ? 0 : d.value2),
value_tot: d3.sum(group, d => isNaN(d.value_tot) ? 0 : d.value_tot),
diff: d3.sum(group, d => isNaN(d.diff) ? 0 : d.diff),
count: group.length
}),
d => d[admin_level] || d.admin0_name
).map(([region, values]) => ({
region,
...values,
perc1: values.value_tot > 0 ? (values.value1 / values.value_tot) * 100 : 0,
perc2: values.value_tot > 0 ? (values.value2 / values.value_tot) * 100 : 0,
perc_diff: values.value_tot > 0 ? (values.diff / values.value_tot) * 100 : 0
})).sort((a, b) => b.value2 - a.value2);
const total_summary = {
total_value1: d3.sum(data, d => isNaN(d.value1) ? 0 : d.value1),
total_value2: d3.sum(data, d => isNaN(d.value2) ? 0 : d.value2),
total_diff: d3.sum(data, d => isNaN(d.diff) ? 0 : d.diff),
total_exposure: d3.sum(data, d => isNaN(d.value_tot) ? 0 : d.value_tot),
crop_count: new Set(data.map(d => d.crop)).size,
hazard_count: new Set(data.map(d => d.hazard)).size
};
console.log("Aggregation complete:", {
hazards: by_hazard.length,
crops: by_crop.length,
regions: by_geography.length
});
return {
by_hazard,
by_crop,
by_geography,
total_summary
};
} catch (error) {
console.error("Error in aggregated_data:", error);
return {
by_hazard: [],
by_crop: [],
by_geography: [],
total_summary: {
total_value1: 0,
total_value2: 0,
total_diff: 0,
total_exposure: 0,
crop_count: 0,
hazard_count: 0
}
};
}
}Code
// Reactive raster data based on selected commodities
raster_data_reactive = {
const default_crop = "maize";
const selected_crops = selected_commodities?.filter(c =>
!["all", "crops", "cereals", "legumes", "root_tubers", "livestock"].includes(c)
) || [];
const crop = selected_crops[0] || default_crop;
const url = `https://digital-atlas.s3.amazonaws.com/domain=exposure/type=raster/source=spam2020v1r2_ssa/region=ssa/crop=${crop}/exposure.tif`;
console.log("Raster configuration initialized");
console.log("Selected/Default crop:", crop);
console.log("Raster URL:", url);
return {
crop: crop,
url: url,
loaded: true,
message: "Raster URL ready for download or visualization"
};
}Q4 Where Are the Geographic Hotspots?
Code
viewof q4_controls = {
const f = Inputs.form({
q4_scenario: Inputs.select(
[scenario_config.scenario1.scenario, scenario_config.scenario2.scenario, "difference"],
{
label: "Scenario",
value: scenario_config.scenario1.scenario
}
),
relative: Inputs.toggle({
label: "Show % (relative values)",
value: false
}),
difference: Inputs.toggle({
label: "Show differences",
value: false
}),
compound: Inputs.toggle({
label: "Show compound hazards",
value: false
}),
simplify_compound: Inputs.toggle({
label: "Simplify compound hazards",
value: false
}),
n_geographies: Inputs.range(
[1, (boundaries_ready && boundaries_ready.data ? boundaries_ready.data.length : 10)],
{
label: "No. geographies to show",
value: (boundaries_ready && boundaries_ready.data ? boundaries_ready.data.length : 5),
step: 1
}
)
});
// Force grid row layout
Object.assign(f.style, {
display: "grid",
gridTemplateColumns: "repeat(4, minmax(0, 1fr))",
gap: "1rem",
alignItems: "start",
width: "100%"
});
return f;
}Code
q4_map_data = {
const SC = (typeof scenario_config !== 'undefined') ? scenario_config : {unit: "usd15", scenario1: {scenario: "historic", timeframe: "historic"}, scenario2: {scenario: "ssp585", timeframe: "2041-2060"}};
const PD = (typeof processed_data !== 'undefined') ? processed_data : {merged_data: [], enhanced_merged_data: []};
const BR = (typeof boundaries_ready !== 'undefined') ? boundaries_ready : {data: [], ready: false};
const AL = (typeof admin_level !== 'undefined') ? admin_level : "admin1_name";
const AO = (typeof q4_controls !== 'undefined') ? q4_controls : {q4_scenario: SC.scenario1.scenario, relative: false, difference: false};
const SHOW = (typeof show_visualizations !== 'undefined') ? show_visualizations : true;
if (!SHOW || !PD.merged_data || PD.merged_data.length === 0 || !BR.ready || BR.data.length === 0) {
console.warn("Q4 map data: No data available (merged_data or boundaries_ready empty)");
return [];
}
try {
const enhanced = PD.enhanced_merged_data || PD.merged_data;
// Aggregate total exposure per geography and scenario
const geo_agg = d3.rollups(
enhanced,
v => ({
total_exposure: d3.sum(v, d => AO.relative ? (isNaN(d.perc2) ? (isNaN(d.perc1) ? 0 : d.perc1) : d.perc2) : (isNaN(d.value2) ? (isNaN(d.value1) ? 0 : d.value1) : d.value2)),
scenario: v[0].scenario || (v[0].timeframe === SC.scenario1.timeframe ? SC.scenario1.scenario : SC.scenario2.scenario)
}),
d => d[AL]
).map(([geo, vals]) => ({
geography: geo,
...vals
}));
let map_data = geo_agg;
if (AO.q4_scenario === "difference") {
const s1_data = map_data.filter(d => d.scenario === SC.scenario1.scenario);
const s2_data = map_data.filter(d => d.scenario === SC.scenario2.scenario);
map_data = s2_data.map(s2 => {
const s1 = s1_data.find(s1 => s1.geography === s2.geography) || {total_exposure: 0};
return {
geography: s2.geography,
total_exposure: s2.total_exposure - s1.total_exposure,
scenario: "difference"
};
});
} else {
map_data = map_data.filter(d => d.scenario === AO.q4_scenario);
}
// Attach geometries using wellknown.parse
const map_data_with_geom = map_data.map(d => {
const b = BR.data.find(bd => bd[AL] === d.geography) || {};
const geom = b.wkt_geom ? wkt.parse(b.wkt_geom) : null;
return {
...d,
geometry: geom,
admin0_name: b.admin0_name || d.geography,
admin1_name: AL === "admin1_name" ? d.geography : b.admin1_name,
admin2_name: AL === "admin2_name" ? d.geography : b.admin2_name
};
}).filter(d => d.geometry && !isNaN(d.total_exposure));
console.log("Q4 map data prepared:", map_data_with_geom.length, "records");
return map_data_with_geom;
} catch (error) {
console.error("Error preparing Q4 map data:", error);
return [];
}
}Code
q4_map = {
const SC = (typeof scenario_config !== 'undefined') ? scenario_config : {unit: "usd15", scenario1: {scenario: "historic"}, scenario2: {scenario: "ssp585"}};
let MD = (typeof q4_map_data !== 'undefined' && q4_map_data !== null) ? await q4_map_data : [];
if (!Array.isArray(MD)) MD = [];
const AO = (typeof q4_controls !== 'undefined') ? q4_controls : {q4_scenario: SC.scenario1.scenario, relative: false, difference: false};
const SHOW = (typeof show_visualizations !== 'undefined') ? show_visualizations : true;
const BR = (typeof boundaries_ready !== 'undefined') ? boundaries_ready : {data: [], ready: false};
// Create container div
const container = htl.html`<div style="height: 500px; width: 100%; position: relative;"></div>`;
try {
// Load Leaflet CSS first
if (!document.querySelector('link[href*="leaflet.css"]')) {
const link = document.createElement('link');
link.rel = 'stylesheet';
link.href = 'https://unpkg.com/leaflet@1.9.4/dist/leaflet.css';
document.head.appendChild(link);
}
// Import Leaflet JS as a script tag instead of ES6 module
await new Promise((resolve, reject) => {
if (window.L) {
resolve();
} else {
const script = document.createElement('script');
script.src = 'https://unpkg.com/leaflet@1.9.4/dist/leaflet.js';
script.onload = resolve;
script.onerror = reject;
document.head.appendChild(script);
}
});
// Wait a bit for Leaflet to fully initialize
await new Promise(resolve => setTimeout(resolve, 100));
// Now L should be available globally
if (!window.L) {
throw new Error("Leaflet library failed to load");
}
// Initialize map (clear container first to avoid multiple maps)
container.innerHTML = '';
const map = L.map(container).setView([0, 0], 2);
// Add base layer
L.tileLayer('https://{s}.basemaps.cartocdn.com/light_all/{z}/{x}/{y}.png', {
attribution: '© <a href="https://carto.com/">CartoDB</a>'
}).addTo(map);
// Only add data layers if data is available
if (SHOW && MD.length > 0) {
// Prepare color scale
const valid_exposures = MD.map(d => d.total_exposure).filter(v => !isNaN(v));
const extent = valid_exposures.length > 0 ? d3.extent(valid_exposures) : [0, 1];
const getColor = d3.scaleSequential(d3.interpolateYlOrRd).domain(extent);
const plot_title = AO.q4_scenario === "difference"
? `Total Exposure Δ ${SC.scenario2.scenario} minus ${SC.scenario1.scenario} (${SC.unit})`
: `Total Exposure ${AO.q4_scenario} (${SC.unit})`;
// Add GeoJSON layer
const geoLayer = L.geoJSON(MD.filter(d => d.geometry).map(d => ({
type: "Feature",
geometry: d.geometry,
properties: d
})), {
style: feature => ({
fillColor: getColor(feature.properties.total_exposure),
fillOpacity: 0.7,
color: "#444",
weight: 1
}),
onEachFeature: (feature, layer) => {
const props = feature.properties;
const label = `${props.admin1_name || props.admin2_name || props.admin0_name} (${props.admin0_name}): ${Math.round(props.total_exposure)} ${SC.unit}`;
layer.bindTooltip(label);
layer.on('mouseover', function() {
this.setStyle({weight: 2, color: "#333", fillOpacity: 0.9});
});
layer.on('mouseout', function() {
this.setStyle({weight: 1, color: "#444", fillOpacity: 0.7});
});
}
}).addTo(map);
// Add legend
const legend = L.control({position: 'bottomright'});
legend.onAdd = function() {
const div = L.DomUtil.create('div', 'info legend');
div.style.background = 'white';
div.style.padding = '10px';
div.style.borderRadius = '5px';
div.style.boxShadow = '0 1px 3px rgba(0,0,0,0.2)';
div.innerHTML = `<strong>${plot_title}</strong><br>`;
const grades = [0, 0.25, 0.5, 0.75, 1].map(d => extent[0] + d * (extent[1] - extent[0]));
grades.forEach((grade, i) => {
div.innerHTML += `<i style="background:${getColor(grade)}; width: 18px; height: 18px; display: inline-block; margin-right: 8px;"></i> ${Math.round(grade)}<br>`;
});
return div;
};
legend.addTo(map);
// Fit to country bounds using all available boundaries for the country
if (BR.data && BR.data.length > 0) {
const countryGeoJSON = L.geoJSON(BR.data.filter(d => d.wkt_geom).map(d => ({
type: "Feature",
geometry: wkt.parse(d.wkt_geom)
})));
if (countryGeoJSON.getLayers().length > 0) {
setTimeout(() => {
map.fitBounds(countryGeoJSON.getBounds(), {
padding: [50, 50],
maxZoom: 5
});
map.invalidateSize(); // Force redraw
}, 100);
}
} else {
map.invalidateSize(); // Force redraw if no bounds
}
} else {
map.invalidateSize(); // Force redraw even if no data
}
} catch (error) {
console.error("Error creating Q4 map:", error);
container.innerHTML = `<div style="padding: 20px; text-align: center;">Error loading map: ${error.message}</div>`;
}
return container;
}Code
q4_bar_data = {
const SC = (typeof scenario_config !== 'undefined') ? scenario_config : {unit: "usd15", scenario1: {scenario: "historic"}, scenario2: {scenario: "ssp585"}};
const PD = (typeof processed_data !== 'undefined') ? processed_data : {merged_data: []};
const AO = (typeof q4_controls !== 'undefined') ? q4_controls : {difference: false, relative: false, compound: false, simplify_compound: false, n_geographies: 5};
const AL = (typeof admin_level !== 'undefined') ? admin_level : "admin1_name";
const SHOW = (typeof show_visualizations !== 'undefined') ? show_visualizations : true;
const compound_set_full = ["no hazard", "dry (only)", "heat (only)", "wet (only)", "dry+heat", "dry+wet", "heat+wet", "dry+heat+wet"];
const compound_set_simple = ["no hazard", "1 hazard", "2 hazards", "3 hazards"];
const solo_set = ["no hazard", "dry (any)", "heat (any)", "wet (any)", "any hazard"];
if (!SHOW || PD.merged_data.length === 0) {
return [];
}
try {
const enhanced = PD.enhanced_merged_data || PD.merged_data;
const haz_set = AO.compound ? (AO.simplify_compound ? compound_set_simple : compound_set_full) : solo_set;
const filtered = enhanced.filter(d => d.admin0_name !== "all" && haz_set.includes(d.hazard));
// Get top N geographies by total exposure
const top_geos = d3.rollups(filtered, g => d3.sum(g, d => d.value2 || 0), d => d[AL] || d.admin0_name)
.sort((a, b) => b[1] - a[1])
.slice(0, AO.n_geographies)
.map(d => d[0]);
const aggregated = d3.rollups(
filtered.filter(d => top_geos.includes(d[AL] || d.admin0_name)),
v => ({
value1: d3.sum(v, d => d.value1 || 0),
value2: d3.sum(v, d => d.value2 || 0),
perc1: d3.mean(v, d => d.perc1 || 0),
perc2: d3.mean(v, d => d.perc2 || 0),
diff: d3.sum(v, d => d.diff || (d.value2 - d.value1) || 0)
}),
d => d[AL] || d.admin0_name,
d => d.hazard
);
const plot_data = [];
aggregated.forEach(([geo, hazards]) => {
hazards.forEach(([hazard, vals]) => {
plot_data.push({ geography: geo, hazard, ...vals });
});
});
return plot_data;
} catch (error) {
console.error("Error preparing Q4 bar data:", error);
return [];
}
}Code
q4_bar_chart = {
const SC = (typeof scenario_config !== 'undefined') ? scenario_config : {unit: "usd15", scenario1: {scenario: "historic"}, scenario2: {scenario: "ssp585"}};
const BD = (typeof q4_bar_data !== 'undefined') ? q4_bar_data : [];
const AO = (typeof q4_controls !== 'undefined') ? q4_controls : {difference: false, relative: false, compound: false, simplify_compound: false};
const SHOW = (typeof show_visualizations !== 'undefined') ? show_visualizations : true;
const base_hues = { "dry": "#8dd3c7", "heat": "#ffffb3", "wet": "#bebada" };
const mix_hex = (col1, col2, p = 0.5) => d3.interpolateLab(col1, col2)(p);
const cols_fill = {
"no hazard": "#e9ecef",
"dry (only)": d3.color(base_hues.dry).brighter(0.5),
"heat (only)": d3.color(base_hues.heat).brighter(0.5),
"wet (only)": d3.color(base_hues.wet).brighter(0.5),
"dry+heat": mix_hex(base_hues.dry, base_hues.heat, 0.5),
"dry+wet": mix_hex(base_hues.dry, base_hues.wet, 0.5),
"heat+wet": mix_hex(base_hues.heat, base_hues.wet, 0.5),
"dry+heat+wet": mix_hex(mix_hex(base_hues.dry, base_hues.heat, 0.5), base_hues.wet, 0.67)
};
const cols_fill_simple = {
"no hazard": "#e9ecef",
"1 hazard": d3.color(base_hues.dry).brighter(0.7),
"2 hazards": d3.color(base_hues.dry).brighter(0.35),
"3 hazards": base_hues.dry
};
const cols_fill_solo = {
"no hazard": "#e9ecef",
"dry (any)": "#8dd3c7",
"heat (any)": "#ffffb3",
"wet (any)": "#bebada",
"any hazard": "#6c757d"
};
const haz_pal = AO.compound ? (AO.simplify_compound ? cols_fill_simple : cols_fill) : cols_fill_solo;
if (!SHOW || BD.length === 0) {
return htl.html`<div class="loading-message">⚠️ No bar chart data available.</div>`;
}
try {
const value_field = AO.difference ? "diff" : (AO.relative ? "perc2" : "value2");
let plot_title = AO.difference ? `${SC.scenario2.scenario} minus ${SC.scenario1.scenario}` : "Exposure by Geography and Hazard";
return Plot.plot({
title: plot_title,
width: 900,
height: Math.max(400, BD.length * 30),
marginLeft: 150,
x: {
label: AO.relative ? "Exposure (%)" : `Exposure (${SC.unit})`,
tickFormat: AO.relative ? (d => `${d}%`) : "~s"
},
y: { label: "Geography", domain: d3.sort(BD, d => -d[value_field]).map(d => d.geography) },
color: {
domain: Object.keys(haz_pal),
range: Object.values(haz_pal),
legend: true
},
marks: [
Plot.barX(BD, {
x: value_field,
y: "geography",
fill: "hazard",
sort: {y: "-x"},
tip: true
}),
Plot.text(BD, {
x: value_field,
y: "geography",
text: d => formatNumber(d[value_field]),
dx: 5,
fill: "black",
fontSize: 10
})
]
});
} catch (error) {
console.error("Error creating Q4 bar chart:", error);
return htl.html`<div class="error-message">❌ Error: ${error.message}</div>`;
}
}Code
q4_insights_data = {
const SC = (typeof scenario_config !== 'undefined') ? scenario_config : {unit: "usd15", scenario1: {scenario: "historic"}, scenario2: {scenario: "ssp585"}};
const PD = (typeof processed_data !== 'undefined') ? processed_data : {merged_data: []};
const AL = (typeof admin_level !== 'undefined') ? admin_level : "admin1_name";
const SHOW = (typeof show_visualizations !== 'undefined') ? show_visualizations : true;
const hazard_types = ["heat", "dry", "wet", "compound", "any hazard"];
if (!SHOW || PD.merged_data.length === 0) {
return { s1: [], s2: [], increases: [], decreases: [] };
}
try {
const enhanced = PD.enhanced_merged_data || PD.merged_data;
const filtered = enhanced;
// Helper functions
const getMostExposed = (data, hazard_filter, value_field, is_relative = false) => {
const haz_data = data.filter(hazard_filter);
if (haz_data.length === 0) return {geo: "N/A", value: 0, total: 0, perc: 0};
const agg = d3.rollups(haz_data, v => d3.sum(v, d => d[value_field] || 0), d => d[AL] || d.admin0_name);
const sorted = agg.sort((a, b) => b[1] - a[1]);
const top_geo = sorted[0][0];
const top_value = sorted[0][1];
const total = d3.sum(agg, d => d[1]);
const perc = (top_value / total) * 100;
return {geo: top_geo, value: top_value, total, perc};
};
const getChange = (data, hazard_filter, is_increase = true, is_relative = false) => {
const haz_data = data.filter(hazard_filter);
const agg = d3.rollups(haz_data, v => d3.sum(v, d => d.diff || (d.value2 - d.value1) || 0), d => d[AL] || d.admin0_name);
const sorted = agg.sort((a, b) => is_increase ? (b[1] - a[1]) : (a[1] - b[1]));
const top_geo = sorted[0][0];
const delta = sorted[0][1];
const s1_value = d3.sum(haz_data.filter(d => (d[AL] || d.admin0_name) === top_geo), d => d.value1 || 0);
const perc_change = s1_value > 0 ? (delta / s1_value) * 100 : 0;
return {geo: top_geo, delta: Math.abs(delta), perc_change: Math.abs(perc_change)};
};
// Generate insights for each hazard type
const s1 = hazard_types.map(h => {
let hazard_filter = d => d.hazard.toLowerCase().includes(h.toLowerCase()) && !d.hazard.includes("+") && d.hazard !== "no hazard" && d.hazard !== "any";
if (h === "compound") hazard_filter = d => d.hazard.includes("+");
if (h === "any hazard") hazard_filter = d => true;
return {
hazard: h.toUpperCase(),
abs_geo: getMostExposed(filtered, hazard_filter, "value1", false).geo,
abs_exp: getMostExposed(filtered, hazard_filter, "value1", false).value,
abs_total: getMostExposed(filtered, hazard_filter, "value1", false).total,
rel_geo: getMostExposed(filtered, hazard_filter, "value1", true).geo,
rel_perc: getMostExposed(filtered, hazard_filter, "value1", true).perc
};
});
const s2 = hazard_types.map(h => {
let hazard_filter = d => d.hazard.toLowerCase().includes(h.toLowerCase()) && !d.hazard.includes("+") && d.hazard !== "no hazard" && d.hazard !== "any";
if (h === "compound") hazard_filter = d => d.hazard.includes("+");
if (h === "any hazard") hazard_filter = d => true;
return {
hazard: h.toUpperCase(),
abs_geo: getMostExposed(filtered, hazard_filter, "value2", false).geo,
abs_exp: getMostExposed(filtered, hazard_filter, "value2", false).value,
abs_total: getMostExposed(filtered, hazard_filter, "value2", false).total,
rel_geo: getMostExposed(filtered, hazard_filter, "value2", true).geo,
rel_perc: getMostExposed(filtered, hazard_filter, "value2", true).perc
};
});
const increases = hazard_types.map(h => {
let hazard_filter = d => d.hazard.toLowerCase().includes(h.toLowerCase()) && !d.hazard.includes("+") && d.hazard !== "no hazard" && d.hazard !== "any";
if (h === "compound") hazard_filter = d => d.hazard.includes("+");
if (h === "any hazard") hazard_filter = d => true;
return {
hazard: h.toUpperCase(),
abs_geo: getChange(filtered, hazard_filter, true, false).geo,
abs_delta: getChange(filtered, hazard_filter, true, false).delta,
rel_geo: getChange(filtered, hazard_filter, true, true).geo,
rel_perc: getChange(filtered, hazard_filter, true, true).perc_change
};
});
const decreases = hazard_types.map(h => {
let hazard_filter = d => d.hazard.toLowerCase().includes(h.toLowerCase()) && !d.hazard.includes("+") && d.hazard !== "no hazard" && d.hazard !== "any";
if (h === "compound") hazard_filter = d => d.hazard.includes("+");
if (h === "any hazard") hazard_filter = d => true;
return {
hazard: h.toUpperCase(),
abs_geo: getChange(filtered, hazard_filter, false, false).geo,
abs_delta: getChange(filtered, hazard_filter, false, false).delta,
rel_geo: getChange(filtered, hazard_filter, false, true).geo,
rel_perc: getChange(filtered, hazard_filter, false, true).perc_change
};
});
return { s1, s2, increases, decreases };
} catch (error) {
console.error("Error preparing Q4 insights data:", error);
return { s1: [], s2: [], increases: [], decreases: [] };
}
}Code
q4_insights_tables = {
const SC = (typeof scenario_config !== 'undefined') ? scenario_config : {unit: "usd15", scenario1: {scenario: "historic"}, scenario2: {scenario: "ssp585"}};
const ID = (typeof q4_insights_data !== 'undefined') ? q4_insights_data : {s1: [], s2: [], increases: [], decreases: []};
const SHOW = (typeof show_visualizations !== 'undefined') ? show_visualizations : true;
if (!SHOW) {
return htl.html`<div class="loading-message">⚠️ No insights data available.</div>`;
}
const renderTable = (title, data, is_change = false, is_increase = true) => {
return htl.html`
<h3>${title}</h3>
<table style="width: 100%; border-collapse: collapse; table-layout: fixed;">
<thead>
<tr style="background: #f8f9fa;">
<th style="padding: 8px; text-align: left; border-bottom: 2px solid #dee2e6; width: 20%;">Hazard Type</th>
<th style="padding: 8px; text-align: left; border-bottom: 2px solid #dee2e6; width: 20%;">${is_change ? "Geography (Absolute)" : "Most Exposed Geography (Absolute)"}</th>
<th style="padding: 8px; text-align: left; border-bottom: 2px solid #dee2e6; width: 20%;">${is_change ? (is_increase ? "Increase" : "Decrease") + " in Exposure (Absolute)" : "Exposure (Absolute)"}</th>
<th style="padding: 8px; text-align: left; border-bottom: 2px solid #dee2e6; width: 20%;">${is_change ? "Geography (% Change)" : "Most Exposed Geography (Relative)"}</th>
<th style="padding: 8px; text-align: left; border-bottom: 2px solid #dee2e6; width: 20%;">${is_change ? (is_increase ? "Increase" : "Decrease") + " in Exposure (% Change)" : "Exposure (% of Total)"}</th>
</tr>
</thead>
<tbody>
${data.map(row => htl.html`<tr>
<td style="padding: 8px; border-bottom: 1px solid #dee2e6;"><strong>${row.hazard}</strong></td>
<td style="padding: 8px; border-bottom: 1px solid #dee2e6;">${is_change ? row.abs_geo : row.abs_geo}</td>
<td style="padding: 8px; border-bottom: 1px solid #dee2e6;">${is_change ? formatNumber(row.abs_delta) : formatNumber(row.abs_exp)} / ${is_change ? "" : formatNumber(row.abs_total)} ${SC.unit}</td>
<td style="padding: 8px; border-bottom: 1px solid #dee2e6;">${is_change ? row.rel_geo : row.rel_geo}</td>
<td style="padding: 8px; border-bottom: 1px solid #dee2e6;">${is_change ? formatPercentage(row.rel_perc) : formatPercentage(row.rel_perc)}%</td>
</tr>`)}
</tbody>
</table>
`;
};
return htl.html`
${renderTable(SC.scenario1.scenario, ID.s1)}
${renderTable(SC.scenario2.scenario, ID.s2)}
${renderTable("Increase in total exposure", ID.increases, true, true)}
${renderTable("Decrease in total exposure", ID.decreases, true, false)}
`;
}Code
// Q4 Full Section Assembly
// This cell assembles the entire Q4 section: description, map, bar chart description, bar chart, and insights tables.
// It uses htl.html to structure the content with flex displays and styles matching previous sections.
htl.html`
<div class="visualization-container">
<p>Use this interactive map to explore total exposure to climate hazards across the areas and commodities you have selected. The map helps highlight which locations experience the highest overall exposure values and can guide your selection of admin 1 or admin 2 units for further analysis.</p>
<p>The color scale shows total exposure in ${scenario_config.unit || "usd15"} under the selected scenario. Darker colors indicate lower exposure, while lighter colors highlight hotspots of higher exposure.</p>
<div style="display: flex; justify-content: space-around; margin-bottom: 1rem;">
${q4_controls}
</div>
${q4_map}
<p>To complement the map above, the bar chart below shows how different geographies compare in terms of exposure to specific hazard types. While the map highlights overall exposure patterns spatially, this chart breaks that exposure down by hazard (e.g. heat, dry, wet) for each geography.</p>
<p>This makes it easier to identify which locations are most affected by which hazard types, and how these change between scenarios. Use it alongside the map to spot both hotspots of total exposure and the dominant hazard drivers in each area.</p>
${q4_bar_chart}
<div class="insights-section" style="margin-top: 2rem;">
<h3>💡 Dynamic Insights</h3>
${q4_insights_tables}
</div>
</div>
`Q1 How Does Total Exposure Break Down by Hazard Type?
Code
exposureDescription = html`<div style="white-space: pre-wrap; font-family: sans-serif;">
This section shows how total exposure to climate hazards is distributed across individual and compound hazard types (e.g. heat only, or heat + dry), for both the selected baseline and future scenario.
• Bars show total exposure (e.g. in USD or hectares).
• The matrix below each bar shows which hazards are included.
• Dots indicate exposure to combinations (e.g. heat + dry = compound hazard).
What this plot tells you:
• Whether exposure is changnig under future scenarios.
• Which hazards — individually or in combination — are most important.
• If compound hazard exposure is becoming more significant.
What you can prioritize:
• Identify hazard combinations that contribute most to overall exposure.
</div>`Code
// New cell: Compute Q1 data once (aggregated by hazard, with options applied)
q1_data = {
const AO = (typeof q1_options !== 'undefined') ? q1_options : {
show_compound: false, simplify_compound: false, exclude_no_hazard: false, show_difference: false, show_percentages: false
};
const PD = (typeof processed_data !== 'undefined') ? processed_data : {};
const SHOW = (typeof show_visualizations !== 'undefined') ? show_visualizations : true;
if (!SHOW) return {data: [], global_total_vop: 0};
const compound_set_full = ["no hazard", "dry (only)", "heat (only)", "wet (only)", "dry+heat", "dry+wet", "heat+wet", "dry+heat+wet"];
const compound_set_simple = ["no hazard", "1 hazard", "2 hazards", "3 hazards"];
const solo_set = ["no hazard", "dry (any)", "heat (any)", "wet (any)", "any hazard"];
const enhanced = PD.enhanced_merged_data || PD.merged_data;
if (!enhanced || enhanced.length === 0) return {data: [], global_total_vop: 0};
// Compute global_total_vop = sum of value_tot over unique locations (admin0/1/2/crop)
const location_key = d => `${d.admin0_name}|${d.admin1_name || ''}|${d.admin2_name || ''}|${d.crop}`;
const global_total_vop = d3.sum(
d3.rollups(enhanced, v => v[0].value_tot || 0, location_key),
([key, value_tot]) => value_tot
);
const haz_set = AO.show_compound ? (AO.simplify_compound ? compound_set_simple : compound_set_full) : solo_set;
// Aggregate raw data by hazard
let agg_data = d3.rollups(
enhanced,
v => ({
value1: d3.sum(v, d => d.value1 || 0),
value2: d3.sum(v, d => d.value2 || 0),
diff: d3.sum(v, d => d.diff ?? (d.value2 - d.value1)),
dry: v[0].hazard.includes("dry"),
heat: v[0].hazard.includes("heat"),
wet: v[0].hazard.includes("wet")
}),
d => d.hazard
).map(([hazard, vals]) => ({hazard, ...vals}));
let data = agg_data;
if (!AO.show_compound) {
// Group to solo_set
const grouped = {};
solo_set.forEach(s => grouped[s] = {value1:0, value2:0, diff:0, dry: s.includes("dry"), heat: s.includes("heat"), wet: s.includes("wet")});
agg_data.forEach(d => {
if (d.hazard === "no hazard") {
grouped["no hazard"] = d;
} else {
if (d.dry) {
grouped["dry (any)"].value1 += d.value1;
grouped["dry (any)"].value2 += d.value2;
grouped["dry (any)"].diff += d.diff;
}
if (d.heat) {
grouped["heat (any)"].value1 += d.value1;
grouped["heat (any)"].value2 += d.value2;
grouped["heat (any)"].diff += d.diff;
}
if (d.wet) {
grouped["wet (any)"].value1 += d.value1;
grouped["wet (any)"].value2 += d.value2;
grouped["wet (any)"].diff += d.diff;
}
grouped["any hazard"].value1 += d.value1;
grouped["any hazard"].value2 += d.value2;
grouped["any hazard"].diff += d.diff;
}
});
data = Object.entries(grouped).map(([hazard, vals]) => ({hazard, ...vals}));
} else if (AO.simplify_compound) {
// Group to simple count-based
data = d3.rollups(agg_data, v => v[0], d => {
if (d.hazard === "no hazard") return "no hazard";
const count = (d.hazard.match(/\+/g) || []).length + 1;
return count + " hazard" + (count > 1 ? "s" : "");
}).map(([hazard, vals]) => vals[0]);
}
data = data.filter(d => haz_set.includes(d.hazard) && (!AO.exclude_no_hazard || d.hazard !== "no hazard"));
// Assign correct percentages based on global_total_vop
data.forEach(d => {
d.perc1 = global_total_vop > 0 ? (d.value1 / global_total_vop) * 100 : 0;
d.perc2 = global_total_vop > 0 ? (d.value2 / global_total_vop) * 100 : 0;
d.perc_diff = global_total_vop > 0 ? (d.diff / global_total_vop) * 100 : 0;
});
return {data, global_total_vop};
}Code
// REPLACE the whole q1_options cell with this
viewof q1_options = {
const form = Inputs.form({
show_percentages: Inputs.toggle({
label: "Show as Percentages",
value: false
}),
show_difference: Inputs.toggle({
label: "Show Differences",
value: false
}),
show_compound: Inputs.toggle({
label: "Show Compound Hazards",
value: false
})
});
// Force row layout: max 4 controls per row
Object.assign(form.style, {
display: "grid",
gridTemplateColumns: "repeat(4, minmax(0, 1fr))",
gap: "1rem",
alignItems: "start"
});
return form;
}Code
q1_upset_plot = {
const SC = (typeof scenario_config !== 'undefined') ? scenario_config : {unit:"units"};
const AO = (typeof q1_options !== 'undefined') ? q1_options : {
show_compound: false, simplify_compound: false, exclude_no_hazard: false, show_difference: false, show_percentages: false
};
const Q1D = (typeof q1_data !== 'undefined') ? q1_data : {data: [], global_total_vop: 0};
const DATA = Q1D.data;
const SHOW = (typeof show_visualizations !== 'undefined') ? show_visualizations : true;
const fmtNumber = (typeof formatNumber === 'function') ? formatNumber : (x => {
if (x == null || !isFinite(x)) return "0";
try { return d3.format(",.2~r")(x); } catch(e) { return String(x); }
});
if (!SHOW || DATA.length === 0) {
return htl.html`<div class="loading-message">⚠️ No data available. Please click 'Load Data' to fetch data.</div>`;
}
// Inline definitions (kept)
const mix_hex = (col1, col2, p = 0.5) => d3.interpolateLab(col1, col2)(p);
const base_hues = { "dry": "#8dd3c7", "heat": "#ffffb3", "wet": "#bebada" };
const cols_fill = {
"no hazard": "#e9ecef",
"dry (only)": d3.color(base_hues.dry).brighter(0.5),
"heat (only)": d3.color(base_hues.heat).brighter(0.5),
"wet (only)": d3.color(base_hues.wet).brighter(0.5),
"dry+heat": mix_hex(base_hues.dry, base_hues.heat, 0.5),
"dry+wet": mix_hex(base_hues.dry, base_hues.wet, 0.5),
"heat+wet": mix_hex(base_hues.heat, base_hues.wet, 0.5),
"dry+heat+wet": mix_hex(mix_hex(base_hues.dry, base_hues.heat, 0.5), base_hues.wet, 0.67)
};
const cols_fill_simple = {
"no hazard": "#e9ecef",
"1 hazard": d3.color(base_hues.dry).brighter(0.7),
"2 hazards": d3.color(base_hues.dry).brighter(0.35),
"3 hazards": base_hues.dry
};
const cols_fill_solo = {
"no hazard": "#e9ecef",
"dry (any)": "#8dd3c7",
"heat (any)": "#ffffb3",
"wet (any)": "#bebada",
"any hazard": "#6c757d"
};
const value_field = AO.show_difference ? "diff" : (AO.show_percentages ? "perc2" : "value2");
const value_field1 = AO.show_percentages ? "perc1" : "value1";
const value_field2 = AO.show_percentages ? "perc2" : "value2";
const width = 400, height = 500, margin = {top: 100, right: 50, bottom: 50, left: 200};
if (AO.show_difference) {
const differenceChart = Plot.plot({
width: width * 2,
height: height - margin.top,
marginLeft: margin.left,
y: { label: AO.show_percentages ? "Exposure Difference (%)" : `Exposure Difference (${SC.unit || "units"})`, tickFormat: AO.show_percentages ? (d => `${d}%`) : "~s" },
marks: [
Plot.barY(DATA.filter(d => d.hazard !== "no hazard"), {
x: "hazard",
y: value_field,
fill: d => cols_fill[d.hazard] || "#666",
sort: {x: "-y"},
tip: true
}),
Plot.text(DATA.filter(d => d.hazard !== "no hazard"), {
x: "hazard",
y: value_field,
text: d => fmtNumber(d[value_field]),
dy: -5
}),
Plot.dot(DATA.filter(d => d.dry), { x: "hazard", y: -20, r: 5, fill: base_hues.dry }),
Plot.dot(DATA.filter(d => d.heat), { x: "hazard", y: -40, r: 5, fill: base_hues.heat }),
Plot.dot(DATA.filter(d => d.wet), { x: "hazard", y: -60, r: 5, fill: base_hues.wet }),
Plot.link(DATA.filter(d => d.dry && d.heat), {
x1: "hazard", y1: -20, x2: "hazard", y2: -40, stroke: "#333", strokeWidth: 2
})
]
});
return htl.html`
<div style="display: flex; justify-content: center;">
<div>${differenceChart}</div>
</div>
`;
} else {
// Two separate plots for scenario1 and scenario2
const chart1 = Plot.plot({
title: SC.scenario1.scenario,
width: width,
height: height - margin.top,
marginLeft: margin.left,
y: { label: AO.show_percentages ? "Exposure (%)" : `Exposure (${SC.unit || "units"})`, tickFormat: AO.show_percentages ? (d => `${d}%`) : "~s" },
marks: [
Plot.barY(DATA.filter(d => d.hazard !== "no hazard"), {
x: "hazard",
y: value_field1,
fill: d => cols_fill[d.hazard] || "#666",
sort: {x: "-y"},
tip: true
}),
Plot.text(DATA.filter(d => d.hazard !== "no hazard"), {
x: "hazard",
y: value_field1,
text: d => fmtNumber(d[value_field1]),
dy: -5
}),
Plot.dot(DATA.filter(d => d.dry), { x: "hazard", y: -20, r: 5, fill: base_hues.dry }),
Plot.dot(DATA.filter(d => d.heat), { x: "hazard", y: -40, r: 5, fill: base_hues.heat }),
Plot.dot(DATA.filter(d => d.wet), { x: "hazard", y: -60, r: 5, fill: base_hues.wet }),
Plot.link(DATA.filter(d => d.dry && d.heat), {
x1: "hazard", y1: -20, x2: "hazard", y2: -40, stroke: "#333", strokeWidth: 2
})
]
});
const chart2 = Plot.plot({
title: SC.scenario2.scenario,
width: width,
height: height - margin.top,
marginLeft: margin.left,
y: { label: AO.show_percentages ? "Exposure (%)" : `Exposure (${SC.unit || "units"})`, tickFormat: AO.show_percentages ? (d => `${d}%`) : "~s" },
marks: [
Plot.barY(DATA.filter(d => d.hazard !== "no hazard"), {
x: "hazard",
y: value_field2,
fill: d => cols_fill[d.hazard] || "#666",
sort: {x: "-y"},
tip: true
}),
Plot.text(DATA.filter(d => d.hazard !== "no hazard"), {
x: "hazard",
y: value_field2,
text: d => fmtNumber(d[value_field2]),
dy: -5
}),
Plot.dot(DATA.filter(d => d.dry), { x: "hazard", y: -20, r: 5, fill: base_hues.dry }),
Plot.dot(DATA.filter(d => d.heat), { x: "hazard", y: -40, r: 5, fill: base_hues.heat }),
Plot.dot(DATA.filter(d => d.wet), { x: "hazard", y: -60, r: 5, fill: base_hues.wet }),
Plot.link(DATA.filter(d => d.dry && d.heat), {
x1: "hazard", y1: -20, x2: "hazard", y2: -40, stroke: "#333", strokeWidth: 2
})
]
});
return htl.html`
<div style="display: flex; flex-direction: row; gap: 20px; justify-content: center;">
<div>${chart1}</div>
<div>${chart2}</div>
</div>
`;
}
}Code
htl.html`
<div class="visualization-container">
<div class="chart-title">Exposure Breakdown by Hazard Type (UpSet Plot)</div>
${q1_upset_plot}
<div style="margin-top: 2rem;">
<h4>Detailed Hazard Breakdown</h4>
${q1_insights_table}
</div>
${downloadButton(q1_data.data, "q1_hazard_breakdown.csv", "Download Q1 Data")}
</div>
`Code
// In q1_insights, change to q1_data.global_total_vop
q1_insights = {
const SC = (typeof scenario_config !== 'undefined') ? scenario_config : {unit:"units", scenario1:{scenario:"scenario1"}, scenario2:{scenario:"scenario2"}};
const Q1D = (typeof q1_data !== 'undefined') ? q1_data : {data: [], global_total_vop: 0};
const DATA = Q1D.data;
const global_total_vop = Q1D.global_total_vop;
const SHOW = (typeof show_visualizations !== 'undefined') ? show_visualizations : true;
if (!SHOW || DATA.length === 0) {
return {
total_value1: 0, total_value2: 0, total_vop: 0, percent_exposed1: 0, percent_exposed2: 0, total_change: 0, percent_change: 0,
dominant_hazard: "No data", is_increasing: false, dominant_value: 0,
compound_exposure1: 0, compound_exposure2: 0, compound_share1: 0,
compound_share2: 0, compound_change: 0, compound_percent_change: 0
};
}
try {
const total_value1 = d3.sum(DATA.filter(d => d.hazard !== "no hazard"), d => d.value1 || 0);
const total_value2 = d3.sum(DATA.filter(d => d.hazard !== "no hazard"), d => d.value2 || 0);
const change = total_value2 - total_value1;
const compound_exposure1 = d3.sum(DATA.filter(d => d.hazard && d.hazard.includes("+") && !d.hazard.includes("interaction")), d => d.value1 || 0);
const compound_exposure2 = d3.sum(DATA.filter(d => d.hazard && d.hazard.includes("+") && !d.hazard.includes("interaction")), d => d.value2 || 0);
const dominant = DATA
.filter(d => d.hazard !== "any" && d.hazard !== "no hazard" && !d.hazard.includes("interaction"))
.reduce((max, d) => (d.value2 || 0) > (max.value2 || 0) ? d : max, {value2: 0});
return {
total_value1,
total_value2,
total_vop: global_total_vop,
percent_exposed1: global_total_vop > 0 ? (total_value1 / global_total_vop) * 100 : 0,
percent_exposed2: global_total_vop > 0 ? (total_value2 / global_total_vop) * 100 : 0,
total_change: change,
percent_change: total_value1 > 0 ? (change / total_value1) * 100 : 0,
dominant_hazard: dominant?.hazard || "Unknown",
dominant_value: dominant?.value2 || 0,
is_increasing: change > 0,
compound_exposure1,
compound_exposure2,
compound_share1: total_value1 > 0 ? (compound_exposure1 / total_value1) * 100 : 0,
compound_share2: total_value2 > 0 ? (compound_exposure2 / total_value2) * 100 : 0,
compound_change: compound_exposure2 - compound_exposure1,
compound_percent_change: compound_exposure1 > 0 ? ((compound_exposure2 - compound_exposure1) / compound_exposure1) * 100 : 0
};
} catch (error) {
console.error("Error calculating Q1 insights:", error);
return {
total_value1: 0, total_value2: 0, total_vop: 0, percent_exposed1: 0, percent_exposed2: 0, total_change: 0, percent_change: 0,
dominant_hazard: "Error", is_increasing: false, dominant_value: 0,
compound_exposure1: 0, compound_exposure2: 0, compound_share1: 0,
compound_share2: 0, compound_change: 0, compound_percent_change: 0
};
}
}Code
q1_insights_table = {
const SC = (typeof scenario_config !== 'undefined') ? scenario_config : {scenario1:{scenario:"historic"}, scenario2:{scenario:"ssp585"}, unit:"usd15"};
const Q1D = (typeof q1_data !== 'undefined') ? q1_data : {data: []};
const DATA = Q1D.data;
const SHOW = (typeof show_visualizations !== 'undefined') ? show_visualizations : true;
const fmtNumber = (typeof formatNumber === 'function') ? formatNumber : (x => {
if (x == null || !isFinite(x)) return "0";
try {
let s = d3.format(",.2f")(x);
if (s.endsWith('.00')) s = s.slice(0, -3);
return s;
} catch(e) { return String(x); }
});
if (!SHOW || DATA.length === 0) {
return htl.html`<div class="loading-message">⚠️ No data available.</div>`;
}
try {
const hazardStats = DATA
.map(d => ({
hazard: d.hazard,
value1: d.value1 ?? 0,
value2: d.value2 ?? 0,
perc1: d.perc1 ?? 0,
perc2: d.perc2 ?? 0,
change: d.diff ?? ((d.value2 ?? 0) - (d.value1 ?? 0))
}))
.filter(d => d.value2 > 0 || d.value1 > 0);
return htl.html`
<table style="width: 850px; border-collapse: collapse; table-layout: fixed;">
<thead>
<tr style="background: #f8f9fa;">
<th style="padding: 8px; text-align: left; border-bottom: 2px solid #dee2e6; width: 200px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;">Hazard Type</th>
<th style="padding: 8px; text-align: right; border-bottom: 2px solid #dee2e6; width: 140px; white-space: nowrap;">${SC.scenario1.scenario} (${SC.unit || "units"})</th>
<th style="padding: 8px; text-align: right; border-bottom: 2px solid #dee2e6; width: 80px; white-space: nowrap;">%</th>
<th style="padding: 8px; text-align: right; border-bottom: 2px solid #dee2e6; width: 140px; white-space: nowrap;">${SC.scenario2.scenario} (${SC.unit || "units"})</th>
<th style="padding: 8px; text-align: right; border-bottom: 2px solid #dee2e6; width: 80px; white-space: nowrap;">%</th>
<th style="padding: 8px; text-align: right; border-bottom: 2px solid #dee2e6; width: 210px; white-space: nowrap;">Change</th>
</tr>
</thead>
<tbody>
${hazardStats.map(d => htl.html`<tr>
<td style="padding: 8px; border-bottom: 1px solid #dee2e6; text-align: left; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;">${d.hazard}</td>
<td style="padding: 8px; border-bottom: 1px solid #dee2e6; text-align: right; white-space: nowrap;">${fmtNumber(d.value1)}</td>
<td style="padding: 8px; border-bottom: 1px solid #dee2e6; text-align: right; white-space: nowrap;">${d.perc1.toFixed(1)}%</td>
<td style="padding: 8px; border-bottom: 1px solid #dee2e6; text-align: right; white-space: nowrap;">${fmtNumber(d.value2)}</td>
<td style="padding: 8px; border-bottom: 1px solid #dee2e6; text-align: right; white-space: nowrap;">${d.perc2.toFixed(1)}%</td>
<td style="padding: 8px; border-bottom: 1px solid #dee2e6; text-align: right; white-space: nowrap;" class="${d.change > 0 ? 'trend-increasing' : 'trend-decreasing'}">
${d.change > 0 ? '↑' : '↓'} ${fmtNumber(Math.abs(d.change))}
</td>
</tr>`)}
</tbody>
</table>
`;
} catch (error) {
console.error("Error in q1_insights_table:", error);
return htl.html`<div class="error-message">❌ Error: ${error.message}</div>`;
}
}Code
htl.html`
<div class="insights-section">
<h3>💡 Key Insights</h3>
<p>For the selected commodities, total exposure under <strong>${(typeof scenario_config !== 'undefined' && scenario_config.scenario1) ? scenario_config.scenario1.scenario : "scenario1"}</strong> is <span class="dynamic-value">${(typeof formatNumber==='function'?formatNumber:(x=>d3.format(",.2~r")(x||0)))(q1_insights.total_value1)}</span> ${(typeof scenario_config!=='undefined' && scenario_config.unit) ? scenario_config.unit : "units"} (<span class="dynamic-value">${(typeof formatPercentage==='function'?formatPercentage:(x,d=1)=>`${(x||0).toFixed(d)}%`)(q1_insights.percent_exposed1, 1)}</span> of total production). Exposure to <strong>compound hazards</strong> (i.e., more than one hazard in the same location) accounts for <span class="dynamic-value">${(typeof formatNumber==='function'?formatNumber:(x=>d3.format(",.2~r")(x||0)))(q1_insights.compound_exposure1)}</span> ${(typeof scenario_config!=='undefined' && scenario_config.unit) ? scenario_config.unit : "units"} (<span class="dynamic-value">${(typeof formatPercentage==='function'?formatPercentage:(x,d=1)=>`${(x||0).toFixed(d)}%`)(q1_insights.compound_share1, 1)}</span> of total exposure).</p>
<p>Under <strong>${(typeof scenario_config !== 'undefined' && scenario_config.scenario2) ? scenario_config.scenario2.scenario : "scenario2"}</strong>, total exposure is <span class="dynamic-value">${(typeof formatNumber==='function'?formatNumber:(x=>d3.format(",.2~r")(x||0)))(q1_insights.total_value2)}</span> ${(typeof scenario_config!=='undefined' && scenario_config.unit) ? scenario_config.unit : "units"} (<span class="dynamic-value">${(typeof formatPercentage==='function'?formatPercentage:(x,d=1)=>`${(x||0).toFixed(d)}%`)(q1_insights.percent_exposed2, 1)}</span> of total production). Compound hazard exposure is <span class="dynamic-value">${(typeof formatNumber==='function'?formatNumber:(x=>d3.format(",.2~r")(x||0)))(q1_insights.compound_exposure2)}</span> ${(typeof scenario_config!=='undefined' && scenario_config.unit) ? scenario_config.unit : "units"} (<span class="dynamic-value">${(typeof formatPercentage==='function'?formatPercentage:(x,d=1)=>`${(x||0).toFixed(d)}%`)(q1_insights.compound_share2, 1)}</span> of total exposure).</p>
<p>Comparing <strong>${(typeof scenario_config !== 'undefined' && scenario_config.scenario1) ? scenario_config.scenario1.scenario : "scenario1"}</strong> to <strong>${(typeof scenario_config !== 'undefined' && scenario_config.scenario2) ? scenario_config.scenario2.scenario : "scenario2"}</strong>, total exposure <span class="${q1_insights.is_increasing ? 'trend-increasing' : 'trend-decreasing'}">${q1_insights.is_increasing ? 'increases' : 'decreases'}</span> by <span class="dynamic-value">${(typeof formatNumber==='function'?formatNumber:(x=>d3.format(",.2~r")(x||0)))(Math.abs(q1_insights.total_change))}</span> ${(typeof scenario_config!=='undefined' && scenario_config.unit) ? scenario_config.unit : "units"} (<span class="dynamic-value">${(typeof formatPercentage==='function'?formatPercentage:(x,d=1)=>`${(x||0).toFixed(d)}%`)(Math.abs(q1_insights.percent_change), 1)}</span>). Exposure to compound hazards <span class="${q1_insights.compound_change > 0 ? 'trend-increasing' : 'trend-decreasing'}">${q1_insights.compound_change > 0 ? 'increases' : 'decreases'}</span> by <span class="dynamic-value">${(typeof formatNumber==='function'?formatNumber:(x=>d3.format(",.2~r")(x||0)))(Math.abs(q1_insights.compound_change))}</span> ${(typeof scenario_config!=='undefined' && scenario_config.unit) ? scenario_config.unit : "units"} (<span class="dynamic-value">${(typeof formatPercentage==='function'?formatPercentage:(x,d=1)=>`${(x||0).toFixed(d)}%`)(Math.abs(q1_insights.compound_percent_change), 1)}</span>).</p>
</div>
`Q2 Which Crops Are Most Exposed?
Code
cropExposureDescription = html`<div style="font-family: sans-serif; line-height: 1.5;">
<p>
This section is crop centric and helps you drill into the risk profile of each crop. These plots help you explore which commodities are most exposed to climate hazards, either:
</p>
<ul>
<li>Individually (e.g. just heat) — when <b>Show compound hazards</b> is unselected, or</li>
<li>In combination (e.g. heat + dry + wet) — when <b>Show compound hazards</b> is selected.</li>
</ul>
<p>You can switch between:</p>
<ul>
<li>Absolute values (<b>Show %</b> not selected) – these show the total exposure in real terms (e.g. USD), useful to understand the economic impact.</li>
<li>Relative values (<b>Show %</b> selected) – these show exposure as a percentage of total production per crop, helping you identify crops that are most vulnerable proportionally (even if they aren’t economically dominant).</li>
</ul>
<p><b>Use absolute plots</b> to prioritize which crops matter most economically.<br>
<b>Use relative plots</b> to spot hidden vulnerabilities in less valuable but highly exposed crops.
</p>
</div>`Code
viewof q2_options = {
const maxItems = selected_commodities?.length || 20;
const defaultValue = Math.min(10, maxItems);
const form = Inputs.form({
show_percentages: Inputs.toggle({
label: "Show as Percentages",
value: false
}),
show_difference: Inputs.toggle({
label: "Show Differences",
value: false
}),
show_compound: Inputs.toggle({
label: "Show Compound Hazards",
value: false
}),
simplify_compound: Inputs.toggle({
label: "Simplify Compound Hazards",
value: false
}),
n_top_items: Inputs.range([1, maxItems], {
step: 1,
label: "Top N Crops",
value: defaultValue
})
});
Object.assign(form.style, {
display: "grid",
gridTemplateColumns: "repeat(3, minmax(0, 1fr))",
gap: "1rem",
alignItems: "start"
});
return form;
}Code
q2_enhanced_chart = {
const SC = (typeof scenario_config !== 'undefined') ? scenario_config : {unit:"units", scenario1:{scenario:"scenario1"}, scenario2:{scenario:"scenario2"}};
const AO = (typeof q2_options !== 'undefined') ? q2_options : {
show_compound: false, simplify_compound: false, exclude_no_hazard: false, n_top_items: 10,
show_difference: false, show_percentages: false
};
const PD = (typeof processed_data !== 'undefined') ? processed_data : {};
const SHOW = (typeof show_visualizations !== 'undefined') ? show_visualizations : true;
if (!SHOW) {
return htl.html`<div style="text-align: center; padding: 2rem; color: #666;">
Click "Show Analysis" above to view visualizations
</div>`;
}
// Inline definitions - Using clearer color schemes as per instructions (base_hues for hazards, brighter for clarity)
const compound_set_full = ["no hazard", "dry (only)", "heat (only)", "wet (only)", "dry+heat", "dry+wet", "heat+wet", "dry+heat+wet"];
const compound_set_simple = ["no hazard", "1 hazard", "2 hazards", "3 hazards"];
const solo_set = ["no hazard", "dry (any)", "heat (any)", "wet (any)", "any hazard"];
const mix_hex = (col1, col2, p = 0.5) => d3.interpolateLab(col1, col2)(p);
const base_hues = { "dry": "#8dd3c7", "heat": "#ffffb3", "wet": "#bebada" }; // As specified
const cols_fill = {
"no hazard": "#e9ecef",
"dry (only)": d3.color(base_hues.dry).brighter(0.5),
"heat (only)": d3.color(base_hues.heat).brighter(0.5),
"wet (only)": d3.color(base_hues.wet).brighter(0.5),
"dry+heat": mix_hex(base_hues.dry, base_hues.heat, 0.5),
"dry+wet": mix_hex(base_hues.dry, base_hues.wet, 0.5),
"heat+wet": mix_hex(base_hues.heat, base_hues.wet, 0.5),
"dry+heat+wet": mix_hex(mix_hex(base_hues.dry, base_hues.heat, 0.5), base_hues.wet, 0.67)
};
const cols_fill_simple = {
"no hazard": "#e9ecef",
"1 hazard": d3.color(base_hues.dry).brighter(0.7),
"2 hazards": d3.color(base_hues.dry).brighter(0.35),
"3 hazards": base_hues.dry
};
const cols_fill_solo = {
"no hazard": "#e9ecef",
"dry (any)": "#8dd3c7",
"heat (any)": "#ffffb3",
"wet (any)": "#bebada",
"any hazard": "#6c757d"
};
try {
const enhanced = PD.enhanced_merged_data || PD.merged_data;
if (!(SHOW && (typeof window !== 'undefined' ? (window.current_data?.loaded ?? true) : true)) || !enhanced) {
return htl.html`<div class="loading-message">⚠️ No data available. Please click 'Load Data' to fetch data.</div>`;
}
const haz_set = AO.show_compound ? (AO.simplify_compound ? compound_set_simple : compound_set_full) : solo_set;
const filtered_data = enhanced.filter(d => haz_set.includes(d.hazard) && (!AO.exclude_no_hazard || d.hazard !== "no hazard"));
const top_crops = d3.rollups(filtered_data, g => d3.sum(g, d => d.value2 || 0), d => d.crop)
.sort((a, b) => b[1] - a[1])
.slice(0, AO.n_top_items || 10)
.map(d => d[0]);
// Compute global exposed for percentages (total exposed value1 and value2 across all)
const global_exposed1 = d3.sum(filtered_data, d => d.value1 || 0);
const global_exposed2 = d3.sum(filtered_data, d => d.value2 || 0);
const crop_hazard_data = d3.rollups(
filtered_data,
group => ({
value1: d3.sum(group, d => d.value1 || 0),
value2: d3.sum(group, d => d.value2 || 0),
diff: d3.sum(group, d => (d.diff != null ? d.diff : (d.value2||0)-(d.value1||0))),
perc1: global_exposed1 > 0 ? (d3.sum(group, d => d.value1 || 0) / global_exposed1 * 100) : 0,
perc2: global_exposed2 > 0 ? (d3.sum(group, d => d.value2 || 0) / global_exposed2 * 100) : 0
}),
d => top_crops.includes(d.crop) ? d.crop : "other",
d => d.hazard
);
const plot_data = [];
crop_hazard_data.forEach(([crop, hazards]) => {
hazards.forEach(([hazard, values]) => plot_data.push({ crop, hazard, ...values }));
});
if (AO.show_difference) {
return Plot.plot({
title: `Change in Crop Exposure: ${SC.scenario2?.scenario || "scenario2"} - ${SC.scenario1?.scenario || "scenario1"}`,
width: 900,
height: Math.max(400, top_crops.length * 60),
marginLeft: 150,
x: { label: `Change in Exposure (${SC.unit || "units"})`, tickFormat: "~s" },
y: { domain: top_crops, label: "Crop" },
color: {
domain: haz_set,
range: AO.show_compound ? (AO.simplify_compound ? Object.values(cols_fill_simple) : Object.values(cols_fill)) : Object.values(cols_fill_solo),
legend: true
},
marks: [
Plot.barX(plot_data, { x: "diff", y: "crop", fill: "hazard", sort: {y: "-x"}, tip: true }),
Plot.ruleX([0])
]
});
} else {
const s1_data = plot_data.map(d => ({...d, scenario: SC.scenario1?.scenario || "scenario1", value: d.value1, perc: d.perc1}));
const s2_data = plot_data.map(d => ({...d, scenario: SC.scenario2?.scenario || "scenario2", value: d.value2, perc: d.perc2}));
const combined_data = [...s1_data, ...s2_data];
const cropScenarioGroups = d3.groups(combined_data, d => d.crop, d => d.scenario);
return Plot.plot({
title: `Crop Exposure Comparison by Hazard Type`,
width: 1000,
height: Math.max(400, top_crops.length * 80),
marginLeft: 150,
marginBottom: 100,
x: {
label: AO.show_percentages ? "Exposure (%)" : `Exposure (${SC.unit || "units"})`,
tickFormat: AO.show_percentages ? (d => `${d}%`) : "~s"
},
y: { domain: top_crops, label: "Crop" },
fx: { domain: [SC.scenario1?.scenario || "scenario1", SC.scenario2?.scenario || "scenario2"], label: "Scenario" },
color: {
domain: haz_set,
range: AO.show_compound ? (AO.simplify_compound ? Object.values(cols_fill_simple) : Object.values(cols_fill)) : Object.values(cols_fill_solo),
legend: true
},
marks: [
Plot.barX(combined_data, {
x: AO.show_percentages ? "perc" : "value",
y: "crop",
fill: "hazard",
fx: "scenario",
sort: {y: "-x"},
tip: true
}),
Plot.text(
cropScenarioGroups.flatMap(([crop, scenarioGroups]) =>
scenarioGroups.map(([scenario, rows]) => {
const total = d3.sum(rows, r => r[AO.show_percentages ? "perc" : "value"] || 0);
const label = AO.show_percentages ? `${total.toFixed(1)}%` : (typeof formatNumber==='function'?formatNumber:(x=>d3.format(",.2~r")(x||0)))(total);
return {
crop, scenario, value: total,
label
};
})
),
{ x: "value", y: "crop", fx: "scenario", text: "label", dx: 5, fontSize: 10 }
)
]
});
}
} catch (error) {
console.error("Error creating Q2 enhanced chart:", error);
return htl.html`<div class="error-message">❌ Error: ${error.message}</div>`;
}
}Code
q2_insights_table = {
const SC = (typeof scenario_config !== 'undefined') ? scenario_config : {scenario1:{scenario:"scenario1"},scenario2:{scenario:"scenario2"}, unit:"units"};
const PD = (typeof processed_data !== 'undefined') ? processed_data : {};
const AG = (typeof aggregated_data !== 'undefined') ? aggregated_data : null;
const SHOW = (typeof show_visualizations !== 'undefined') ? show_visualizations : true;
const fmtNumber = (typeof formatNumber === 'function') ? formatNumber : (x => {
if (x == null || !isFinite(x)) return "0";
try { return d3.format(",.2~r")(x); } catch(e) { return String(x); }
});
if (!SHOW) {
return htl.html`<div style="text-align: center; padding: 2rem; color: #666;">
Click "Show Analysis" above to view visualizations
</div>`;
}
// Build by-crop aggregates if missing
const buildByCrop = (enh) => {
const data = Array.isArray(enh) ? enh : [];
const total1 = d3.sum(data, d => d.value1 || 0);
const total2 = d3.sum(data, d => d.value2 || 0);
const byCrop = d3.rollups(
data,
v => ({
value1: d3.sum(v, d => d.value1 || 0),
value2: d3.sum(v, d => d.value2 || 0),
diff: d3.sum(v, d => (d.diff != null ? d.diff : (d.value2||0)-(d.value1||0)))
}),
d => d.crop
).map(([crop, vals]) => ({
crop,
...vals,
perc_diff: (vals.value1 ? (vals.value2 - vals.value1) / vals.value1 * 100 : 0)
}));
return {
total_summary: { total_value1: total1, total_value2: total2 },
by_crop: byCrop
};
};
try {
const agg = AG && AG.by_crop ? AG : buildByCrop(PD.enhanced_merged_data || PD.merged_data || []);
if (!agg.by_crop || agg.by_crop.length === 0) {
return htl.html`<div>No data available</div>`;
}
const crop_changes = agg.by_crop
.map(d => ({ crop: d.crop, value1: d.value1, value2: d.value2, diff: d.diff, perc_change: d.perc_diff }))
.sort((a, b) => b.value2 - a.value2)
.slice(0, 5);
const crop_changes_diff = agg.by_crop
.map(d => ({ crop: d.crop, value1: d.value1, value2: d.value2, diff: d.diff, perc_change: d.perc_diff }))
.sort((a, b) => b.diff - a.diff)
.slice(0, 5);
// Helpers over enhanced data
const enhanced = PD.enhanced_merged_data || PD.merged_data || [];
const get_top_for_crop = (crop_data, is_compound) => {
const filtered = crop_data.filter(d => (d.hazard?.includes("+") === is_compound) && d.hazard !== "no hazard" && d.hazard !== "any hazard");
if (filtered.length === 0) return {hazard: "N/A", percent: 0};
const top = filtered.reduce((max, d) => (d.value2 || 0) > (max.value2 || 0) ? d : max, filtered[0]);
return { hazard: top.hazard, percent: (d3.sum(filtered, d => d.value2 || 0) > 0) ? ((top.value2 || 0) / d3.sum(filtered, d => d.value2 || 0) * 100) : 0 };
};
const get_top_change = (crop_data, is_compound) => {
const filtered = crop_data.filter(d => (d.hazard?.includes("+") === is_compound) && d.hazard !== "no hazard" && d.hazard !== "any hazard");
if (filtered.length === 0) return {hazard: "N/A", percent: 0};
const top = filtered.reduce((max, d) => ((d.diff != null ? d.diff : (d.value2||0)-(d.value1||0)) > (max._diff || 0)) ? {...d, _diff:(d.diff != null ? d.diff : (d.value2||0)-(d.value1||0))} : max, {_diff:-Infinity});
const sumd = d3.sum(filtered, d => (d.diff != null ? d.diff : (d.value2||0)-(d.value1||0)));
return { hazard: top.hazard || "N/A", percent: sumd !== 0 ? ((top._diff || 0) / sumd * 100) : 0 };
};
const s1_table = htl.html`
<table style="width: 100%; border-collapse: collapse; table-layout: fixed; margin-bottom: 1rem;">
<thead>
<tr style="background: #f8f9fa;">
<th style="padding: 8px; text-align: left; border-bottom: 2px solid #dee2e6; width: 20%;">Commodity</th>
<th style="padding: 8px; text-align: left; border-bottom: 2px solid #dee2e6; width: 20%;">Unit</th>
<th style="padding: 8px; text-align: left; border-bottom: 2px solid #dee2e6; width: 20%;">%</th>
<th style="padding: 8px; text-align: left; border-bottom: 2px solid #dee2e6; width: 20%;">Top Alone</th>
<th style="padding: 8px; text-align: left; border-bottom: 2px solid #dee2e6; width: 20%;">Top Compound</th>
</tr>
</thead>
<tbody>
${crop_changes.map(d => {
const crop_data = enhanced.filter(c => c.crop === d.crop);
const top_alone = get_top_for_crop(crop_data, false);
const top_compound = get_top_for_crop(crop_data, true);
return htl.html`<tr>
<td style="padding: 8px; border-bottom: 1px solid #dee2e6;">${(d.crop || "").replace(/-/g, " ")}</td>
<td style="padding: 8px; border-bottom: 1px solid #dee2e6;">${fmtNumber(d.value1)}</td>
<td style="padding: 8px; border-bottom: 1px solid #dee2e6;">${d.value1 > 0 ? (d.value1 / (agg.total_summary.total_value1 || 1) * 100).toFixed(1) : 0}%</td>
<td style="padding: 8px; border-bottom: 1px solid #dee2e6;">${top_alone.hazard} ${top_alone.percent.toFixed(1)}%</td>
<td style="padding: 8px; border-bottom: 1px solid #dee2e6;">${top_compound.hazard} ${top_compound.percent.toFixed(1)}%</td>
</tr>`;
})}
</tbody>
</table>
`;
const s2_table = htl.html`
<table style="width: 100%; border-collapse: collapse; table-layout: fixed; margin-bottom: 1rem;">
<thead>
<tr style="background: #f8f9fa;">
<th style="padding: 8px; text-align: left; border-bottom: 2px solid #dee2e6; width: 20%;">Commodity</th>
<th style="padding: 8px; text-align: left; border-bottom: 2px solid #dee2e6; width: 20%;">Unit</th>
<th style="padding: 8px; text-align: left; border-bottom: 2px solid #dee2e6; width: 20%;">%</th>
<th style="padding: 8px; text-align: left; border-bottom: 2px solid #dee2e6; width: 20%;">Top Alone</th>
<th style="padding: 8px; text-align: left; border-bottom: 2px solid #dee2e6; width: 20%;">Top Compound</th>
</tr>
</thead>
<tbody>
${crop_changes.map(d => {
const crop_data = enhanced.filter(c => c.crop === d.crop);
const top_alone = get_top_for_crop(crop_data, false);
const top_compound = get_top_for_crop(crop_data, true);
return htl.html`<tr>
<td style="padding: 8px; border-bottom: 1px solid #dee2e6;">${(d.crop || "").replace(/-/g, " ")}</td>
<td style="padding: 8px; border-bottom: 1px solid #dee2e6;">${fmtNumber(d.value2)}</td>
<td style="padding: 8px; border-bottom: 1px solid #dee2e6;">${d.value2 > 0 ? (d.value2 / (agg.total_summary.total_value2 || 1) * 100).toFixed(1) : 0}%</td>
<td style="padding: 8px; border-bottom: 1px solid #dee2e6;">${top_alone.hazard} ${top_alone.percent.toFixed(1)}%</td>
<td style="padding: 8px; border-bottom: 1px solid #dee2e6;">${top_compound.hazard} ${top_compound.percent.toFixed(1)}%</td>
</tr>`;})}
</tbody>
</table>
`;
const change_table = htl.html`
<table style="width: 100%; border-collapse: collapse; table-layout: fixed;">
<thead>
<tr style="background: #f8f9fa;">
<th style="padding: 8px; text-align: left; border-bottom: 2px solid #dee2e6; width: 20%;">Commodity</th>
<th style="padding: 8px; text-align: left; border-bottom: 2px solid #dee2e6; width: 20%;">Δ Unit</th>
<th style="padding: 8px; text-align: left; border-bottom: 2px solid #dee2e6; width: 20%;">Δ %</th>
<th style="padding: 8px; text-align: left; border-bottom: 2px solid #dee2e6; width: 20%;">Top Δ Alone</th>
<th style="padding: 8px; text-align: left; border-bottom: 2px solid #dee2e6; width: 20%;">Top Δ Compound</th>
</tr>
</thead>
<tbody>
${crop_changes_diff.map(d => {
const crop_data = enhanced.filter(c => c.crop === d.crop);
const top_alone_change = get_top_change(crop_data, false);
const top_compound_change = get_top_change(crop_data, true);
return htl.html`<tr>
<td style="padding: 8px; border-bottom: 1px solid #dee2e6;">${(d.crop || "").replace(/-/g, " ")}</td>
<td style="padding: 8px; border-bottom: 1px solid #dee2e6;">${fmtNumber(d.diff)}</td>
<td style="padding: 8px; border-bottom: 1px solid #dee2e6;">${(d.perc_change || 0).toFixed(1)}%</td>
<td style="padding: 8px; border-bottom: 1px solid #dee2e6;">${top_alone_change.hazard} ${top_alone_change.percent.toFixed(1)}%</td>
<td style="padding: 8px; border-bottom: 1px solid #dee2e6;">${top_compound_change.hazard} ${top_compound_change.percent.toFixed(1)}%</td>
</tr>`;})}
</tbody>
</table>
`;
return htl.html`
<h3>${SC.scenario1.scenario}</h3>
${s1_table}
<h3>${SC.scenario2.scenario}</h3>
${s2_table}
<h3>Changes</h3>
${change_table}
`;
} catch (error) {
console.error("Error creating Q2 insights table:", error);
return htl.html`<div class="error-message">Error: ${error.message}</div>`;
}
}Code
// Enhanced insights summary (kept, with fallbacks)
q2_enhanced_insights = {
const PD = (typeof processed_data !== 'undefined') ? processed_data : {};
const AG = (typeof aggregated_data !== 'undefined') ? aggregated_data : null;
const SHOW = (typeof show_visualizations !== 'undefined') ? show_visualizations : true;
if (!SHOW) {
return htl.html`<div style="text-align: center; padding: 2rem; color: #666;">
Click "Show Analysis" above to view visualizations
</div>`;
}
// Fallback by-crop aggregates
const buildByCrop = (enh) => {
const data = Array.isArray(enh) ? enh : [];
const byCrop = d3.rollups(
data,
v => ({
value1: d3.sum(v, d => d.value1 || 0),
value2: d3.sum(v, d => d.value2 || 0),
diff: d3.sum(v, d => (d.diff != null ? d.diff : (d.value2||0)-(d.value1||0))),
perc_diff: 0
}),
d => d.crop
).map(([crop, vals]) => ({
crop, ...vals, perc_change: (vals.value1 ? (vals.value2 - vals.value1) / vals.value1 * 100 : 0)
}));
return { by_crop: byCrop };
};
try {
const agg = AG && AG.by_crop ? AG : buildByCrop(PD.enhanced_merged_data || PD.merged_data || []);
if (!agg.by_crop || agg.by_crop.length === 0) {
return htl.html`<div>No data available</div>`;
}
const crop_changes = agg.by_crop
.map(d => ({ crop: d.crop, value1: d.value1, value2: d.value2, diff: d.diff, perc_change: d.perc_change }))
.sort((a, b) => b.diff - a.diff);
if (crop_changes.length === 0) {
return htl.html`<div>No crop change data available</div>`;
}
const fastest_growing = crop_changes[0];
const fastest_declining = crop_changes[crop_changes.length - 1];
const num_increasing = crop_changes.filter(d => d.diff > 0).length;
const num_decreasing = crop_changes.filter(d => d.diff < 0).length;
return htl.html`
<div style="background: linear-gradient(135deg, #e3f2fd 0%, #bbdefb 100%);
border-left: 4px solid #2196f3; padding: 1rem; margin: 1rem 0; border-radius: 4px;">
<h4 style="margin-top: 0; color: #1565c0;">📊 Crop Exposure Trends</h4>
<p>
<strong>Fastest Growing Risk:</strong>
<span class="dynamic-value">${(fastest_growing.crop || "").replace(/-/g, " ")}</span>
<span class="trend-increasing">↑ ${(fastest_growing.perc_change || 0).toFixed(1)}%</span>
</p>
<p>
<strong>Largest Risk Reduction:</strong>
<span class="dynamic-value">${(fastest_declining.crop || "").replace(/-/g, " ")}</span>
<span class="trend-decreasing">↓ ${Math.abs(fastest_declining.perc_change || 0).toFixed(1)}%</span>
</p>
<p>
<strong>Overall:</strong>
${num_increasing} crops showing increased exposure,
${num_decreasing} showing decreased exposure
</p>
</div>
`;
} catch (error) {
console.error("Error calculating enhanced insights:", error);
return htl.html`<div class="error-message">Error: ${error.message}</div>`;
}
}Code
htl.html`
<div class="visualization-container">
<div class="chart-title">Crop Exposure by Hazard Type</div>
${q2_enhanced_chart}
<div style="margin-top: 2rem;">
<h4>Detailed Crop Breakdown</h4>
${q2_insights_table}
</div>
<div style="margin-top: 1rem;">
${q2_enhanced_insights}
</div>
${downloadButton(q2_insights_table.data, "q2_crop_exposure.csv", "Download Q2Data")}
</div>
`Q3 Which Hazards Pose the Greatest Threat to Commodities?
Code
hazardCentricDescription = html`<div style="font-family: sans-serif; line-height: 1.5;">
<p>
Where the previous section is hazard-centric → helps you see which crops are driving exposure for each hazard.
</p>
<p>This view is useful when:</p>
<ul>
<li>You want to prioritize hazard-specific actions (e.g. drought resilience).</li>
<li>You’re interested in which crops dominate exposure to dry, heat, or compound hazards.</li>
</ul>
<p>
<b>Use the Show % toggle</b> to highlight crops that are proportionally vulnerable, even if their total exposure is small.<br>
<b>Use Show compound hazards</b> to explore how multi-hazard risks (e.g. dry + heat) shift across crops.
</p>
</div>`Code
viewof q3_options = {
const maxItems = selected_commodities?.length || 20;
const defaultValue = Math.min(10, maxItems);
const form = Inputs.form({
show_percentages: Inputs.toggle({
label: "Show as Percentages",
value: false
}),
show_difference: Inputs.toggle({
label: "Show Differences",
value: false
}),
show_compound: Inputs.toggle({
label: "Show Compound Hazards",
value: false
}),
simplify_compound: Inputs.toggle({
label: "Simplify Compound Hazards",
value: false
}),
n_top_items: Inputs.range([1, maxItems], {
step: 1,
label: "Top N Commodities",
value: defaultValue
})
});
Object.assign(form.style, {
display: "grid",
gridTemplateColumns: "repeat(3, minmax(0, 1fr))",
gap: "1rem",
alignItems: "start"
});
return form;
}Code
// q3_plot cell
q3_plot = {
const SC = (typeof scenario_config !== 'undefined') ? scenario_config : {unit: "usd15", scenario1: {scenario: "historic", timeframe: "historic"}, scenario2: {scenario: "ssp585", timeframe: "2041-2060"}};
const AO = (typeof q3_options !== 'undefined') ? q3_options : {show_compound: false, show_percentages: false, show_difference: false, n_top_items: 10, exclude_no_hazard: false};
const PD = (typeof processed_data !== 'undefined') ? processed_data : {enhanced_merged_data: [], merged_data: []};
const AG = (typeof aggregated_data !== 'undefined') ? aggregated_data : {by_hazard: []};
const SHOW = (typeof show_visualizations !== 'undefined') ? show_visualizations : true;
if (!SHOW) {
return htl.html`<div style="text-align: center; padding: 2rem; color: #666;">
Click "Show Analysis" above to view visualizations
</div>`;
}
// Build by-hazard aggregates if missing
const buildByHazard = (enh) => {
const data = Array.isArray(enh) ? enh : [];
const total1 = d3.sum(data, d => d.value1 || 0);
const total2 = d3.sum(data, d => d.value2 || 0);
const byHazard = d3.rollups(
data,
v => ({
value1: d3.sum(v, d => d.value1 || 0),
value2: d3.sum(v, d => d.value2 || 0),
diff: d3.sum(v, d => (d.diff != null ? d.diff : (d.value2||0)-(d.value1||0))),
perc1: total1 > 0 ? (d3.sum(v, d => d.value1 || 0) / total1 * 100) : 0,
perc2: total2 > 0 ? (d3.sum(v, d => d.value2 || 0) / total2 * 100) : 0
}),
d => d.hazard
).map(([hazard, vals]) => ({
hazard,
...vals,
perc_diff: vals.value1 > 0 ? ((vals.value2 - vals.value1) / vals.value1 * 100) : 0
}));
return { total_summary: { total_value1: total1, total_value2: total2 }, by_hazard: byHazard };
};
try {
const enhanced = PD.enhanced_merged_data || PD.merged_data;
if (!enhanced || enhanced.length === 0) {
return htl.html`<div class="loading-message">⚠️ No data available. Please click 'Load Data' to fetch data.</div>`;
}
const agg = AG.by_hazard.length > 0 ? AG : buildByHazard(enhanced);
const top_commodities = d3.rollups(enhanced, g => d3.sum(g, d => d.value2 || d.value1 || 0), d => d.crop)
.sort((a, b) => b[1] - a[1])
.slice(0, AO.n_top_items || 10)
.map(d => d[0]);
const data = enhanced
.filter(d => top_commodities.includes(d.crop) && (!AO.exclude_no_hazard || d.hazard !== "no hazard") && (AO.show_compound || !d.hazard.includes("+")))
.reduce((acc, d) => {
const scen = d.scenario || ((d.timeframe === SC.scenario1.timeframe && d.scenario === SC.scenario1.scenario) ? SC.scenario1.scenario : SC.scenario2.scenario);
const key = `${d.hazard}_${d.crop}_${scen}`;
if (!acc[key]) {
acc[key] = {
hazard: d.hazard,
crop: d.crop,
scenario: scen,
value: 0,
perc: 0,
diff: 0
};
}
acc[key].value += AO.show_percentages ? (d.perc2 || d.perc1 || 0) : (d.value2 || d.value1 || 0);
acc[key].perc += d.perc2 || d.perc1 || 0;
acc[key].diff += d.diff != null ? d.diff : (d.value2 || 0) - (d.value1 || 0);
return acc;
}, {});
const plot_data = Object.values(data);
const value_field = AO.show_difference ? "diff" : (AO.show_percentages ? "perc" : "value");
const unique_scenarios = [...new Set(plot_data.map(d => d.scenario))];
const combined_data = plot_data.filter(d => unique_scenarios.includes(d.scenario));
if (AO.show_difference) {
return Plot.plot({
title: `Change in Hazard Exposure: ${SC.scenario2.scenario} - ${SC.scenario1.scenario}`,
width: 900,
height: Math.max(400, top_commodities.length * 60),
marginLeft: 150,
x: { label: `Change in Exposure (${SC.unit || "units"})`, tickFormat: "~s" },
y: { domain: top_commodities, label: "Commodity" },
color: {
scheme: "Tableau10",
legend: true,
label: "Hazard"
},
marks: [
Plot.barX(combined_data, { x: "diff", y: "crop", fill: "hazard", sort: {y: "-x"}, tip: true }),
Plot.ruleX([0])
]
});
} else {
return Plot.plot({
title: "Hazard Exposure by Commodity",
width: 900,
height: 500,
marginTop: 100,
marginBottom: 100,
x: {
domain: unique_scenarios,
label: "Scenario"
},
y: {
label: AO.show_percentages ? "Exposure (%)" : `Exposure (${SC.unit || "usd15"})`,
tickFormat: AO.show_percentages ? (d => `${d}%`) : "~s"
},
color: {
scheme: "Tableau10",
legend: true,
label: "Commodity"
},
marks: [
Plot.rectY(combined_data, Plot.stackY({
x: "scenario",
y: value_field,
fill: "crop",
fx: "hazard",
order: d => -d[value_field], // Sort descending by value for stack order
channels: {crop: "crop", hazard: "hazard"},
tip: true
}))
]
});
}
} catch (error) {
console.error("Error creating Q3 plot:", error);
return htl.html`<div class="error-message">❌ Error: ${error.message}</div>`;
}
}Code
// q3_insights cell
q3_insights = {
const SC = (typeof scenario_config !== 'undefined') ? scenario_config : {unit: "usd15", scenario1: {scenario: "historic", timeframe: "historic"}, scenario2: {scenario: "ssp585", timeframe: "2041-2060"}};
const PD = (typeof processed_data !== 'undefined') ? processed_data : {enhanced_merged_data: [], merged_data: []};
const AG = (typeof aggregated_data !== 'undefined') ? aggregated_data : {by_hazard: []};
const SHOW = (typeof show_visualizations !== 'undefined') ? show_visualizations : true;
if (!SHOW) {
return htl.html`<div style="text-align: center; padding: 2rem; color: #666;">
Click "Show Analysis" above to view visualizations
</div>`;
}
// Build by-hazard aggregates if missing
const buildByHazard = (enh) => {
const data = Array.isArray(enh) ? enh : [];
const total1 = d3.sum(data, d => d.value1 || 0);
const total2 = d3.sum(data, d => d.value2 || 0);
const byHazard = d3.rollups(
data,
v => ({
value1: d3.sum(v, d => d.value1 || 0),
value2: d3.sum(v, d => d.value2 || 0),
diff: d3.sum(v, d => (d.diff != null ? d.diff : (d.value2||0)-(d.value1||0))),
perc1: total1 > 0 ? (d3.sum(v, d => d.value1 || 0) / total1 * 100) : 0,
perc2: total2 > 0 ? (d3.sum(v, d => d.value2 || 0) / total2 * 100) : 0
}),
d => d.hazard
).map(([hazard, vals]) => ({
hazard,
...vals,
perc_diff: vals.value1 > 0 ? ((vals.value2 - vals.value1) / vals.value1 * 100) : 0
}));
return { total_summary: { total_value1: total1, total_value2: total2 }, by_hazard: byHazard };
};
try {
const enhanced = PD.enhanced_merged_data || PD.merged_data;
const agg = AG.by_hazard.length > 0 ? AG : buildByHazard(enhanced);
if (!agg.by_hazard || agg.by_hazard.length === 0) {
return {
top_hazard1: "N/A",
top_value1: 0,
top_perc1: 0,
top_hazard2: "N/A",
top_value2: 0,
top_perc2: 0,
biggest_delta_hazard: "N/A",
delta_value: 0,
delta_perc: 0,
delta_direction: "increases",
compound_value1: 0,
compound_value2: 0,
top_compound: "compound hazards"
};
}
const non_compound_hazards = agg.by_hazard.filter(d => !d.hazard.includes("+") && d.hazard !== "no hazard" && d.hazard !== "any");
const top_hazard1 = non_compound_hazards.reduce((max, d) => (d.value1 || 0) > (max.value1 || 0) ? d : max, {value1: 0});
const top_hazard2 = non_compound_hazards.reduce((max, d) => (d.value2 || 0) > (max.value2 || 0) ? d : max, {value2: 0});
const biggest_delta = non_compound_hazards.reduce((max, d) => Math.abs(d.diff || 0) > Math.abs(max.diff || 0) ? d : max, {diff: 0});
const compound_hazards = agg.by_hazard.filter(d => d.hazard.includes("+"));
const top_compound_hazard = compound_hazards.reduce((max, d) => (d.value2 || 0) > (max.value2 || 0) ? d : max, {value2: 0, hazard: "compound hazards"});
const compound_value1 = d3.sum(compound_hazards, d => d.value1 || 0);
const compound_value2 = d3.sum(compound_hazards, d => d.value2 || 0);
return {
top_hazard1: top_hazard1.hazard || "N/A",
top_value1: top_hazard1.value1 || 0,
top_perc1: top_hazard1.perc1 || 0,
top_hazard2: top_hazard2.hazard || "N/A",
top_value2: top_hazard2.value2 || 0,
top_perc2: top_hazard2.perc2 || 0,
biggest_delta_hazard: biggest_delta.hazard || "N/A",
delta_value: Math.abs(biggest_delta.diff || 0),
delta_perc: Math.abs(biggest_delta.perc_diff || 0),
delta_direction: (biggest_delta.diff || 0) > 0 ? "increases" : "decreases",
compound_value1: compound_value1,
compound_value2: compound_value2,
top_compound: top_compound_hazard.hazard
};
} catch (error) {
console.error("Error calculating Q3 insights:", error);
return {
top_hazard1: "Error",
top_value1: 0,
top_perc1: 0,
top_hazard2: "Error",
top_value2: 0,
top_perc2: 0,
biggest_delta_hazard: "Error",
delta_value: 0,
delta_perc: 0,
delta_direction: "increases",
compound_value1: 0,
compound_value2: 0,
top_compound: "compound hazards"
};
}
}Code
// htl.html cell
htl.html`
<div class="visualization-container">
<div class="chart-title">Hazards Posing Greatest Threat to Commodities</div>
${q3_plot}
<div class="insights-section" style="margin-top: 2rem;">
<h3>💡 Dynamic Insights</h3>
<p>
In ${scenario_config?.scenario1?.scenario || "selected scenario 1"}, the hazard contributing the most to total exposure is <span class="dynamic-value">${q3_insights.top_hazard1}</span>, with <span class="dynamic-value">${formatNumber(q3_insights.top_value1)}</span> ${scenario_config?.unit || "usd15"} (<span class="dynamic-value">${formatPercentage(q3_insights.top_perc1, 1)}</span> of total).
</p>
<p>
In ${scenario_config?.scenario2?.scenario || "selected scenario 2"}, the leading hazard is <span class="dynamic-value">${q3_insights.top_hazard2}</span>, with <span class="dynamic-value">${formatNumber(q3_insights.top_value2)}</span> ${scenario_config?.unit || "usd15"} (<span class="dynamic-value">${formatPercentage(q3_insights.top_perc2, 1)}</span> of total).
</p>
<p>
The largest change in exposure between scenarios is observed for <span class="dynamic-value">${q3_insights.biggest_delta_hazard}</span>, which <span class="${q3_insights.delta_direction === 'increases' ? 'trend-increasing' : 'trend-decreasing'}">${q3_insights.delta_direction}</span> by <span class="dynamic-value">${formatNumber(q3_insights.delta_value)}</span> ${scenario_config?.unit || "usd15"} (<span class="dynamic-value">${formatPercentage(q3_insights.delta_perc, 1)}</span>).
</p>
<p>
For compound hazards, exposure to <span class="dynamic-value">${q3_insights.top_compound}</span> changes from <span class="dynamic-value">${formatNumber(q3_insights.compound_value1)}</span> ${scenario_config?.unit || "usd15"} in ${scenario_config?.scenario1?.scenario || "selected scenario 1"} to <span class="dynamic-value">${formatNumber(q3_insights.compound_value2)}</span> ${scenario_config?.unit || "usd15"} in ${scenario_config?.scenario2?.scenario || "selected scenario 2"}.
</p>
</div>
${downloadButton(q3_insights.data, "q3_hazard_threat.csv", "Download Q3 Data")}
</div>
`Q5 How Does Exposure Differ Between SSPs, Over Time, and Between GCMs?
Code
Plotly = require("plotly.js-dist-min@2")
// Q5 Data Preparation
q5_data = {
const severity = analysis_options.severity || "severe";
const variable = analysis_options.variable || "vop_usd15";
const period = analysis_options.period || "annual";
const commodities_filter = selected_commodities.filter(c => c !== "---").join("','");
const admin1_filter = selected_admin1?.length > 0 ? `AND admin1_name IN ('${selected_admin1.join("','")}')` : '';
const admin2_filter = selected_admin2?.length > 0 ? `AND admin2_name IN ('${selected_admin2.join("','")}')` : '';
const country = selected_admin0 || "Kenya";
const hazard_historic_path = files.hazard_historic.replace("{variable}", variable).replace("{period}", period).replace("{severity}", severity);
const hazard_ensemble_path = files.hazard_ensemble.replace("{variable}", variable).replace("{period}", period).replace("{severity}", severity);
// Historic query (single)
const [historic, future] = await Promise.all([
runQuery(`
SELECT 'historic' AS scenario, 'historic' AS timeframe, hazard, SUM(value) AS value, 0 AS value_sd
FROM read_parquet('${hazard_historic_path}')
WHERE admin0_name = '${country}'
AND crop IN ('${commodities_filter}')
${admin1_filter}
${admin2_filter}
GROUP BY scenario, timeframe, hazard
`),
runQuery(`
SELECT scenario, timeframe, hazard, SUM(value) AS value, SQRT(SUM(POW(value_sd,2))) AS value_sd
FROM read_parquet('${hazard_ensemble_path}')
WHERE admin0_name = '${country}'
AND crop IN ('${commodities_filter}')
${admin1_filter}
${admin2_filter}
GROUP BY scenario, timeframe, hazard
`)
]);
let data = [...historic, ...future];
// Compute 'any' categories
const computeAny = (data, pattern, newHazard) => {
const anyData = data.filter(d => d.hazard.includes(pattern));
return d3.rollups(
anyData,
v => ({
scenario: v[0].scenario,
timeframe: v[0].timeframe,
value: d3.sum(v, d => d.value),
value_sd: Math.sqrt(d3.sum(v, d => Math.pow(d.value_sd || 0, 2)))
}),
d => `${d.scenario}-${d.timeframe}`
).map(([key, vals]) => ({...vals, hazard: newHazard}));
};
const dryAny = computeAny(data, "dry", "dry (any)");
const heatAny = computeAny(data, "heat", "heat (any)");
const wetAny = computeAny(data, "wet", "wet (any)");
// Compute "any hazard" (all hazards combined)
const anyHazard = d3.rollups(
data.filter(d => !d.hazard.includes("no hazard")),
v => ({
scenario: v[0].scenario,
timeframe: v[0].timeframe,
value: d3.sum(v, d => d.value),
value_sd: Math.sqrt(d3.sum(v, d => Math.pow(d.value_sd || 0, 2)))
}),
d => `${d.scenario}-${d.timeframe}`
).map(([key, vals]) => ({...vals, hazard: "any hazard"}));
data = [...data, ...dryAny, ...heatAny, ...wetAny, ...anyHazard];
// Ensure historic sd=0
data.forEach(d => {
if (d.scenario === "historic") d.value_sd = 0;
});
// Calculate 95% CI (t=2.776 for df=4)
data.forEach(d => {
const error = 2.776 * (d.value_sd || 0) / Math.sqrt(5);
d.value_low = d.value - error;
d.value_high = d.value + error;
});
// Year calculation
data.forEach(d => {
if (d.timeframe === "historic") {
d.year = (2014 + 1995) / 2; // 2004.5
} else {
const [start, end] = d.timeframe.split("-").map(Number);
d.year = (start + end) / 2; // midpoint
}
});
console.log("q5_data:", data.length, "records", data.slice(0, 5));
return data;
}
// Q5 Plotly Chart
q5_plotly = {
const DATA = await q5_data;
const solo_set = ["dry (any)", "heat (any)", "wet (any)", "any hazard"];
const filtered = DATA.filter(d => solo_set.includes(d.hazard));
const historic = filtered.filter(d => d.scenario === "historic");
const future = filtered.filter(d => d.scenario !== "historic");
const scenarios = Array.from(new Set(future.map(d => d.scenario))).sort();
const hazards = solo_set;
// Create container div with unique ID
const divId = DOM.uid('q5_plot').id;
const div = htl.html`<div id="${divId}" style="height: 800px; width: 100%;"></div>`;
if (!filtered.length) {
console.warn("No data for q5_plotly");
div.innerHTML = '<div style="padding: 20px; text-align: center;">No data available for plotting</div>';
return div;
}
// Prepare traces
const traces = [];
const colors = {
'ssp126': '#1b9e77',
'ssp245': '#d95f02',
'ssp370': '#7570b3',
'ssp585': '#e7298a',
'historic': '#888888'
};
hazards.forEach((haz, i) => {
const yaxis = i === 0 ? 'y' : `y${i + 1}`;
// Historic baseline line
const hist_val = historic.find(d => d.hazard === haz)?.value || 0;
if (hist_val > 0) {
traces.push({
type: 'scatter',
mode: 'lines',
x: [1995, 2100],
y: [hist_val, hist_val],
line: {dash: 'dash', color: 'grey', width: 1},
yaxis: yaxis,
showlegend: i === 0,
name: 'Historic baseline',
hovertemplate: `Historic: ${hist_val.toFixed(0)}<extra></extra>`
});
}
// Plot each scenario
scenarios.forEach(scen => {
const scen_data = future.filter(d => d.scenario === scen && d.hazard === haz).sort((a, b) => a.year - b.year);
if (scen_data.length > 0) {
// Main line with markers
traces.push({
type: 'scatter',
mode: 'lines+markers',
x: scen_data.map(d => d.year),
y: scen_data.map(d => d.value),
error_y: {
type: 'data',
symmetric: false,
array: scen_data.map(d => Math.max(0, d.value_high - d.value)),
arrayminus: scen_data.map(d => Math.max(0, d.value - d.value_low)),
color: colors[scen],
thickness: 1.5,
width: 3
},
name: scen,
yaxis: yaxis,
marker: {
size: 6,
color: colors[scen]
},
line: {
width: 2,
color: colors[scen]
},
showlegend: i === 0,
legendgroup: scen,
hovertemplate: '%{y:.0f}<br>%{x}<extra>%{fullData.name}</extra>'
});
}
});
});
// Create layout
const layout = {
title: {
text: `Exposure Over Time by SSP and Hazard (${scenario_config?.unit || 'units'})`,
x: 0.5,
xanchor: 'center',
font: {size: 16}
},
grid: {
rows: hazards.length,
columns: 1,
pattern: 'independent',
roworder: 'top to bottom',
ygap: 0.1
},
height: 800,
showlegend: true,
legend: {
orientation: 'v',
x: 1.02,
y: 1,
xanchor: 'left',
yanchor: 'top',
bgcolor: 'rgba(255,255,255,0.8)',
bordercolor: 'grey',
borderwidth: 1
},
margin: {l: 100, r: 150, t: 80, b: 60},
xaxis: {
title: 'Year',
dtick: 10,
range: [2000, 2100],
fixedrange: false
}
};
// Configure y-axes for each subplot
hazards.forEach((haz, i) => {
const yaxis = i === 0 ? 'yaxis' : `yaxis${i + 1}`;
const domain_bottom = 1 - (i + 1) / hazards.length;
const domain_top = 1 - i / hazards.length;
layout[yaxis] = {
title: {
text: haz,
font: {size: 12, color: '#333'}
},
tickformat: ",.0f",
domain: [domain_bottom + 0.02, domain_top - 0.02],
fixedrange: false,
anchor: 'x',
side: 'left'
};
if (i > 0) {
layout[`xaxis${i + 1}`] = {
...layout.xaxis,
anchor: yaxis,
overlaying: 'x',
matches: 'x'
};
}
});
// Wait for div to be in DOM, then plot
setTimeout(() => {
Plotly.newPlot(divId, traces, layout, {responsive: true})
.catch(error => {
console.error("Plotly error:", error);
document.getElementById(divId).innerHTML = `<div style="padding: 20px; color: red;">Error creating plot: ${error.message}</div>`;
});
}, 100);
return div;
}
// Q5 Dynamic Insights
q5_insights = {
const DATA = await q5_data;
if (!DATA || DATA.length === 0) {
return htl.html`<p>No data available for insights.</p>`;
}
const years = Array.from(new Set(DATA.map(d => d.year))).sort(d3.ascending);
const min_year = years.length ? Math.round(d3.min(years)) : 2005;
const max_year = years.length ? Math.round(d3.max(years)) : 2090;
const scenarios = Array.from(new Set(DATA.map(d => d.scenario))).filter(s => s !== "historic");
const hazards = ["dry (any)", "heat (any)", "wet (any)", "any hazard"];
// Calculate changes for each scenario/hazard combination
const changes = [];
hazards.forEach(h => {
scenarios.forEach(s => {
const scen_data = DATA.filter(d => d.hazard === h && d.scenario === s);
if (scen_data.length > 0) {
const sorted = scen_data.sort((a, b) => a.year - b.year);
const start = sorted[0]?.value || 0;
const end = sorted[sorted.length - 1]?.value || 0;
changes.push({
hazard: h,
scenario: s,
start,
end,
pct_change: start > 0 ? ((end - start) / start * 100) : 0
});
}
});
});
// Find scenario with maximum change
const max_change = changes.reduce((max, d) =>
d.hazard === "any hazard" && d.pct_change > (max?.pct_change || -Infinity) ? d : max,
null
);
const scenario_max = max_change?.scenario || "ssp585";
const val_start = max_change?.start || 0;
const val_end = max_change?.end || 0;
const pct_change = max_change?.pct_change || 0;
// Find peak year
const peak_data = DATA.filter(d => d.scenario === scenario_max && d.hazard === "any hazard");
const peak = peak_data.reduce((max, d) => d.value > (max?.value || -Infinity) ? d : max, null);
const year_peak = peak ? Math.round(peak.year) : max_year;
const val_peak = peak?.value || val_end;
// Find hazard with greatest growth
const hazard_changes = changes.filter(d => d.scenario === scenario_max);
const max_growth = hazard_changes.reduce((max, d) => d.pct_change > (max?.pct_change || -Infinity) ? d : max, null);
const hazard_max_growth = max_growth?.hazard || "any hazard";
const pct_growth_max = max_growth?.pct_change || 0;
// Find most stable hazard
const stable = hazard_changes.reduce((min, d) => Math.abs(d.pct_change) < Math.abs(min?.pct_change || Infinity) ? d : min, null);
const hazard_stable = stable?.hazard || "any hazard";
const pct_stable = stable?.pct_change || 0;
// Find year with highest uncertainty
const uncertainty_data = DATA.filter(d => d.scenario !== "historic" && d.hazard === "any hazard" && d.value_sd > 0);
const max_uncertainty = uncertainty_data.reduce((max, d) =>
(d.value_high - d.value_low) > (max?.range || -Infinity) ? {...d, range: d.value_high - d.value_low} : max,
null
);
const year_uncertain = max_uncertainty ? Math.round(max_uncertainty.year) : max_year;
const val_min_uncertainty = max_uncertainty?.value_low || 0;
const val_max_uncertainty = max_uncertainty?.value_high || 0;
const unit = scenario_config?.unit || "$";
// Format numbers helper
const fmt = d => d.toLocaleString(undefined, {maximumFractionDigits: 0});
const pct = d => d.toFixed(0);
return htl.html`
<div>
<p>Between ${min_year} and ${max_year}, total exposure to all hazards under ${scenario_max} increases from
${fmt(val_start)} to ${fmt(val_end)} ${unit}, representing a ${pct(pct_change)}% increase.
The highest projected exposure occurs in ${year_peak} under ${scenario_max}, reaching ${fmt(val_peak)} ${unit}.</p>
<p>Among all hazard types:<br>
• The greatest increase in exposure is observed for ${hazard_max_growth} (${pct(pct_growth_max)}% rise from ${min_year} to ${max_year})<br>
• The most stable hazard is ${hazard_stable}, with a ${pct(Math.abs(pct_stable))}% change across the same period</p>
<p>Uncertainty is highest in ${year_uncertain}, with model spread ranging from
${fmt(val_min_uncertainty)} to ${fmt(val_max_uncertainty)} ${unit}.</p>
</div>
`;
}
// Q5 Full Section Assembly
htl.html`
<div class="visualization-container">
<p>This plot shows how total exposure to climate hazards evolves over time (2030–2090) under different future scenarios (SSPs), broken down by hazard type.</p>
<p>Each dot represents the median exposure across multiple climate models (GCMs), and the vertical bars show the spread (variation) across models – helping you judge both average risk and uncertainty.</p>
<p>The rows separate hazard types:<br>
• Top row shows total combined hazard exposure<br>
• Other rows isolate exposure to dry, heat, and wet hazards</p>
<p>Use this view to:<br>
• Compare SSP trajectories (e.g. ssp126 vs ssp585)<br>
• Identify when and where exposure is rising fastest<br>
• Understand which hazards dominate exposure under each scenario<br>
• Assess how much uncertainty (variation across models) exists at different points in time</p>
${q5_plotly}
<div class="insights-section" style="margin-top: 2rem;">
<h3>💡 Dynamic Insights</h3>
${q5_insights}
</div>
</div>
`Q7 Raster Exposure Visualization
Code
leaflet_geotiff_deps = {
console.log("Q7: Starting dependency loading");
// Add CSS first
if (!document.querySelector('link[href*="leaflet.css"]')) {
const css = document.createElement('link');
css.rel = 'stylesheet';
css.href = 'https://unpkg.com/leaflet@1.9.4/dist/leaflet.css';
document.head.appendChild(css);
}
// Function to load script and verify global object
const loadScriptAndVerify = async (src, globalName, maxRetries = 5) => {
for (let i = 0; i < maxRetries; i++) {
if (window[globalName]) {
console.log(`Q7: ${globalName} already available`);
return true;
}
await new Promise((resolve, reject) => {
const script = document.createElement('script');
script.src = src;
script.onload = () => {
console.log(`Q7: Script loaded from ${src}`);
setTimeout(resolve, 200); // Give time for initialization
};
script.onerror = reject;
document.head.appendChild(script);
}).catch(e => console.error(`Q7: Failed to load ${src}`, e));
// Check if global is now available
if (window[globalName]) {
console.log(`Q7: ${globalName} now available after retry ${i + 1}`);
return true;
}
await new Promise(resolve => setTimeout(resolve, 500));
}
return false;
};
// Load Leaflet first and verify
const leafletLoaded = await loadScriptAndVerify(
'https://unpkg.com/leaflet@1.9.4/dist/leaflet.js',
'L'
);
if (!leafletLoaded) {
throw new Error("Failed to load Leaflet after multiple attempts");
}
console.log("Q7: Leaflet verification - L exists:", !!window.L);
console.log("Q7: Leaflet version:", window.L?.version);
// Load GeoTIFF
await loadScriptAndVerify(
'https://cdn.jsdelivr.net/npm/geotiff@2.1.3/dist-browser/geotiff.js',
'GeoTIFF'
);
// Now try to load leaflet-geotiff using alternative CDN if needed
let geotiffPluginLoaded = false;
// Try primary CDN
try {
await new Promise((resolve, reject) => {
const script = document.createElement('script');
script.src = 'https://cdn.jsdelivr.net/npm/leaflet-geotiff-2@latest/dist/leaflet-geotiff.js';
script.onload = resolve;
script.onerror = reject;
document.head.appendChild(script);
});
await new Promise(resolve => setTimeout(resolve, 500));
geotiffPluginLoaded = !!window.L?.LeafletGeotiff;
} catch (e) {
console.log("Q7: Primary CDN failed, trying alternative");
}
// If still not loaded, try alternative approach
if (!geotiffPluginLoaded) {
try {
// Try loading the plotty renderer first
await new Promise((resolve) => {
const script = document.createElement('script');
script.src = 'https://unpkg.com/plotty@0.4.9/dist/plotty.min.js';
script.onload = resolve;
script.onerror = () => resolve(); // Continue even if fails
document.head.appendChild(script);
});
// Try alternative leaflet-geotiff source
await new Promise((resolve) => {
const script = document.createElement('script');
script.src = 'https://cdn.jsdelivr.net/npm/leaflet-geotiff-2@latest/dist/leaflet-geotiff.js';
script.onload = resolve;
script.onerror = () => resolve();
document.head.appendChild(script);
});
} catch (e) {
console.log("Q7: Alternative loading also failed");
}
}
console.log("Q7: Final check - L.LeafletGeotiff exists:", !!window.L?.LeafletGeotiff);
console.log("Q7: Final check - L.leafletGeotiff exists:", !!window.L?.leafletGeotiff);
return true;
}
// Load SpamCodes.csv and create mapping
spam_codes = d3.csv("https://raw.githubusercontent.com/AdaptationAtlas/hazards_prototype/main/metadata/SpamCodes.csv").then(rows => {
console.log("Q7: SpamCodes data loaded:", rows.length, "rows collected");
const mapping = new Map();
rows.forEach(row => {
if (row.compound === "no" && row.Code) {
const fullname = row.Fullname.toLowerCase().replace(/ /g, "-");
const code = row.Code.toLowerCase();
mapping.set(fullname, code);
}
});
console.log("Q7: SpamCodes mapping created with", mapping.size, "entries");
return mapping;
});
// In q7_raster_map, change to use crop_code in URL
q7_raster_map = {
try {
await leaflet_geotiff_deps;
} catch (error) {
console.error("Q7 Map: Dependency error:", error);
return htl.html`<div style="padding: 20px; background: #fee;">
Dependency loading failed: ${error.message}
</div>`;
}
const codes = await spam_codes; // Wait for mapping
const individual_crops = selected_commodities?.filter(c =>
!["all", "crops", "cereals", "legumes", "root_tubers", "livestock", "---"].includes(c)
) || ["maize"];
console.log("Q7 Map: Selected commodities:", selected_commodities);
console.log("Q7 Map: Individual crops filtered:", individual_crops.length, "records");
const selected_crop = individual_crops[0] || "maize";
const crop_code = selected_crop; // Use full name directly as per correction
// Corrected URL format based on R code
const variable = analysis_options.variable || "vop_usd15";
const url = `https://digital-atlas.s3.amazonaws.com/domain=exposure/type=raster/source=spam2020v1r2_ssa/region=ssa/crop=${crop_code}/exposure.tif`;
console.log("Q7 Map: Creating map for crop:", selected_crop, "code:", crop_code, "URL:", url);
const options = { onError: err => console.error("GeoTIFF load error:", err) };
const container = htl.html`<div style="height: 500px; width: 100%;"></div>`;
// Wait for DOM
await new Promise(resolve => setTimeout(resolve, 100));
if (!window.L) {
container.innerHTML = "Leaflet library not available";
return container;
}
// Create basic map first
const map = L.map(container).setView([0, 35], 4);
L.tileLayer('https://{s}.basemaps.cartocdn.com/light_all/{z}/{x}/{y}.png', {
attribution: '© CartoDB'
}).addTo(map);
console.log("Q7 Map: Base map initialized");
// Check if we can add raster
const GeotiffConstructor = window.L?.LeafletGeotiff || window.L?.leafletGeotiff;
if (GeotiffConstructor) {
console.log("Q7 Map: Adding GeoTIFF layer");
try {
const layer = new GeotiffConstructor(url, {
band: 0,
displayMin: 0,
displayMax: 1000,
colorScale: 'viridis'
});
layer.addTo(map);
console.log("Q7 Map: GeoTIFF layer added successfully");
} catch (e) {
console.error("Q7 Map: Error adding GeoTIFF:", e);
container.innerHTML += `<div style="position: absolute; top: 10px; left: 10px; background: white; padding: 10px; border: 1px solid red;">
Error loading raster: ${e.message}<br>URL: ${url}
</div>`;
}
} else {
console.log("Q7 Map: GeoTIFF plugin not available, showing base map only");
// Add a notice on the map
const info = L.control({position: 'topright'});
info.onAdd = () => {
const div = L.DomUtil.create('div', 'info');
div.style.cssText = 'background: white; padding: 10px; border-radius: 5px;';
div.innerHTML = `<strong>Note:</strong> Raster layer unavailable<br>Showing base map for ${selected_crop}`;
return div;
};
info.addTo(map);
}
// Add basic legend regardless
const legend = L.control({position: 'bottomright'});
legend.onAdd = () => {
const div = L.DomUtil.create('div', 'legend');
div.style.cssText = 'background: white; padding: 10px; border-radius: 5px;';
div.innerHTML = `<strong>${selected_crop}</strong>`;
return div;
};
legend.addTo(map);
console.log("Q7 Map: Map rendering complete");
return container;
}Code
htl.html`
<div class="visualization-container">
<p>This map displays the spatial distribution of exposure intensity for the selected crop(s) as a raster layer.
The visualization uses TIFF data to show exposure patterns at high resolution across the region.</p>
<p><strong>Selected commodity:</strong> ${selected_commodities?.filter(c =>
!["all", "crops", "cereals", "legumes", "root_tubers", "livestock", "---"].includes(c)
)[0] || "maize"}</p>
${q7_raster_map}
<p>Colors represent exposure intensity using a viridis scale where darker purple indicates lower exposure
and bright yellow indicates higher exposure. This high-resolution view helps identify local hotspots
and spatial patterns that may not be visible in the aggregated admin-level data.</p>
</div>
`