import {
atlasHero,
downloadButton,
multiLineText,
loaderDiv,
} from "/helpers/uiComponents.ojs";
import { filterableDataTable as dataTable } from "/components/atlasTable.ojs";
import { cleanAdminInput_SQL, patchWindowsCache } from "/helpers/data.js";
import { enhancedMultiSelect } from "/helpers/enhancedMultiSelect.ojs";
import { atlasTOC } from "/helpers/toc.ojs";
atlasHero(nbTitle, "../../images/default_crop.webp");{
renderToDiv("plotRecentChanges", () => {
if (viewRecentChanges === "divergingBar") return barplot_recentChanges();
if (viewRecentChanges === "warmingStripes") {
return warmingStripes_recentChanges();
}
return dataTable(recentChanges_plotData, {
format: {
year: (d) => d,
},
header: {
iso3: "ISO3",
admin0_name: "Country",
admin1_name: "Admin 1",
season: "Season",
year: "Year",
hazard: "Hazard",
mean: "Mean",
mean_anomaly: "Mean Anomaly",
},
columns: [
"iso3",
"admin0_name",
"admin1_name",
"season",
"year",
"hazard",
"mean",
"mean_anomaly",
],
});
});
}Data Sources
Methods
Source code
Text and Language Translations
import { lang as Lang } from "/helpers/lang.js";
general_translations = await FileAttachment(
"/data/shared/generalTranslations.json",
).json();
languages = [
{ key: "en", label: "English", locale: 'en-US' },
{ key: "fr", label: "Français", locale: 'fr-FR' }
]
defaultLangKey = {
const name = "lang";
const list = languages.map((d) => d.key);
const defaultKey = "en";
const queryParam = await Lang.getParamFromList({ name, list });
return queryParam ?? defaultKey;
}
_lang = Lang.lg(masterLanguage.key)
viewof masterLanguage = Inputs.radio(languages, {
label: "Main language toggle",
format: (d) => d.key,
value: languages.find((x) => x.key === defaultLangKey),
})nbTitle = _lang({
en: "Formulate A Climate Rationale",
fr: "Formuler une justification climatique",
});
nbText = await FileAttachment("/data/climateRationale/nbText.json").json();
heading1 = _lang(general_translations.overview);
heading2 = _lang(nbText.sections.keyFacts.title);
heading3 = _lang(nbText.sections.recentChanges.title);
heading4 = _lang(nbText.sections.futureProjections.title);
heading5 = _lang({ en: "Extreme Events", fr: "Evenements extremes" }); //nbText.sections.extremeEvents.title
heading6 = _lang(nbText.sections.hazardExposure.title);
summary = _lang(general_translations.summary);
appendix = _lang(general_translations.appendix);
methodsData = _lang(general_translations.methodsData);General
formatters
sqlWhereNull = ({ value, field } = {}) => {
return value === null ? `and ${field} is null` : `and ${field} = '${value}'`;
};
sqlList = (values = []) => `('${values.join("', '")}')`;
withAdminName = (rows = []) =>
rows.map((row) => ({
...row,
adminName: row.admin1_name ? `${row.admin1_name} (${row.iso3})` : row.iso3,
}));
renderToDiv = (id, render) => {
const div = document.getElementById(id);
const viz = render();
div.replaceChildren(viz);
};Styling
function NavbarLangSelector(language_obj, masterLanguage) {
let navEnd = document.querySelector(".navbar-nav.ms-auto .nav-item.compact");
if (navEnd) {
let existingLangSelector = document.getElementById("nav-lang-selector");
if (!existingLangSelector) {
let lang_sel = Inputs.bind(
Inputs.radio(language_obj, {
label: "",
format: (d) => d.label
}),
viewof masterLanguage
);
lang_sel.id = "nav-lang-selector";
// Hack the css together for the observable inputs
lang_sel.style.display = "flex";
lang_sel.style.alignItems = "center";
lang_sel.style.marginLeft = "10px";
let lang_div = lang_sel.querySelector("div");
lang_div.style.display = "flex";
lang_div.style.flexDirection = "column";
// Insert the new item after the GitHub icon and other elements
navEnd.parentNode.appendChild(lang_sel);
}
}
}
NavbarLangSelector(languages, masterLanguage)Data
Database
data_obj = {
const obj = await FileAttachment("/data/climateRationale/nbData.json").json();
return obj.data
}
db ={
const _cleaned = data_obj.map(item => ({
...item,
s3_path: patchWindowsCache(item.s3_path)
}));
return await generateDB(
// Future projection data is big, so split into different database
_cleaned.filter((d) => !d.sections.includes("futureProjections")),
);
}dbFutureHive = {
const fut = data_obj.filter((d) => d.sections.includes("futureProjections"));
const _db = await DuckDBClient.of();
await _db.query(`SET enable_object_cache = true;`)
await _db.query(`SET enable_http_metadata_cache = true;`)
const paths = fut.map((d) => {
let p
// if (d.local_path) {
// p = "http://localhost:4040" + d.local_path
// } else {
p = d.s3_path
// }
return `'${patchWindowsCache(p)}'`
}) // `'${||d.s3_path}'
await _db.query(`
CREATE VIEW futureProjections as
SELECT *, period as timeperiod
FROM parquet_scan([
${paths.join(", ")}
],filename=true, hive_partitioning = 1)
`)
return _db
}Sections
data - keyFacts
gdp_plotData = {
const resp = await db.query(`
SELECT iso3,
sector,
year,
gdp_usd2015,
ROUND(
gdp_usd2015
/ SUM(CASE WHEN sector != 'total' THEN gdp_usd2015 END)
OVER (PARTITION BY iso3, year),
4
) * 100 AS pct_of_total
FROM a0_gdp
WHERE iso3 in ${sqlList(admin0Iso3)}
AND sector != 'total'
AND year = 2022
`)
return resp
}
landuse_plotData = {
const resp = await db.query(`
SELECT iso3,
sub_group,
"group",
year,
ha,
ROUND(
ha / SUM(ha) OVER (PARTITION BY iso3, year),
4
) * 100 as ha_pct
FROM a0_landuse
WHERE iso3 in ${sqlList(admin0Iso3)}
AND year = 2021
`)
return resp
}pov_plotData = {
const resp = await db.query(`
SELECT
p.iso3,
p.admin0_name,
p.admin1_name,
g.group_name AS group,
ROUND(
CASE
WHEN g.group_name = 'in_poverty' THEN p.poor420_ln * 100
ELSE 100 - p.poor420_ln * 100
END,
2
) AS pov_rate,
FROM poverty p
CROSS JOIN (
VALUES
('in_poverty'),
('not_poverty')
) AS g(group_name)
WHERE 1=1
AND iso3 in ${sqlList(admin0Iso3)}
AND (
admin1_name in ${sqlList(admin1NamesClean)}
OR admin1_name IS NULL
)
`);
return withAdminName(resp)
}exposure_plotData = {
const resp = await db.query(`
SELECT
iso3,
admin1_name,
crop,
value
FROM exposure
WHERE 1=1
AND iso3 in ${sqlList(admin0Iso3)}
AND (
admin1_name in ${sqlList(admin1NamesClean)}
OR admin1_name IS NULL
)
`);
const cropData = resp.map((row) => {
const category = cropCategoryMap.get(row.crop) || "other";
return { ...row, category };
});
return withAdminName(cropData)
}
exposureMap = new Map(
exposure_plotData.map(d => [
`${d.iso3}|${d.admin1_name}|${d.crop}`,
d.value
])
);
exposure_plotDatamapSpamCrops = await FileAttachment("/data/shared/MapSpamCrops.json").json();
cropTranslations = Object.fromEntries(
Object.entries(mapSpamCrops).flatMap(([category, { label, items }]) => [
[category, label],
...items.map(({ id, label }) => [id, label]),
]),
);
exposure_groups = Object.fromEntries(
Object.entries(mapSpamCrops).map(([category, data]) => [
category,
data.items.map((item) => item.id),
]),
);
cropCategoryMap = new Map(
Object.entries(exposure_groups).flatMap(([category, crops]) =>
crops.map((crop) => [crop, category]),
),
);data - recentChanges
recentChanges_data = {
const _season = seasonSelect.season;
// const _variable = climateVarSelect.id;
const resp = await db.query(`
SELECT
iso3,
admin0_name,
admin1_name,
season,
year,
hazard,
mean,
mean_anomaly
FROM historic_climate_timeseries
WHERE 1=1
AND iso3 in ${sqlList(admin0Iso3)}
AND (
admin1_name in ${sqlList(admin1Names)}
OR admin1_name IS NULL
)
${sqlWhereNull({ field: "season", value: _season })}
`);
return withAdminName(resp);
}
recentChanges_plotData = recentChanges_data.filter((d) => d.hazard === climateVarSelect.id)data - futureProjections
futureProjections_data = {
const _season = seasonSelect.season;
const resp = await dbFutureHive.query(`
SELECT
iso3,
admin0_name,
admin1_name,
season,
scenario,
year,
timeperiod,
hazard,
mean,
mean_anomaly,
max,
max_anomaly,
min,
min_anomaly
FROM futureProjections
where 1=1
AND iso3 in ${sqlList(admin0Iso3)}
AND (
admin1_name in ${sqlList(admin1Names)}
OR admin1_name IS NULL
)
${sqlWhereNull({ field: "season", value: _season })}
AND timeperiod = '${futurePeriodSelect}'
AND scenario in ${sqlList(futureScenarioSelect.map((d) => d.toLowerCase()))}
`);
return withAdminName(resp);
}
futureProjections_data;data - extremeEvents
function addZScores(data, groupKeys, field = "mean") {
const groups = d3.group(data, (d) => groupKeys.map((k) => d[k]).join("|"));
const result = [];
for (const [_, records] of groups) {
const mean = d3.mean(records, (d) => d[field]);
const std = d3.deviation(records, (d) => d[field]);
for (const d of records) {
result.push({
...d,
z: std === 0 ? 0 : (d[field] - mean) / std,
});
}
}
return result;
}
zThresholds = new Object({
extreme_low: (z) => z <= -2,
unusual_low: (z) => z > -2 && z <= -1,
unusual_high: (z) => z >= 1 && z < 2,
extreme_high: (z) => z >= 2,
});
function classifyZ(z) {
return Object.keys(zThresholds).find((k) => zThresholds[k](z)) || null;
}
function aggregateEvents(data, scenario_name) {
const groups = new Map();
for (const d of data) {
const key = `${d.iso3}|${d.admin0_name}|${d.admin1_name}|${d.hazard}|${d.season}|${d.scenario}`;
if (!groups.has(key)) {
groups.set(key, {
iso3: d.iso3,
admin0: d.admin0_name,
admin1: d.admin1_name,
adminName: d.adminName,
hazard: d.hazard,
scenario: d.scenario || scenario_name,
// count number in each class from thresholds
...Object.fromEntries(Object.keys(zThresholds).map((k) => [k, 0])),
});
}
const group = groups.get(key);
const category = classifyZ(d.z);
if (category) group[category] += 1;
}
return Array.from(groups.values());
}
extremeEvents_plotData = {
const historicZ = addZScores(recentChanges_plotData, [
"iso3",
"admin0_name",
"admin1_name",
"season",
"hazard",
])
const historicData = aggregateEvents(historicZ, "historical")
const futureZ = addZScores(futureProjections_plotData, [
"iso3",
"admin0_name",
"admin1_name",
"season",
"hazard",
"scenario",
])
const futureData = aggregateEvents(futureZ)
const data = [...historicData, ...futureData]
const longData = data.flatMap(d =>
Object.entries({
extreme_low: d.extreme_low,
unusual_low: d.unusual_low,
unusual_high: d.unusual_high,
extreme_high: d.extreme_high
}).map(([category, value]) => ({
iso3: d.iso3,
admin0_name: d.admin0,
admin1_name: d.admin1,
scenario: d.scenario,
adminName: d.adminName,
hazard: d.hazard,
category,
num_events: value
}))
);
return longData
}data - hazardExposure
hazardExposure_plotData = {
const resp = await db.query(`
SELECT
iso3,
admin0_name,
admin1_name,
crop,
hazard,
timeframe,
scenario,
value
FROM hazard_exposure
where 1=1
AND admin2_name IS NULL
AND hazard_vars in ('NDWS+NTx35+NDWL0', 'NDWS+THI-max+NDWL0')
AND exposure_unit = 'nominal-usd-2021'
AND crop != 'generic-crop'
AND iso3 in ${sqlList(admin0Iso3)}
AND (
admin1_name in ${sqlList(admin1Names)}
OR admin1_name IS NULL
)
AND timeframe in ('1995-2014', '${futurePeriodSelect}')
AND scenario in ${sqlList(['historic', ...futureScenarioSelect.map((d) => d.toLowerCase())])}
`);
// Add in total production not exposed
// const merged = data.map(d => ({
// ...d,
// total_value: exposureMap.get(`${d.iso3}|${d.admin1_name}|${d.crop}`) ?? null
// }));
// return merged
return withAdminName(resp)
}
hazardExposure_plotData;Global Selectors
Admin Regions
countries = {
const country_list = await FileAttachment("/data/shared/atlas_countries.json").json();
const filteredCountries = country_list
.filter((c) => c.include && c.iso3c !== "SDN")
.map(({ include, ...rest }) => rest);
return filteredCountries;
};
viewof admin0Select = Inputs.input(Object.values(countries).slice(0, 1))
renderA0Multi = ({maxSelections = Infinity, requireAtLeastOne = false} = {}) => {
const _admin0 = Inputs.select(countries, {
//NOTE: _lang & general_translations are defined in
// components/_lang.qmd
//Both must be loaded into a notebook for this to work.
label: _lang(general_translations.country),
format: (d) => _lang(d.translation),
// format: (d) => d.iso3c,
multiple: true
}
);
// Adds the additional features to the multi-select
enhancedMultiSelect(_admin0, { maxSelections, requireAtLeastOne })
// Bind to the main global multi-select for updates across NB
return Inputs.bind(_admin0, viewof admin0Select)
}allAdmin1 = {
const resp = await adminDB.query(`
SELECT DISTINCT admin0_name, admin1_name, iso3
FROM admin_lookup
WHERE iso3 in ('${countries.map((d) => d.iso3c).join("', '")}')
`);
return resp;
}
dataAdmin1 = allAdmin1.filter(d =>
admin0Select.some(a0 => a0.iso3c === d.iso3)
);
viewof admin1Select = Inputs.input([])
renderA1Multi = ({maxSelections = Infinity, requireAtLeastOne = false} = {}) => {
const _admin1 = Inputs.select(
dataAdmin1,
{
label: _lang(general_translations.region),
format: (d) => `${d.admin1_name || "Full Country"} (${d.iso3})`,
multiple: true,
}
)
enhancedMultiSelect(_admin1, { maxSelections, requireAtLeastOne })
return Inputs.bind(_admin1, viewof admin1Select)
}dataAdmin2 = await adminDB.query(`
SELECT DISTINCT admin0_name, admin1_name, admin2_name, iso3
FROM admin_lookup
WHERE iso3 in ('${admin0Select.map((d) => d.iso3c).join("', '")}')
AND admin1_name in ('${admin1Select.map((d) => d.admin1_name).join("', '")}')
`)
viewof admin2Select = Inputs.input([])
renderA2Multi = ({maxSelections = Infinity, requireAtLeastOne = false} = {}) => {
const _admin2 = Inputs.select(
dataAdmin2,
{
label: _lang(general_translations.district),
format: (d) => `${d.admin2_name} (${d.iso3})`,
multiple: true
}
)
enhancedMultiSelect(_admin2, { maxSelections, requireAtLeastOne })
return Inputs.bind(_admin2, viewof admin2Select)
}function setSelectorValue(input, value) {
input.value = value;
input.dispatchEvent(new Event("input", { bubbles: true }));
}
{
// If nothing selected in admin1, nothing to validate
if (!admin1Select.length) return;
// Set of iso3 codes from selected admin1 (a1)
const selectedA1Iso3 = new Set(admin1Select.map(d => d.iso3));
// If all iso3 codes in a0 are not in selectedA1Iso3
const a1NoMatch = admin0Select.every(obj => !selectedA1Iso3.has(obj.iso3c));
if (a1NoMatch) {
// Clear admin1 selection since it conflicts with the selected admin0
setSelectorValue(viewof admin1Select, []);
return;
}
//TODO: This probably needs expanded to admin2 as well
}Climate data
seasons = new Array(
{ season: "annual", season_rank: null, season_string: "Annual Average" },
{ season: "JFM", season_rank: 0, season_string: "Jan-Feb-Mar" },
{ season: "FMA", season_rank: 1, season_string: "Feb-Mar-Apr" },
{ season: "MAM", season_rank: 2, season_string: "Mar-Apr-May" },
{ season: "AMJ", season_rank: 3, season_string: "Apr-May-Jun" },
{ season: "MJJ", season_rank: 4, season_string: "May-Jun-Jul" },
{ season: "JJA", season_rank: 5, season_string: "Jun-Jul-Aug" },
{ season: "JAS", season_rank: 6, season_string: "Jul-Aug-Sep" },
{ season: "ASO", season_rank: 7, season_string: "Aug-Sep-Oct" },
{ season: "SON", season_rank: 8, season_string: "Sep-Oct-Nov" },
{ season: "OND", season_rank: 9, season_string: "Oct-Nov-Dec" },
{ season: "NDJ", season_rank: 10, season_string: "Nov-Dec-Jan" },
{ season: "DJF", season_rank: 11, season_string: "Dec-Jan-Feb" },
);
viewof seasonSelect = Inputs.select(seasons, {
format: (d) => d.season,
});hazards_obj = {
const input = general_translations.hazardVariables;
const hazard_lookup = new Object({
"ndwl0": "NDWL0",
"ndws": "NDWS",
"ntx35": "NTx35",
"ntx40": "NTx40",
"ptot": "PTOT",
"tavg": "TAVG",
"thi": "THI-max",
"tmax": "TMAX",
"hsh": "HSH-max"
})
return Object.entries(input).map(([id, data]) => ({
id: hazard_lookup[id],
...data
}));
}
viewof climateVarSelect = Inputs.select(hazards_obj, {format: (d) => d.id});climateForm = () => {
let season = Inputs.bind(
Inputs.select(seasons, {
format: (d) => d.season_string,
label: _lang(general_translations.season),
}),
viewof seasonSelect
);
const climateVar = Inputs.bind(
Inputs.select(hazards_obj, {
format: (d) => _lang(d.name),
label: _lang(general_translations.climateVar),
}),
viewof climateVarSelect
)
return Inputs.form(
[
season,
climateVar
],
{
template: inputTemplate()
}
)
};
climateForm();futurePeriods = [
"2021-2040",
"2041-2060",
"2061-2081",
"2080-2100"
]
viewof futurePeriodSelect = Inputs.input(futurePeriods[0]);
scenarioColors = new Object({
SSP126: "#4FB5B7",
SSP245: "#F4BB21",
SSP370: "#EE624F",
SSP585: "#B34E65",
});
scenarioNames = Object.keys(scenarioColors);
viewof futureScenarioSelect = Inputs.input([scenarioNames[1], scenarioNames[3]]);scenarioForm = () => {
const futurePeriod = Inputs.bind(
Inputs.select(futurePeriods, {
label: _lang(general_translations.timeperiod),
}),
viewof futurePeriodSelect
);
const futureScenario = Inputs.bind(
Inputs.checkbox(scenarioNames, {
label: _lang(general_translations.scenario),
}),
viewof futureScenarioSelect
);
return Inputs.form(
[futurePeriod, futureScenario],
{
template: inputTemplate()
}
);
}Quick Insights
createCountryInsights = (insightFunctions) => {
const country_insights = admin0Iso3
.map((c) => {
const countryName = `**${_lang(iso3ToTranslation[c])}**`;
const insights = insightFunctions
.map((fn) => fn(c))
.filter(Boolean)
.join(" \n\n");
return `${countryName} \n${insights}`;
})
.join("\n\n");
return md`${country_insights}`;
};
seasonInsight = () => {
const seasonTemplate = _lang(
nbText.sections.recentChanges.quickInsight.season,
);
const seasonInsight = Lang.reduceReplaceTemplateItems(seasonTemplate, [
{
name: "season",
value: seasonSelect.season_string,
},
]);
return md`_${seasonInsight}_`;
};Insights - Key Facts
povInsight = (iso3) => {
const insightBase = nbText.sections.keyFacts.quickInsight.poverty;
const countryTemplate = _lang(insightBase.country);
const directions = nbText.general.direction;
const country = _lang(iso3ToTranslation[iso3]);
const _pov = pov_plotData.filter(
(d) => d.iso3 === iso3 && d.group === "in_poverty",
);
const _countryPov = _pov.find((d) => !d.admin1_name)?.pov_rate;
const _adminPov = _pov.filter((d) => d.admin1_name);
const _countryInsight = Lang.reduceReplaceTemplateItems(countryTemplate, [
{ name: "country", value: country },
{ name: "povPct", value: _countryPov.toFixed(1) },
]);
const _adminInsight =
_adminPov.length > 0
? _adminPov
.map((d) => {
const direction =
d.pov_rate > _countryPov ? directions.greater : directions.less;
const adminTemplate = _lang(insightBase.admin);
return Lang.reduceReplaceTemplateItems(adminTemplate, [
{ name: "admin", value: d.admin1_name },
{ name: "povPct", value: d.pov_rate.toFixed(1) },
{ name: "direction", value: _lang(direction) },
]);
})
.join("\n")
: "";
return _adminInsight
? `${_countryInsight}\n${_adminInsight}`
: _countryInsight;
};
agInsight = (iso3) => {
const _agGDP = gdp_plotData
.filter((d) => d.iso3 === iso3 && d.sector === "agriculture")
.reduce(
(acc, d) => ({
pct: acc.pct + d.pct_of_total,
total: acc.total + d.gdp_usd2015,
}),
{ pct: 0, total: 0 },
);
const _agLandUse = landuse_plotData
.filter((row) => row.iso3 === iso3 && row.group === "Agriculture")
.reduce(
(acc, row) => ({
pct: acc.pct + row.ha_pct,
total: acc.total + row.ha,
}),
{ pct: 0, total: 0 },
);
const country = _lang(iso3ToTranslation[iso3]);
const template = _lang(
nbText.sections.keyFacts.quickInsight.gdpLandUse.country,
);
const items = [
{ name: "country", value: country },
{ name: "pctAgGDP", value: _agGDP.pct.toFixed(2) },
{ name: "agGDP", value: formatUSD()(_agGDP.total) },
{ name: "pctAgLandUse", value: _agLandUse.pct.toFixed(2) },
{ name: "LandUse", value: formatNumCompactShort()(_agLandUse.total) },
];
return Lang.reduceReplaceTemplateItems(template, items);
};exposureInsight = (iso3) => {
const insightBase = nbText.sections.keyFacts.quickInsight.production;
const allTemplate = _lang(insightBase.allAdmins);
const template = _lang(insightBase.topAdmin);
const directions = nbText.general.direction;
const country = _lang(iso3ToTranslation[iso3]);
const getTopCategory = (data) => {
const categoryTotals = data.reduce((acc, d) => {
acc[d.category] = (acc[d.category] || 0) + d.value;
return acc;
}, {});
return Object.entries(categoryTotals).reduce(
(max, [category, total]) =>
total > max.total ? { category, total } : max,
{ category: null, total: 0 },
);
};
const createAllInsight = (data, adminName) => {
const topGroup = getTopCategory(data);
const topCrop = data.reduce((max, d) => (d.value > max.value ? d : max), {
crop: null,
value: 0,
});
return Lang.reduceReplaceTemplateItems(allTemplate, [
{ name: "admin", value: adminName },
{ name: "group", value: _lang(cropTranslations[topGroup.category]) },
{ name: "groupValue", value: formatUSD()(topGroup.total) },
{ name: "topCommodity", value: _lang(cropTranslations[topCrop.crop]) },
{ name: "topValue", value: formatUSD()(topCrop.value) },
]);
};
const _countryData = exposure_plotData.filter(
(d) => d.iso3 === iso3 && !d.admin1_name,
);
const _countryInsight = createAllInsight(_countryData, country);
const _adminData = exposure_plotData.filter(
(d) => d.iso3 === iso3 && d.admin1_name,
);
const _adminInsights =
_adminData.length > 0
? [...new Set(_adminData.map((d) => d.admin1_name))]
.map((admin1Name) => {
const admin1Data = _adminData.filter(
(d) => d.admin1_name === admin1Name,
);
return `- ${createAllInsight(admin1Data, admin1Name, allTemplate)}`;
})
.join("\n")
: "";
return _adminInsights
? `${_countryInsight}\n${_adminInsights}`
: _countryInsight;
};Insights - recentChanges
climateInsight = (iso3) => {
const climateTemplates = {
temperature: _lang(nbText.sections.recentChanges.quickInsight.temp),
precipitation: _lang(nbText.sections.recentChanges.quickInsight.precip),
};
const climateData = recentChanges_data;
const country = _lang(iso3ToTranslation[iso3]);
const countryData = climateData.filter(
(d) => d.iso3 === iso3 && !d.admin1_name,
);
const adminData = climateData.filter((d) => d.iso3 === iso3 && d.admin1_name);
const buildInsight = (data, adminName) => {
const tempData = data.filter((d) => d.hazard === "TAVG");
const precipData = data.filter((d) => d.hazard === "PTOT");
const tempTrend = climateDataTools.getTrend(tempData);
const precipTrend = climateDataTools.getTrend(precipData);
const parts = [];
if (tempTrend) {
parts.push(
Lang.reduceReplaceTemplateItems(climateTemplates.temperature, [
{ name: "admin", value: adminName },
{
name: "tempChange",
value: Math.abs(tempTrend.totalChange).toFixed(2),
},
{ name: "startYear", value: tempTrend.startYear },
{ name: "endYear", value: tempTrend.endYear },
{
name: "tempPerDecade",
value: Math.abs(tempTrend.perDecade).toFixed(2),
},
]),
);
}
if (precipTrend) {
parts.push(
Lang.reduceReplaceTemplateItems(climateTemplates.precipitation, [
{ name: "admin", value: adminName },
{
name: "precipPerDecade",
value: Math.abs(precipTrend.perDecade).toFixed(1),
},
{
name: "precipDirection",
value:
precipTrend.perDecade < 0
? _lang(general_translations.less)
: _lang(general_translations.more),
},
]),
);
}
return parts.join(" ");
};
const countryInsight = buildInsight(countryData, country);
const adminInsights =
adminData.length > 0
? [...new Set(adminData.map((d) => d.admin1_name))]
.map((admin1Name) => {
const admin1Data = adminData.filter(
(d) => d.admin1_name === admin1Name,
);
return `- ${buildInsight(admin1Data, admin1Name)}`;
})
.join("\n")
: "";
return adminInsights
? `${countryInsight}\n\n${adminInsights}`
: countryInsight;
};extremeEventsInsight = (iso3) => {
const insightBase = nbText.sections.extremeEvents.quickInsight;
const countryTemplate = _lang(insightBase.country);
const adminTemplate = _lang(insightBase.admin);
const country = _lang(iso3ToTranslation[iso3]);
const rank = {
ssp126: 1,
ssp245: 2,
ssp370: 3,
ssp585: 4,
};
const toScenarioSummary = (rows) =>
rows.reduce((acc, d) => {
if (!acc[d.scenario]) {
acc[d.scenario] = {
unusual_low: 0,
extreme_low: 0,
unusual_high: 0,
extreme_high: 0,
};
}
acc[d.scenario][d.category] += d.num_events;
return acc;
}, {});
const buildInsight = (rows, adminName, template) => {
if (!rows.length) return null;
const byScenario = toScenarioSummary(rows);
const historical = byScenario.historical;
if (!historical) return null;
const futureScenario = Object.keys(byScenario)
.filter((s) => s !== "historical")
.sort((a, b) => (rank[b] || 0) - (rank[a] || 0))[0];
if (!futureScenario) return null;
const future = byScenario[futureScenario];
return Lang.reduceReplaceTemplateItems(template, [
{ name: "admin", value: adminName },
{ name: "scenario", value: futureScenario.toUpperCase() },
{ name: "extremeHighFuture", value: future.extreme_high },
{ name: "extremeLowFuture", value: future.extreme_low },
{ name: "extremeHighHistorical", value: historical.extreme_high },
{ name: "extremeLowHistorical", value: historical.extreme_low },
]);
};
const countryRows = extremeEvents_plotData.filter(
(d) => d.iso3 === iso3 && !d.admin1_name,
);
const countryInsight = buildInsight(countryRows, country, countryTemplate);
const adminRows = extremeEvents_plotData.filter(
(d) => d.iso3 === iso3 && d.admin1_name,
);
const adminInsights = [...new Set(adminRows.map((d) => d.admin1_name))]
.sort((a, b) => a.localeCompare(b))
.map((adminName) => {
const rows = adminRows.filter((d) => d.admin1_name === adminName);
return buildInsight(rows, adminName, adminTemplate);
})
.filter(Boolean)
.join("\n");
if (countryInsight && adminInsights)
return `${countryInsight}\n${adminInsights}`;
return countryInsight || adminInsights || null;
};climateDataTools = {
const getTrend = (data) => {
if (data.length < 2) return null;
const sorted = [...data].sort((a, b) => a.year - b.year);
const first = sorted[0];
const last = sorted[sorted.length - 1];
const years = last.year - first.year;
const totalChange = last.mean - first.mean;
const perDecade = (totalChange / years) * 10;
return {
startYear: first.year,
endYear: last.year,
totalChange,
perDecade,
};
};
return {
getTrend
}
}Insights - futureProjections
climateProjectionInsight = (iso3) => {
/* ================================
* Templates
* ================================ */
const temperatureTemplate =
":::admin::: is projected to warm by :::tempChange:::°C between :::startYear::: and :::endYear::: under a :::scenarioLabel::: scenario (:::scenario:::), corresponding to :::tempPerDecade:::°C per decade. :::tempComparison::: Model uncertainty remains substantial, with warming projections ranging from :::minAnomaly:::°C (coolest) to :::maxAnomaly:::°C (warmest).";
const precipitationTemplate =
"Precipitation projections under :::scenario::: indicate a change of :::precipChange::: mm per year between :::startYear::: and :::endYear::: on average across :::admin:::. :::precipComparison::: Precipitation uncertainty remains, with projections ranging from :::minPrecip::: to :::maxPrecip::: mm per year across models.";
const tempComparisonTemplate =
"By :::midYear:::, projected warming under :::compareScenario::: is :::diffValue:::°C :::diffDirection::: than under :::baseScenario:::.";
const precipComparisonTemplate =
"Compared to :::baseScenario:::, precipitation change under :::compareScenario::: differs by :::precipDiff::: mm per year by :::midYear:::.";
const adminSummaryTemplate =
"**:::admin:::**: +:::tempChange:::°C (:::tempPerDecade:::°C/decade), :::compareHigh:::–:::compareLow::: difference: :::tempDiff:::°C; precipitation: :::precipChange::: mm/yr.";
/* ================================
* Helpers
* ================================ */
const scenarioLabels = {
ssp126: "very stringent mitigation",
ssp245: "moderately stringent mitigation",
ssp370: "high-emissions",
};
const getMidYear = (timeperiod) => {
const [start, end] = timeperiod.split("-").map(Number);
return Math.round((start + end) / 2);
};
const selectScenarioComparison = (scenarios) => {
if (scenarios.length === 2) return scenarios;
if (scenarios.includes("ssp245") && scenarios.includes("ssp370")) {
return ["ssp245", "ssp370"];
}
if (scenarios.includes("ssp245") && scenarios.includes("ssp585")) {
return ["ssp245", "ssp585"];
}
// fallback: lowest vs highest forcing
const sorted = [...scenarios].sort();
return [sorted[0], sorted[sorted.length - 1]];
};
const buildTempComparison = ({ comparePair, byScenario }) => {
if (!comparePair) return "";
const [base, compare] = comparePair;
const diff =
byScenario[compare][0].mean_anomaly - byScenario[base][0].mean_anomaly;
return Lang.reduceReplaceTemplateItems(tempComparisonTemplate, [
{ name: "midYear", value: getMidYear(byScenario[base][0].timeperiod) },
{ name: "compareScenario", value: compare.toUpperCase() },
{ name: "baseScenario", value: base.toUpperCase() },
{ name: "diffValue", value: Math.abs(diff).toFixed(2) },
{ name: "diffDirection", value: diff > 0 ? "higher" : "lower" },
]);
};
const buildPrecipComparison = ({ comparePair, byScenario }) => {
if (!comparePair) return "";
const [base, compare] = comparePair;
const baseTrend = climateDataTools.getTrend(
byScenario[base].filter((d) => d.hazard === "PTOT"),
);
const compTrend = climateDataTools.getTrend(
byScenario[compare].filter((d) => d.hazard === "PTOT"),
);
if (!baseTrend || !compTrend) return "";
return Lang.reduceReplaceTemplateItems(precipComparisonTemplate, [
{ name: "baseScenario", value: base.toUpperCase() },
{ name: "compareScenario", value: compare.toUpperCase() },
{
name: "precipDiff",
value: (compTrend.perDecade - baseTrend.perDecade).toFixed(1),
},
{
name: "midYear",
value: getMidYear(byScenario[base][0].timeperiod),
},
]);
};
/* ================================
* Data prep
* ================================ */
const data = futureProjections_data;
const country = _lang(iso3ToTranslation[iso3]);
const countryData = data.filter((d) => d.iso3 === iso3 && !d.admin1_name);
const adminData = data.filter((d) => d.iso3 === iso3 && d.admin1_name);
/* ================================
* Country-level narrative
* ================================ */
const buildCountryInsight = (rows, adminName) => {
if (!rows.length) return null;
// group by scenario
const byScenario = rows.reduce((acc, d) => {
acc[d.scenario] = acc[d.scenario] || [];
acc[d.scenario].push(d);
return acc;
}, {});
const scenarios = Object.keys(byScenario);
if (!scenarios.length) return null;
// comparison logic (shared everywhere)
const comparePair =
scenarios.length >= 2 ? selectScenarioComparison(scenarios) : null;
// primary scenario = higher forcing if comparison exists
const primaryScenario = comparePair ? comparePair[1] : scenarios[0];
/* ================================
* Temperature paragraph
* ================================ */
const tempData = byScenario[primaryScenario].filter(
(d) => d.hazard === "HSH-max",
);
const tempTrend = climateDataTools.getTrend(tempData);
if (!tempTrend) return null;
const tempComparison = buildTempComparison({ comparePair, byScenario });
const uTemp = byScenario[comparePair ? comparePair[1] : primaryScenario][0];
const temperatureParagraph = Lang.reduceReplaceTemplateItems(
temperatureTemplate,
[
{ name: "admin", value: adminName },
{ name: "scenario", value: primaryScenario.toUpperCase() },
{
name: "scenarioLabel",
value: scenarioLabels[primaryScenario] || "climate",
},
{
name: "tempChange",
value: Math.abs(tempTrend.totalChange).toFixed(2),
},
{ name: "startYear", value: tempTrend.startYear },
{ name: "endYear", value: tempTrend.endYear },
{
name: "tempPerDecade",
value: Math.abs(tempTrend.perDecade).toFixed(2),
},
{ name: "tempComparison", value: tempComparison },
{
name: "minAnomaly",
value: Math.abs(uTemp.min_anomaly).toFixed(2),
},
{
name: "maxAnomaly",
value: Math.abs(uTemp.max_anomaly).toFixed(2),
},
],
);
/* ================================
* Precipitation paragraph
* ================================ */
const precipData = byScenario[primaryScenario].filter(
(d) => d.hazard === "PTOT",
);
const precipTrend = climateDataTools.getTrend(precipData);
let precipitationParagraph = null;
if (precipTrend) {
const precipComparison = buildPrecipComparison({
comparePair,
byScenario,
});
const uPrecipData = byScenario[
comparePair ? comparePair[1] : primaryScenario
].filter((d) => d.hazard === "PTOT");
precipitationParagraph = Lang.reduceReplaceTemplateItems(
precipitationTemplate,
[
{ name: "admin", value: adminName },
{ name: "scenario", value: primaryScenario.toUpperCase() },
{
name: "precipChange",
value: precipTrend.perDecade.toFixed(1),
},
{ name: "startYear", value: precipTrend.startYear },
{ name: "endYear", value: precipTrend.endYear },
{ name: "precipComparison", value: precipComparison },
{
name: "minPrecip",
value: Math.min(...uPrecipData.map((d) => d.mean)).toFixed(1),
},
{
name: "maxPrecip",
value: Math.max(...uPrecipData.map((d) => d.mean)).toFixed(1),
},
],
);
}
/* ================================
* Final output (2 paragraphs max)
* ================================ */
return precipitationParagraph
? `${temperatureParagraph}\n\n${precipitationParagraph}`
: temperatureParagraph;
};
/* ================================
* Admin-level compact summaries
* ================================ */
const buildAdminSummary = (rows, adminName) => {
if (!rows.length) return null;
const byScenario = rows.reduce((acc, d) => {
acc[d.scenario] = acc[d.scenario] || [];
acc[d.scenario].push(d);
return acc;
}, {});
const scenarios = Object.keys(byScenario);
const comparePair =
scenarios.length >= 2 ? selectScenarioComparison(scenarios) : null;
const primaryScenario = comparePair
? comparePair[1] // higher forcing
: scenarios[0];
// temperature trend
const tempTrend = climateDataTools.getTrend(
byScenario[primaryScenario].filter((d) => d.hazard === "HSH-max"),
);
if (!tempTrend) return null;
// precipitation trend (optional)
const precipTrend = climateDataTools.getTrend(
byScenario[primaryScenario].filter((d) => d.hazard === "PTOT"),
);
// temperature difference (mean anomaly only)
let tempDiff = "n/a";
if (comparePair) {
const [base, compare] = comparePair;
const baseVal = byScenario[base][0].mean_anomaly;
const compVal = byScenario[compare][0].mean_anomaly;
tempDiff = Math.abs(compVal - baseVal).toFixed(2);
}
const [compareLow, compareHigh] = comparePair ?? [];
return Lang.reduceReplaceTemplateItems(adminSummaryTemplate, [
{ name: "admin", value: adminName },
{
name: "tempChange",
value: Math.abs(tempTrend.totalChange).toFixed(2),
},
{
name: "tempPerDecade",
value: Math.abs(tempTrend.perDecade).toFixed(2),
},
{ name: "tempDiff", value: tempDiff },
{ name: "compareLow", value: compareLow?.toUpperCase() || "—" },
{ name: "compareHigh", value: compareHigh?.toUpperCase() || "—" },
{
name: "precipChange",
value: precipTrend ? precipTrend.perDecade.toFixed(1) : "n/a",
},
]);
};
/* ================================
* Assemble output
* ================================ */
const countryInsight = buildCountryInsight(countryData, country);
const adminInsights =
adminData.length > 0
? [
"**Subnational patterns**",
...[...new Set(adminData.map((d) => d.admin1_name))].map(
(admin1Name) => {
const rows = adminData.filter(
(d) => d.admin1_name === admin1Name,
);
return `- ${buildAdminSummary(rows, admin1Name)}`;
},
),
].join("\n")
: "";
return adminInsights
? `${countryInsight}\n\n${adminInsights}`
: countryInsight;
};Figures
Figures - Key facts
exposureBars_keyFacts = function () {
const dataCategories = exposure_plotData;
let _data;
// If no prod_type, aggregate by category
if (!prod_type) {
_data = Array.from(
d3.rollup(
dataCategories,
(v) => ({
value: d3.sum(v, (d) => d.value),
iso3: v[0].iso3,
admin1_name: v[0].admin1_name,
}),
(d) => d.adminName,
(d) => d.category,
),
([adminName, catMap]) =>
Array.from(catMap, ([category, obj]) => ({
adminName,
crop: category,
value: obj.value,
iso3: obj.iso3,
admin1_name: obj.admin1_name,
})),
).flat();
} else {
_data = dataCategories.filter((d) => d.category === prod_type);
}
if (_data.length === 0) {
//TODO: make this a function
return html`
<div style="
width: ${width}px;
height: 400px;
display: flex;
align-items: center;
justify-content: center;
background: #f5f5f5;
border-radius: 4px;
color: #666;
font-size: 16px;
">
No data available for the plot
</div>
`;
}
return Plot.plot({
width,
height: 400,
marginLeft: 70,
marginBottom: 50,
marginTop: 30,
subtitle: "Agricultural Production",
caption: multiLineText(
[
"Data is from 2020, measured in 2021 US dollars",
"Source: MapSpam 2020 SSA Adaptation Atlas",
],
"atlasFigCaption",
),
x: {
axis: "bottom",
grid: true,
label: "Value of Production",
tickFormat: formatUSD(),
},
y: {
label: null,
tickFormat: (d) => wrapTickLabel(_lang(cropTranslations[d])),
},
facet: {
data: _data,
x: "adminName",
label: null,
},
color: {
type: "linear",
range: ["#F7D732", "#216729"],
domain: d3.extent(_data, (d) => d.value),
},
marks: [
Plot.barX(_data, {
x: "value", // summed value
y: "crop",
fill: "value", // color by total
sort: { y: "x", reverse: true },
channels: {
crop: {
label: "Crop",
value: "crop",
},
a1: {
label: "Region",
value: "admin1_name",
},
a0: {
label: "Country",
value: (d) => _lang(iso3ToTranslation[d.iso3]),
},
},
tip: {
format: {
a0: true,
a1: true,
crop: (d) => _lang(cropTranslations[d]),
// x: formatUSD(),
fill: false,
y: false,
fx: false,
},
},
}),
],
});
};function createStackedBarChart({
data,
// Required field mappings
xField = "percentage", // Field for bar width
yField = "admin0_name", // Field for faceting/grouping
fillField = "sector", // Field for coloring segments
// Chart dimensions
width,
heightPerRow = 100,
// Color configuration
fillColors = {},
fillLabelFormatter = (d) => d,
// Labels and text
title = "",
caption = "",
// Text formatting
textFormatter = (d) => `${d[xField].toFixed(1)}%`,
// Tooltip configuration
channels = {}, // Custom channels for tooltips
tooltipFormat = {}, // Which fields to show in tooltip
}) {
const _data = data.filter((d) => d[xField] != null && d[fillField] != null);
// Calculate number of unique groups for height
const uniqueGroups = new Set(_data.map((d) => d[yField]));
const numGroups = uniqueGroups.size;
// Setup color scale
const dataCategories = new Set(_data.map((d) => d[fillField]));
const fillDomain = Object.keys(fillColors).filter((k) =>
dataCategories.has(k),
);
const fillRange = fillDomain.map((d) => fillColors[d]);
const colorScale = d3.scaleOrdinal().domain(fillDomain).range(fillRange);
const marks = [
Plot.barX(
_data,
Plot.stackX({
x: xField,
y: yField,
fill: (d) => d[fillField],
channels: channels,
tip: {
dy: 6,
format: {
fill: false,
x: false,
y: false,
...tooltipFormat,
},
},
}),
),
Plot.textX(
_data.filter((d) => d[xField] > 3), //Assume labels under 1% are too small to show well
Plot.stackX({
x: xField,
y: yField,
text: textFormatter,
fill: "#fff",
fontWeight: "bold",
}),
),
];
return Plot.plot({
style: {
overflow: "visible",
},
subtitle: title,
caption,
width,
height: heightPerRow * numGroups,
x: {
axis: null,
},
y: {
label: null,
tickSize: 0,
},
color: {
domain: fillDomain,
range: fillRange,
legend: true,
label: fillLabelFormatter,
},
marks,
});
}gdpBar_keyFacts = () => {
return createStackedBarChart({
data: gdp_plotData.filter((d) => d.sector !== "total"),
xField: "pct_of_total",
yField: "iso3",
fillField: "sector",
width: width,
fillColors: {
agriculture: "#74B95A",
industry: "#8C3E5F",
manufacturing: "#B34E65",
services: "#523D4E",
},
fillLabelFormatter: (d) => (d) => d.toUpperCase(),
title: "Country GDP by Sector",
caption: multiLineText(
["Data is from 2022, measured in 2015 US dollars", "Source: FAOSTAT"],
"atlasFigCaption",
),
channels: {
pct: {
value: "pct_of_total",
label: "% of GDP",
},
group: {
value: "sector",
label: "Sector",
},
admin0: {
value: (d) => _lang(iso3ToTranslation[d.iso3]),
label: "Country",
},
},
tip: {
pct: true,
group: true,
admin0: true,
},
});
};
areaBar_keyFacts = () => {
return createStackedBarChart({
data: landuse_plotData.filter((d) => d.sector !== "total"),
xField: "ha_pct",
yField: "iso3",
fillField: "sub_group",
width: width,
fillColors: {
"Permanent meadows and pastures": "#E8C654",
Cropland: "#E0A810",
"Naturally regenerating forest": "#4C843A",
"Planted Forest": "#8DB878",
"Other land": "#955D7C",
"Land used for aquaculture": "#4FB5B7",
},
fillLabelFormatter: (d) => (d) => d.toUpperCase(),
title: "Country Land Use by Sector",
caption: multiLineText(
["Data is based on 2021 values", "Source: FAOSTAT"],
"atlasFigCaption",
),
channels: {
pct: {
value: "ha_pct",
label: "% of area",
},
group: {
value: "sub_group",
label: "Group",
},
admin0: {
value: (d) => _lang(iso3ToTranslation[d.iso3]),
label: "Country",
},
},
tip: {
pct: true,
group: true,
admin0: true,
},
});
};povBar_keyFacts = () => {
return createStackedBarChart({
data: pov_plotData,
xField: "pov_rate",
yField: "adminName",
fillField: "group",
width: width,
fillColors: {
in_poverty: "#EC5A47",
not_poverty: "#F4BB21",
},
fillLabelFormatter: (d) => (d) => d.toUpperCase(),
title: "Poverty Rates",
caption: multiLineText(
[
"Data uses purchasing power parity (PPP) values for 2023. Poverty threshold is $4.20 USD/day.",
"Source: World Bank GSAP 2025.",
],
"atlasFigCaption",
),
channels: {
rate: {
value: (d) => d.pov_rate + "%",
label: "% of populatio,n",
},
group: {
value: "group",
label: "Group",
},
admin0: {
value: (d) => _lang(iso3ToTranslation[d.iso3]),
label: "Country",
},
},
tip: {
rate: true,
group: true,
admin0: true,
},
});
};Recent changes
barplot_recentChanges = () => {
const _data = recentChanges_plotData;
const _season = seasonSelect.season_string;
const _variableName = _lang(climateVarSelect.name);
const _variableAxis = _lang(climateVarSelect.labelAxis);
const _admin1 = admin1Select.admin1_name;
const _admin0 = _lang(admin0Select.translation);
const _mainTitle = `${_variableName} (${_lang(general_translations.anomaly)})`;
const _reverse = ["PTOT"].includes(climateVarSelect.id);
// 1. Determine the min and max for consistent axis scaling
const maxAnomaly = Math.max(..._data.map((d) => d.mean_anomaly));
const minAnomaly = Math.min(..._data.map((d) => d.mean_anomaly));
const absMax = Math.max(Math.abs(minAnomaly), Math.abs(maxAnomaly));
const chart = Plot.plot({
// --- General Configuration ---
width,
height: 400,
marginTop: 30,
title: _mainTitle,
subtitle: `Season: ${_season}`,
// --- Axis Scales ---
x: {
label: "Year",
tickFormat: "d",
},
y: {
// Ensure the Y-axis is also symmetric around 0 for visual balance
domain: [-absMax, absMax],
grid: true,
label: _variableAxis,
},
facet: { data: _data, y: "adminName", label: null },
// --- Color Scale ---
color: {
scheme: "BuRd",
type: "diverging",
reverse: _reverse,
legend: true,
label: `${_variableAxis} (${_lang(general_translations.anomaly)})`,
},
// --- Marks ---
marks: [
// A single bar mark is used, with 'fill' set to the anomaly value
Plot.barY(_data, {
x: "year",
y: "mean_anomaly",
// The color scale defined above will automatically color and grade the bars
fill: "mean_anomaly",
tip: true,
channels: {
Admin: "adminName",
},
tip: {
format: {
y: false,
fy: false,
Admin: true,
x: (d) => d,
color: true,
},
},
}),
// Add a black line at y=0 for a clear baseline
Plot.ruleY([0], { stroke: "black", strokeWidth: 1.5 }),
],
});
return chart;
};warmingStripes_recentChanges = () => {
const _data = recentChanges_plotData;
const _season = seasonSelect.season_string;
const _variableName = _lang(climateVarSelect.name);
const _variableAxis = _lang(climateVarSelect.labelAxis);
const _admin1 = admin1Select.admin1_name;
const _admin0 = _lang(admin0Select.translation);
const _showAnomaly = true;
const _anomalyText = _showAnomaly
? `(${_lang(general_translations.anomaly)})`
: "";
const _mainTitle = `${_variableName}${_anomalyText}`;
const plotValue = _showAnomaly ? "mean_anomaly" : "mean";
const lineColor = "#000";
const yMax = d3.max(_data, (d) => d[plotValue]);
const yMin = d3.min(_data, (d) => d[plotValue]);
const yExtent = Math.abs(yMax - yMin);
const yBufferPerc = 0.05;
const yBuffer = yExtent * yBufferPerc;
const yDomain = [yMin - yBuffer, yMax + yBuffer];
const _reverse = ["PTOT"].includes(climateVarSelect.id);
const warmingStripes = Plot.plot({
width,
height: 350,
title: _mainTitle,
subtitle: `Season: ${_season}`,
// caption: caption,
x: {
label: "Year",
tickFormat: "d",
padding: 0,
},
y: {
// axis: ws_iToggleLine ? "left" : null,
label: `${_variableAxis} ${_anomalyText}`,
domain: yDomain,
},
facet: { data: _data, y: "adminName", label: null },
color: {
scheme: _showAnomaly ? "BuRd" : "Reds",
legend: true,
label: _variableAxis,
reverse: _reverse,
},
marks: [
// stripes
Plot.barY(_data, {
x: "year",
y1: yDomain[0],
y2: yDomain[1],
fill: (d) => d[plotValue],
}),
// zero line
Plot.ruleY(_showAnomaly ? [0] : [], {
strokeDasharray: [5],
stroke: lineColor,
}),
// line
Plot.line(_data, {
x: "year",
y: (d) => d[plotValue],
stroke: lineColor,
}),
// dots
Plot.dot(_data, {
x: "year",
y: (d) => d[plotValue],
fill: lineColor,
}),
// tooltip
Plot.tip(
_data,
Plot.pointerX({
x: "year",
y: (d) => yMin,
tip: true,
channels: {
Admin: "adminName",
varMean: {
label: _variableAxis,
value: (d) => d.mean,
},
varMeanAnomaly: {
label: _lang(general_translations.anomaly),
value: (d) => d.mean_anomaly,
},
},
format: {
y: false,
fy: false,
Admin: true,
x: (d) => d,
varMean: true,
varMeanAnomaly: true,
},
lineWidth: Infinity,
}),
),
],
});
return warmingStripes;
};futureProjections
timeseries_futureProjections = () => {
const _data = futureProjections_plotData;
const _season = seasonSelect.season_string;
const _variableName = _lang(climateVarSelect.name);
const _variableAxis = _lang(climateVarSelect.labelAxis);
const _admin1 = admin1Select.admin1_name;
const _admin0 = _lang(admin0Select.translation);
const _showAnomaly = true;
const _anomalyText = _showAnomaly
? ` (${_lang(general_translations.anomaly)})`
: "";
const _mainTitle = `${_variableName}${_anomalyText}`;
const plotValue_mean = _showAnomaly ? "mean_anomaly" : "mean";
const plotValue_max = _showAnomaly ? "max_anomaly" : "max";
const plotValue_min = _showAnomaly ? "min_anomaly" : "min";
const formatScenario = (d) => d?.toUpperCase();
const scenariosInData = [
...new Set(_data.map((d) => formatScenario(d.scenario))),
];
return Plot.plot({
width,
title: _mainTitle,
// caption: caption,
x: {
tickFormat: "d",
label: null,
},
y: {
axis: "right",
grid: true,
label: _variableAxis,
tickSize: 0,
},
facet: { data: _data, y: "adminName", label: null },
color: {
domain: scenariosInData,
range: scenariosInData.map((s) => scenarioColors[s]),
legend: true,
},
marks: [
Plot.ruleY(_showAnomaly ? [0] : [], {
strokeDasharray: [5],
}),
Plot.line(_data, {
x: "year",
y: plotValue_mean,
stroke: (d) => formatScenario(d.scenario),
}),
Plot.areaY(_data, {
x: "year",
y1: plotValue_max,
y2: plotValue_min,
fill: (d) => formatScenario(d.scenario),
fillOpacity: 0.2,
}),
Plot.dot(_data, {
x: "year",
y: plotValue_mean,
fill: (d) => formatScenario(d.scenario),
// tip: true
tip: {
channels: {
scenario: {
label: _lang(general_translations.scenario),
value: (d) => formatScenario(d.scenario),
},
year: {
label: _lang(general_translations.year),
value: (d) => d3.format("d")(d.year),
},
Admin: "adminName",
varMean: {
label: _variableAxis,
value: (d) => d.mean,
},
varMeanAnomaly: {
label: _lang(general_translations.anomaly),
value: (d) => d.mean_anomaly,
},
},
format: {
Admin: true,
scenario: true,
year: true,
varMean: true,
varMeanAnomaly: true,
// value: d => `${d} ${plotField.unit}`,
x: false,
y: false,
fy: false,
fill: false,
},
},
}),
Plot.linearRegressionY(false ? _data : [], {
x: "year",
y: plotValue_mean,
stroke: (d) => formatScenario(d.scenario),
strokeDasharray: [5],
fill: null,
}),
],
});
};extremeEvents
bars_extremeEvents = () => {
const categories = [
"extreme_low",
"unusual_low",
"unusual_high",
"extreme_high",
];
const interp = (t) => d3.interpolateRdBu(1 - t);
const colors = categories.map((_, i) => interp(i / (categories.length - 1)));
const _data = extremeEvents_plotData;
return Plot.plot({
width,
height: 500,
marginLeft: 60,
marginBottom: 60,
facet: {
data: _data,
},
fy: {
label: null,
},
x: {
label: "Scenario",
},
fx: {
domain: categories,
label: "Category",
},
y: {
label: "Number of events",
grid: true,
},
color: {
legend: true,
domain: categories,
range: colors,
label: "Category",
},
marks: [
Plot.barY(_data, {
x: "scenario",
fx: "category",
fy: "adminName",
y: "num_events",
fill: "category",
tip: true,
}),
],
});
};
// bars_extremeEvents = () => {
// const categories = [
// "extreme_low",
// "unusual_low",
// "unusual_high",
// "extreme_high",
// ];
// const interp = (t) => d3.interpolateRdBu(1 - t);
// const colors = categories.map((_, i) => interp(i / (categories.length - 1)));
// const colorScale = d3.scaleOrdinal(categories, colors);
// const _data = extremeEvents_plotData;
//
// return Plot.plot({
// width,
// height: 420,
// marginLeft: 60,
// marginBottom: 50,
// facet: {
// data: _data,
// x: "adminName",
// },
// x: {
// domain: ["historical", "ssp245", "ssp585"],
// },
// color: {
// legend: true,
// domain: categories,
// range: colors,
// },
// marks: [
// Plot.barY(_data, {
// x: "scenario",
// y: "num_events",
// fill: (d) => colorScale(d.category),
// tip: true,
// }),
// ],
// });
// };hazardExposure
stackbars_hazardExposure = () => {
// --- Assign category to each crop (same as first chart) ---
const dataWithCategory = hazardExposure_plotData.map((row) => {
const category = cropCategoryMap.get(row.crop) || "other";
if (category === "other") console.log(row.crop);
return { ...row, category };
});
// --- Filter to only needed timeframe/scenario AND remove "any" ---
let baseFiltered = dataWithCategory.filter(
(d) =>
d.hazard !== "any" &&
["1995-2014", "2021-2040"].includes(d.timeframe) &&
["historic", "ssp245", "ssp585"].includes(d.scenario),
);
// --- Apply prod_type logic (same as first chart) ---
let dataForAggregation;
if (!prod_type) {
const grouped = d3.rollup(
baseFiltered,
(v) => ({
value: d3.sum(v, (d) => d.value),
iso3: v[0].iso3,
admin1_name: v[0].admin1_name,
}),
(d) =>
`${d.adminName}|${d.category}|${d.timeframe}|${d.scenario}|${d.hazard}`,
);
dataForAggregation = Array.from(grouped, ([key, data]) => {
const [adminName, category, timeframe, scenario, hazard] = key.split("|");
return {
adminName,
iso3: data.iso3,
admin1_name: data.admin1_name,
crop: category,
timeframe,
scenario,
hazard,
value: data.value,
};
});
} else {
// Filter to only the selected category
dataForAggregation = baseFiltered.filter((d) => d.category === prod_type);
}
// --- 5. Hazards stacked in defined order ---
const _hazards = [
"wet",
"dry",
"heat",
"dry+heat",
"dry+wet",
"heat+wet",
"heat+wet+dry",
];
if (dataForAggregation.length === 0) {
return html`
<div style="
width: ${width}px;
height: 400px;
display: flex;
align-items: center;
justify-content: center;
background: #f5f5f5;
border-radius: 4px;
color: #666;
font-size: 16px;
">
No data available for the plot
</div>
`;
}
// --- 6. Final hazard plot by category (or filtered category) ---
return Plot.plot({
width,
height: 400,
marginLeft: 100,
color: {
legend: true,
range: [
"#4FB5B7",
"#FCC42C",
"#FC8A34",
"#EE624F",
"#B34E65",
"#8C3E5F",
"#523D4E",
"#EFEFEF",
],
domain: _hazards,
},
x: {
ticks: 1,
axis: "top",
grid: true,
label: "Value",
tickFormat: formatUSD(),
},
y: {
label: null,
tickFormat: (d) => wrapTickLabel(_lang(cropTranslations[d])),
},
facet: {
data: dataForAggregation,
x: "scenario",
y: "adminName",
label: null,
},
marks: [
Plot.barX(
dataForAggregation,
Plot.stackX(
{ order: _hazards },
{
x: "value",
y: "crop",
fill: "hazard",
stroke: "#fff",
strokeWidth: 0.25,
channels: {
crop: {
label: "Crop",
value: (d) => _lang(cropTranslations[d.crop]),
},
a1: {
label: "Region",
value: "admin1_name",
},
a0: {
label: "Country",
value: (d) => _lang(iso3ToTranslation[d.iso3]),
},
scenario: {
lable: "Scenario",
value: (d) => d.scenario,
},
},
tip: {
format: {
a0: true,
a1: true,
crop: true,
x: formatUSD(),
fill: false,
y: false,
fy: false,
fx: false,
},
},
},
),
),
],
});
};