Public
Edited
Apr 29
Insert cell
Insert cell
data = FileAttachment("CO2_emission.csv").csv({ typed: true })
Insert cell
Insert cell
/**
* @fileoverview Creates a line chart visualizing CO₂ emissions per capita over time,
* highlighting the top 10 highest emitting countries (based on latest data)
* and labeling the top 5. Other countries are shown as faint grey lines.
* Includes interactive tooltips.
* Assumes running in an Observable notebook environment with access to FileAttachment.
*
* @requires D3.js (v7) - For data manipulation and utilities.
* @requires Observable Plot (v0.6) - For chart creation.
* @assume The 'data' variable is pre-loaded in the Observable environment
* (e.g., from a FileAttachment("CO2_emission.csv").csv({ typed: true }) cell).
*/
{
/**
* Observable Plot library instance.
* @external Plot
*/
const Plot = await import("https://cdn.jsdelivr.net/npm/@observablehq/plot@0.6/+esm");

/**
* D3.js library instance.
* @external d3
*/
const d3 = await import("https://cdn.jsdelivr.net/npm/d3@7/+esm");

// Data Transformation

/**
* Extracts column names that represent years between 1990 and 2019.
* @type {string[]}
*/
const yearColumns = data.length > 0
? Object.keys(data[0]).filter(key => /^\d{4}$/.test(key) && key >= "1990" && key <= "2019")
: [];

/**
* Transforms the wide-format input data into a long format suitable for plotting time series.
* Each row represents a single country in a single year with its CO₂ emission value.
* Rows with null/invalid values are filtered out.
*
* @typedef {object} LongDataRow
* @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} Value - The CO₂ emissions per capita (metric tons).
*/

/**
* Array containing the long-format CO₂ emission data.
* @type {LongDataRow[]}
*/
const longData = data.flatMap(country => {
const countryName = country["Country Name"];
const region = country["Region"];
// Skip rows without a valid country name
if (!countryName) return [];
// Map each year column to a separate row object
return yearColumns.map(year => {
const value = country[year];
const parsedValue = parseFloat(value);
return {
"Country Name": countryName,
"Region": region,
"Year": +year, // Convert year string to number
"Value": (value == null || value === "" || isNaN(parsedValue))
? null // Handle missing or non-numeric data
: parsedValue
};
});
}).filter(d => d.Value != null); // Filter out rows where Value is null

// Get the top emitters

/**
* Finds the data record corresponding to the latest available year for each country.
* Uses d3.rollup to group by country and reduce to find the max Year entry.
* @type {Map<string, LongDataRow>}
*/
const latestDataPerCountryMap = d3.rollup(
longData,
vs => vs.reduce((a, b) => b.Year > a.Year ? b : a), // Find entry with the latest year
d => d["Country Name"]
);

/**
* Array containing the latest data record for each country.
* @type {LongDataRow[]}
*/
const latestDataPerCountry = Array.from(latestDataPerCountryMap.values());

// Sort countries by their latest emission value in descending order.
latestDataPerCountry.sort((a, b) => b.Value - a.Value);

/**
* Contains data for the top 10 countries based on latest emissions, including their rank.
* @type {Array<{name: string, rank: number}>}
*/
const top10CountriesData = latestDataPerCountry
.slice(0, 10)
.map((d, i) => ({ name: d["Country Name"], rank: i })); // rank 0 is highest emitter

/**
* A Set containing the names of the top 10 emitting countries for quick lookups.
* @type {Set<string>}
*/
const top10Countries = new Set(top10CountriesData.map(d => d.name));

/**
* A Map associating the name of each top 10 country with its rank (0-9).
* @type {Map<string, number>}
*/
const top10RankMap = new Map(top10CountriesData.map(d => [d.name, d.rank]));

// Prepare Label Data

/**
* Generates data specifically for placing labels at the end of the lines for the top 5 countries.
* Includes coordinates (Year, Value) and rank for positioning/styling.
*
* @typedef {object} LabelDataRow
* @property {string} Country Name - The name of the country.
* @property {number} Year - The year of the last data point for the country.
* @property {number} Value - The CO₂ emission value at the last data point.
* @property {number} rank - The rank (0-4) of the country among the top 10.
*/

/**
* Array containing data points for labeling the top 5 lines.
* @type {LabelDataRow[]}
*/
const top5LabelData = top10CountriesData.slice(0, 5).map(({ name, rank }) => {
// Find all data points for this country
const pts = longData.filter(d => d["Country Name"] === name);
// Find the data point for the latest year
const last = pts.reduce((a, b) => b.Year > a.Year ? b : a);
return {
"Country Name": name,
Year: last.Year,
Value: last.Value,
rank: rank // Add rank for label positioning (dy)
};
});

/**
* Filters the longData to include only countries NOT in the top 10.
* Used for drawing the background grey lines.
* @type {LongDataRow[]}
*/
const otherCountriesData = longData.filter(d => !top10Countries.has(d["Country Name"]));

// Create Line Chart

/**
* Generates the final line chart using Observable Plot.
* @returns {HTMLElement} An HTML element containing the rendered SVG chart.
*/
return Plot.plot({
title: "Top 10 highest CO₂ Emissions per Capita",
subtitle: "Background lines in grey. Hover for details.",
height: 500,
width: 900,
marginTop: 50,
marginBottom: 50,
marginLeft: 70,
marginRight: 180, // Extra space for end-of-line labels
// X-axis configuration (Year)
x: {
label: "Year",
tickFormat: d3.format("d"), // Display years as integers
labelAnchor: "center", // Position label centrally below axis
},
// Y-axis configuration (CO₂ Emissions)
y: {
label: "↑ CO₂ Emissions per Capita (metric tons)",
grid: true // Show horizontal grid lines
},
// Color scale configuration
color: {
domain: top10CountriesData.map(d => d.name), // Use top 10 country names for domain
scheme: "tableau10", // Use a categorical color scheme
legend: true // Display the color legend
},
// Define the marks (visual elements) of the plot
marks: [
// Mark 1: Faint grey lines for countries not in the top 10.
Plot.line(otherCountriesData, {
x: "Year",
y: "Value",
z: "Country Name", // Group line segments by country
stroke: "grey",
strokeWidth: 0.5,
strokeOpacity: 0.5
}),

// Mark 2: Invisible wider lines for the top 10 countries.
// These act as larger hover targets for the tooltip.
Plot.line(longData.filter(d => top10Countries.has(d["Country Name"])), {
x: "Year",
y: "Value",
z: "Country Name",
strokeOpacity: 0, // Make the line invisible
strokeWidth: 20 // Make the hover area wide
}),

// Mark 3: Visible, colored lines for the top 10 countries.
Plot.line(longData.filter(d => top10Countries.has(d["Country Name"])), {
x: "Year",
y: "Value",
stroke: "Country Name", // Color lines according to the color scale
strokeWidth: 1.75,
z: "Country Name"
}),

// Mark 4: Text labels at the end of the lines for the top 5 countries.
Plot.text(top5LabelData, {
x: "Year", // Position horizontally at the last year
y: "Value", // Position vertically near the last value
text: "Country Name", // Display the country name
fill: "Country Name", // Match text color to line color
stroke: "white", // Add a white outline for readability
strokeWidth: 3,
dx: 6, // Shift label slightly right
dy: d => (d.rank - 2) * 14, // Offset vertically to spread labels (adjust multiplier as needed)
textAnchor: "start", // Align text start to the (x+dx, y+dy) point
fontSize: 10
}),

// Mark 5: Horizontal line at y=0 for reference.
Plot.ruleY([0], { strokeOpacity: 0.3 }),

// Mark 6: Interactive tooltip configuration.
// Uses pointerX to find the nearest data point horizontally.
Plot.tip(
longData, // Use the full dataset for finding tip data
Plot.pointerX({ // Find data nearest to the pointer's X position
x: "Year",
y: "Value",
z: "Country Name", // Consider the line group (country)
// Define the content of the tooltip
title: (d) => `${d["Country Name"]}\n${d.Year}: ${d.Value.toFixed(2)} t`
})
)
]
});
}
Insert cell
Insert cell
/**
* @fileoverview Generates a grid of choropleth maps showing CO₂ emissions
* per capita for different years (1990, 2000, 2010, 2019).
* Uses Observable Plot for map creation and D3.js for data loading/manipulation.
* Relies on an external CSV for mapping country codes.
* Assumes running in an Observable notebook environment.
*
* @requires D3.js (v7) - For data loading and manipulation.
* @requires Observable Plot (v0.6) - For chart creation.
* @requires topojson-client (v3) - For processing TopoJSON map data (data handling). NOT VISUALIZATION
* @assume The 'data' variable (wide-format CO₂ data) is pre-loaded in the Observable environment.
* @assume A FileAttachment named "all.csv" containing country code mappings is available.
*/
{
/**
* Observable Plot library instance.
* @external Plot
*/
const Plot = await import("https://cdn.jsdelivr.net/npm/@observablehq/plot@0.6/+esm");

/**
* D3.js library instance.
* @external d3
*/
const d3 = await import("https://cdn.jsdelivr.net/npm/d3@7/+esm");

/**
* TopoJSON client library instance (for converting TopoJSON to GeoJSON).
* ONLY FOR DATA HANDLING
* @external topojson
*/
const topojson = await import("https://cdn.jsdelivr.net/npm/topojson-client@3/+esm");


// Data transformation

/**
* 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>
`;
}
Insert cell
Insert cell
/**
* @fileoverview Creates a line chart showing the average CO₂ emissions per capita
* for different world regions over time (1990-2019).
* Uses Observable Plot for charting and D3.js for data transformation and aggregation.
* Assumes running in an Observable notebook environment.
*
* @requires D3.js (v7) - For data manipulation (grouping, averaging).
* @requires Observable Plot (v0.6) - For chart creation.
* @assume The 'data' variable (wide-format CO₂ data) is pre-loaded in the Observable environment.
*/
{
/**
* Observable Plot library instance.
* @external Plot
*/
const Plot = await import("https://cdn.jsdelivr.net/npm/@observablehq/plot@0.6/+esm");

/**
* D3.js library instance.
* @external d3
*/
const d3 = await import("https://cdn.jsdelivr.net/npm/d3@7/+esm");

// Data transformation

/**
* Extracts column names that represent years between 1990 and 2019.
* @type {string[]}
*/
const yearColumns = data.length > 0
? Object.keys(data[0]).filter(key => /^\d{4}$/.test(key) && key >= "1990" && key <= "2019")
: [];

/**
* Represents a single data point in the initial long format, before aggregation.
* @typedef {object} LongDataRowRegion
* @property {string} Country Name - The name of the country.
* @property {string | null | undefined} 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 | null | undefined} country_code - The 3-letter ISO alpha-3 country code.
*/

/**
* Transforms wide data to long format and filters for valid rows with non-null Value and Region.
* @type {Array<Omit<LongDataRowRegion, 'Value' | 'Region'> & { Value: number; Region: string }>}
*/
const longData = data.flatMap(country => {
const countryName = country["Country Name"];
const region = country["Region"]; // Region is crucial here
const countryCodeAlpha3 = country["country_code"];

if (!countryName || !region) return [];

return yearColumns.map(year => {
const value = country[year];
const parsedValue = parseFloat(value);
return /** @type {LongDataRowRegion} */ ({
"Country Name": countryName,
"Region": region,
"Year": +year,
"Value": (value == null || value === "" || isNaN(parsedValue))
? null
: parsedValue,
"country_code": countryCodeAlpha3
});
});
}).filter(
/**
* Type predicate ensuring Value and Region are non-null.
* @param {LongDataRowRegion} d
* @returns {d is Omit<LongDataRowRegion, 'Value' | 'Region'> & { Value: number; Region: string }}
*/
d => d.Value != null && d.Region != null // Filter out rows with null CO2 value or null Region
);

// Group and Aggregate Data

/**
* Represents a data point after aggregating by Year and Region.
* @typedef {object} AggregatedRegionData
* @property {number} Year - The year.
* @property {string} Region - The region name.
* @property {number | undefined} AverageValue - The mean CO₂ emissions per capita for that region in that year. Can be undefined if input group was empty.
*/

/**
* Calculates the average CO₂ emissions per capita for each region for each year.
* Groups the longData first by Year, then by Region, and computes the mean Value for each group.
* @type {Array<AggregatedRegionData>}
*/
const regionalAverages = Array.from(
// Group data first by Year, then by Region. Result is Map<number, Map<string, Array<...>>>
d3.group(longData, d => d.Year, d => d.Region),
// Process the outer Map (Years)
([year, regionMap]) => Array.from(
// Process the inner Map (Regions for a given Year)
regionMap,
// Calculate average for each region
([region, valuesInGroup]) => ({
Year: year,
Region: region,
/** Calculate the mean CO₂ value for the group. d3.mean handles potential empty arrays. */
AverageValue: d3.mean(valuesInGroup, v => v.Value)
})
)
).flat();

// Prepare for Plotting

/**
* Extracts a sorted list of unique region names from the aggregated data.
* Used for the color scale domain to ensure consistent coloring.
* @type {string[]}
*/
const uniqueRegions = Array.from(new Set(regionalAverages.map(d => d.Region))).sort();

// Create the Regional Average Line Chart

/**
* Generates the final line chart showing regional averages using Observable Plot.
* @returns {HTMLElement} An HTML element containing the rendered SVG chart.
*/
return Plot.plot({
title: "Average CO₂ Emissions per Capita by Region",
subtitle: "Mean emissions across countries within each region over time.",
width: 900,
height: 500,
marginLeft: 70,
// X-axis configuration (Year)
x: {
label: "Year",
tickFormat: "d" // Format years as integers
},
// Y-axis configuration (Average CO₂ Value)
y: {
label: "↑ Avg. CO₂ Emissions (t/capita)",
grid: true, // Display horizontal grid lines
// Optional: Ensure y-axis starts at 0 or slightly below
// domain: [0, d3.max(regionalAverages, d => d.AverageValue)]
},
// Color scale configuration (Regions)
color: {
domain: uniqueRegions, // Use the sorted unique regions for consistent color mapping
scheme: "tableau10", // A categorical color scheme suitable for regions
legend: true // Show the color legend
},
// Define the marks (visual elements) of the plot
marks: [
// Mark 1: Baseline rule at y=0
Plot.ruleY([0]),
// Mark 2: Lines connecting the average values for each region over time
Plot.line(regionalAverages.filter(d => d.AverageValue !== undefined), { // Filter out undefined averages if any
x: "Year",
y: "AverageValue",
stroke: "Region", // Color lines by region using the color scale
strokeWidth: 2,
/**
* Defines the tooltip content for the lines.
* @param {AggregatedRegionData} d - The data point for the hovered segment.
* @returns {string} Formatted tooltip text.
*/
title: d => `${d.Region}\n${d.Year}: ${d.AverageValue?.toFixed(2) ?? 'N/A'} t (avg)` // Add optional chaining for safety
}),
// Mark 3: Dots at each data point for better hover interaction (optional but recommended)
Plot.dot(regionalAverages.filter(d => d.AverageValue !== undefined), {
x: "Year",
y: "AverageValue",
fill: "Region", // Match dot color to line color
r: 2.5, // Radius of the dots
/**
* Defines the tooltip content for the dots.
* @param {AggregatedRegionData} d - The data point for the hovered dot.
* @returns {string} Formatted tooltip text.
*/
title: d => `${d.Region}\n${d.Year}: ${d.AverageValue?.toFixed(2) ?? 'N/A'} t (avg)`
})
],
// Enable default tooltips using the 'title' property defined in the marks
tip: true
});
}
Insert cell
Insert cell

Purpose-built for displays of data

Observable is your go-to platform for exploring data and creating expressive data visualizations. Use reactive JavaScript notebooks for prototyping and a collaborative canvas for visual data exploration and dashboard creation.
Learn more