{
const Plot = await import("https://cdn.jsdelivr.net/npm/@observablehq/plot@0.6/+esm");
const d3 = await import("https://cdn.jsdelivr.net/npm/d3@7/+esm");
const topojson = await import("https://cdn.jsdelivr.net/npm/topojson-client@3/+esm");
/**
* Extracts column names that represent years between 1990 and 2019 from the input data.
* @type {string[]}
*/
const yearColumns = data.length > 0
? Object.keys(data[0]).filter(key => /^\d{4}$/.test(key) && key >= "1990" && key <= "2019")
: [];
/**
* Loads country code mapping data from the attached "all.csv" file.
* Expected format includes 'alpha-3' and 'country-code' (numeric) columns.
* @type {Array<object>} // Adjust specific type if known, e.g., { "alpha-3": string; "country-code": number; ... }
*/
const countryCodes = await FileAttachment("all.csv").csv({typed: true});
/**
* Creates a Map for efficient lookup from 3-letter ISO country code (alpha-3)
* to the 3-digit numeric ISO country code.
* @type {Map<string, number>}
*/
const alpha3ToNumericMap = new Map(countryCodes.map(d => [d["alpha-3"], d["country-code"]]));
/**
* Represents a single data point in the transformed long format.
* @typedef {object} LongDataRowMap
* @property {string} Country Name - The name of the country.
* @property {string} Region - The region the country belongs to.
* @property {number} Year - The year of the data point.
* @property {number | null} Value - The CO₂ emissions per capita (metric tons), or null if missing/invalid.
* @property {string} country_code - The 3-letter ISO alpha-3 country code.
* @property {number | undefined} iso_n3 - The 3-digit ISO numeric country code (potentially undefined if lookup failed).
*/
/**
* Transforms wide data to long format, adds numeric ISO codes, and filters invalid rows.
* Each row represents a country-year observation with its CO₂ value and ISO code.
* @type {Array<Omit<LongDataRowMap, 'Value' | 'iso_n3'> & { Value: number; iso_n3: number }>}
*/
const longData = data.flatMap(country => {
const countryName = country["Country Name"];
const countryCodeAlpha3 = country["country_code"];
const region = country["Region"];
// Look up the numeric ISO code using the alpha-3 code
const iso_n3 = alpha3ToNumericMap.get(countryCodeAlpha3);
// Skip if country name or numeric ISO code lookup failed
if (!countryName || !iso_n3) return [];
return yearColumns.map(year => {
const value = country[year];
const parsedValue = parseFloat(value);
return /** @type {LongDataRowMap} */ ({
"Country Name": countryName,
"Region": region,
"Year": +year,
"Value": (value == null || value === "" || isNaN(parsedValue))
? null
: parsedValue,
"country_code": countryCodeAlpha3,
"iso_n3": iso_n3 // Add the numeric code
});
});
}).filter(
/**
* Type predicate ensuring Value and iso_n3 are non-null.
* @param {LongDataRowMap} d
* @returns {d is Omit<LongDataRowMap, 'Value' | 'iso_n3'> & { Value: number; iso_n3: number }}
*/
d => d.Value != null && d.iso_n3 != null
); // Filter out rows with null Value or failed ISO lookup
// Load Geographic Data
/**
* Loads world map boundary data in TopoJSON format.
* @type {object} // Specific TopoJSON type structure if needed
*/
const world = await d3.json("https://cdn.jsdelivr.net/npm/world-atlas@2/countries-110m.json");
/**
* Converts the TopoJSON country objects into a GeoJSON FeatureCollection.
* The `id` property of features in this dataset corresponds to the numeric ISO code.
* @type {object} // Specific GeoJSON FeatureCollection type structure if needed
*/
const countries = topojson.feature(world, world.objects.countries);
// --- Prepare Data for Plotting ---
/**
* Array of specific years to generate maps for.
* @type {number[]}
*/
const plotYears = [1990, 2000, 2010, 2019];
/**
* Filters the longData to include only the data for the selected plotYears.
* @type {Array<Omit<LongDataRowMap, 'Value' | 'iso_n3'> & { Value: number; iso_n3: number }>}
*/
const plotData = longData.filter(d => plotYears.includes(d.Year));
/**
* Array of CO₂ values for the selected years, sorted ascendingly.
* Used for calculating percentile-based color domain.
* @type {number[]}
*/
const values = plotData.map(d => d.Value).sort(d3.ascending);
/**
* The 1st percentile of CO₂ values across the selected years.
* @type {number | undefined}
*/
const p01 = d3.quantile(values, 0.01);
/**
* The 99th percentile of CO₂ values across the selected years.
* @type {number | undefined}
*/
const p99 = d3.quantile(values, 0.99);
/**
* The domain (min, max) for the color scale, based on percentiles to reduce outlier impact.
* Ensures the lower bound is not negative.
* @type {[number, number]}
*/
const colorDomain = [p01 < 0 ? 0 : p01 ?? 0, p99 ?? d3.max(values) ?? 0]; // Use fallbacks if percentiles are undefined
/**
* Creates a Map for efficient lookup from numeric ISO code to the country name
* from the original dataset (useful for tooltips).
* @type {Map<number, string>}
*/
const idToNameMap = new Map(longData.map(d => [d.iso_n3, d["Country Name"]]));
// Plotting Function
/**
* Creates a single choropleth map plot for a given year.
* @param {number} year - The year for which to create the map.
* @returns {HTMLElement} An HTML element containing the rendered SVG map.
*/
function createMapPlot(year) {
/**
* Creates a Map for efficient lookup from numeric ISO code to CO₂ value
* specifically for the given input year.
* @type {Map<number, number>}
*/
const yearDataMap = new Map(
plotData
.filter(d => d.Year === year)
.map(d => [d.iso_n3, d.Value])
);
// Define and return the Observable Plot configuration.
return Plot.plot({
title: `CO₂ Emissions per Capita (${year})`,
subtitle: "Metric tons per capita. Grey indicates no data.",
projection: "equal-earth", // A projection suitable for thematic world maps
width: 900,
style: { background: "white" }, // Ensure plot background is white
// Color scale configuration
color: {
type: "sequential", // Continuous data implies sequential scale
scheme: "ylgnbu", // Color scheme (Yellow-Green-Blue)
domain: colorDomain, // Use the pre-calculated consistent domain
label: "↑ CO₂ Emissions (t/capita)", // Label for the legend
legend: "ramp", // Use a continuous color ramp for the legend
clamp: true, // Clamp values outside the domain to the min/max colors
unknown: "#ccc" // Color for map features with no matching data
},
// Define the visual marks
marks: [
// Mark 1: Draw the outline of the globe
Plot.sphere({ stroke: "black", strokeWidth: 0.5 }),
// Mark 2: Draw the countries using GeoJSON features
Plot.geo(countries, {
// Fill color is determined by looking up the country's ID (numeric ISO code)
// in the year-specific data map.
fill: (d) => yearDataMap.get(+d.id), // `+d.id` ensures the ID is treated as a number
stroke: "white", // Add subtle white borders between countries
strokeWidth: 0.25,
/**
* Defines the tooltip content when hovering over a country.
* @param {object} d - The GeoJSON feature data object for the hovered country. Contains `id`.
* @returns {string} The formatted tooltip text.
*/
title: (d) => {
const countryId = +d.id;
// Look up the country name using the ID map
const countryName = idToNameMap.get(countryId) ?? `Unknown ID: ${countryId}`;
// Look up the CO₂ value using the year-specific map
const value = yearDataMap.get(countryId);
// Format the tooltip string
return `${countryName}\n${year}: ${value != null ? value.toFixed(2) + ' t' : 'No data'}`;
}
})
]
});
}
// Generate plots for each selected year
/**
* An array containing the generated map plot elements for each year in plotYears.
* @type {HTMLElement[]}
*/
const mapPlots = plotYears.map(year => createMapPlot(year));
// Display the plots in a grid using HTML
/**
* Returns an HTML container element that arranges the generated maps in a responsive grid.
* @returns {HTMLDivElement} The container div holding the map plots.
*/
return html`
<div style="
display: grid;
/* Create columns that automatically fill, each at least 300px wide */
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
gap: 16px; /* Spacing between grid items */
align-items: start; /* Align items to the top of their grid cell */
">
${mapPlots} <!-- Embed the array of plot elements directly -->
</div>
`;
}