Public
Edited
Apr 1
Insert cell
Insert cell
import {vl} from "@vega/vega-lite-api"
Insert cell
vegalite = require("@observablehq/vega-lite")
Insert cell
d3 = require("d3")

Insert cell
Insert cell
energyMining = FileAttachment("energy-and-mining.csv").csv()
Insert cell
countryMetadata = FileAttachment("Metadata_Country_API_5_DS2_en_csv_v2_3060772.csv").csv()
Insert cell
indicatorMetadata = FileAttachment("Metadata_Indicator_API_5_DS2_en_csv_v2_3060772.csv").csv()
Insert cell
rawEnergyData = FileAttachment("energy-and-mining-raw-2021.csv").csv()
Insert cell
indicatorInfo = Inputs.table(indicatorMetadata.slice(0, 10), {
width: 800,
format: {
"SOURCE_NOTE": x => x.slice(0, 100) + (x.length > 100 ? "..." : "")
}
})
Insert cell
Insert cell
regionsMap = {
const regionMap = {}
for (const country of countryMetadata) {
if (country["Country Code"] && country.Region) {
regionMap[country["Country Code"]] = country.Region
}
}
return regionMap
}

Insert cell

// Function to analyze available indicators and their data coverage
dataExploration = {
// Get all unique indicators (column names)
const indicators = Object.keys(energyMining[0])
.filter(key => key.startsWith("average_value_"))
.map(key => key.replace("average_value_", ""))
// Calculate data availability by year
const yearCoverage = {}
for (const row of energyMining) {
const year = row.Year
if (!yearCoverage[year]) {
yearCoverage[year] = {
year: year,
countryCount: 0,
indicators: {}
}
}
yearCoverage[year].countryCount++
for (const indicator of indicators) {
const fullName = "average_value_" + indicator
if (row[fullName] && row[fullName] !== "") {
if (!yearCoverage[year].indicators[indicator]) {
yearCoverage[year].indicators[indicator] = 0
}
yearCoverage[year].indicators[indicator]++
}
}
}
// Create summary of data availability
const recentYears = Object.keys(yearCoverage)
.filter(year => year >= 2010)
.sort((a, b) => b - a)
const dataAvailability = []
for (const year of recentYears) {
const yearData = yearCoverage[year]
for (const indicator of indicators) {
if (yearData.indicators[indicator]) {
dataAvailability.push({
Year: +year,
Indicator: indicator,
CountryCoverage: yearData.indicators[indicator] / yearData.countryCount * 100
})
}
}
}
// Create visualization of data availability
return vl.markRect()
.data(dataAvailability)
.encode(
vl.y().fieldN("Indicator").sort({field: "CountryCoverage", op: "mean", order: "descending"}).title(null),
vl.x().fieldO("Year").title("Year"),
vl.color().fieldQ("CountryCoverage").title("Data Coverage (%)").scale({scheme: "blues"}),
vl.tooltip([
{field: "Year", type: "ordinal"},
{field: "Indicator", type: "nominal"},
{field: "CountryCoverage", type: "quantitative", format: ".1f", title: "Coverage (%)"}
])
)
.width(600)
.height(800)
.title("Data Availability Heatmap (2010-2019)")
.render()
}

Insert cell

// Prepare region and income group mappings
countryInfo = {
const infoMap = {}
for (const country of countryMetadata) {
if (country["Country Code"]) {
infoMap[country["Country Code"]] = {
Region: country.Region || "Unknown",
IncomeGroup: country.IncomeGroup || "Unknown",
SpecialNotes: country.SpecialNotes || ""
}
}
}
return infoMap
}

Insert cell
energyTransitionViz = {
// Filter data for years with good coverage on both fossil fuels and renewables
const energyMixData = energyMining.filter(d =>
Number(d.Year) >= 2000 &&
Number(d.Year) <= 2019 &&
d["average_value_Fossil fuel energy consumption (% of total)"] !== "" &&
d["average_value_Renewable energy consumption (% of total final energy consumption)"] !== ""
)
// Add region and income group information
const withMetadata = energyMixData.map(d => ({
"Country Code": d["Country Code"],
"Country Name": d["Country Name"],
Year: +d.Year,
Region: countryInfo[d["Country Code"]]?.Region || "Unknown",
IncomeGroup: countryInfo[d["Country Code"]]?.IncomeGroup || "Unknown",
FossilFuel: +d["average_value_Fossil fuel energy consumption (% of total)"],
Renewable: +d["average_value_Renewable energy consumption (% of total final energy consumption)"]
})).filter(d => d.Region !== "Unknown" && d.IncomeGroup !== "Unknown")
// Group by income group and year
const byIncomeYear = {}
for (const d of withMetadata) {
const key = `${d.IncomeGroup}|${d.Year}`
if (!byIncomeYear[key]) {
byIncomeYear[key] = {
IncomeGroup: d.IncomeGroup,
Year: +d.Year,
fossilValues: [],
renewableValues: []
}
}
if (!isNaN(d.FossilFuel)) byIncomeYear[key].fossilValues.push(d.FossilFuel)
if (!isNaN(d.Renewable)) byIncomeYear[key].renewableValues.push(d.Renewable)
}
// Calculate averages
const energyTrends = []
for (const key in byIncomeYear) {
const group = byIncomeYear[key]
if (group.fossilValues.length > 0 && group.renewableValues.length > 0) {
const fossilAvg = group.fossilValues.reduce((sum, val) => sum + val, 0) / group.fossilValues.length
const renewableAvg = group.renewableValues.reduce((sum, val) => sum + val, 0) / group.renewableValues.length
energyTrends.push({
IncomeGroup: group.IncomeGroup,
Year: group.Year,
Type: "Fossil Fuel",
Value: fossilAvg
})
energyTrends.push({
IncomeGroup: group.IncomeGroup,
Year: group.Year,
Type: "Renewable",
Value: renewableAvg
})
}
}
// Create visualization with direct JSON specification
const spec = {
"$schema": "https://vega.github.io/schema/vega-lite/v5.json",
"title": "Energy Transition by Income Group (2000-2019)",
"data": {
"values": energyTrends
},
"facet": {
"column": {"field": "IncomeGroup", "title": null}
},
"spec": {
"width": 200,
"height": 200,
"mark": {"type": "line", "point": true},
"encoding": {
"x": {"field": "Year", "type": "quantitative", "title": "Year"},
"y": {"field": "Value", "type": "quantitative", "title": "Energy Consumption (%)"},
"color": {"field": "Type", "type": "nominal", "title": "Energy Type"}
}
}
}
return vegalite(spec)
}
Insert cell
resourceDependencyViz = {
// Filter data for recent year with good coverage on resource rents
const resourceData = energyMining.filter(d =>
+d.Year >= 2015 &&
d["average_value_Total natural resources rents (% of GDP)"] !== "" &&
(d["average_value_Oil rents (% of GDP)"] !== "" ||
d["average_value_Natural gas rents (% of GDP)"] !== "" ||
d["average_value_Mineral rents (% of GDP)"] !== "")
)
// Add region and income group information
const withMetadata = resourceData.map(d => ({
"Country Code": d["Country Code"],
"Country Name": d["Country Name"],
Year: +d.Year,
Region: countryInfo[d["Country Code"]]?.Region || "Unknown",
IncomeGroup: countryInfo[d["Country Code"]]?.IncomeGroup || "Unknown",
TotalRents: +d["average_value_Total natural resources rents (% of GDP)"] || 0,
OilRents: +d["average_value_Oil rents (% of GDP)"] || 0,
GasRents: +d["average_value_Natural gas rents (% of GDP)"] || 0,
MineralRents: +d["average_value_Mineral rents (% of GDP)"] || 0
})).filter(d => d.Region !== "Unknown" && d.TotalRents > 0)
// Find most recent year for each country
const countryLatestYear = {}
for (const d of withMetadata) {
const key = d["Country Code"]
if (!countryLatestYear[key] || +d.Year > +countryLatestYear[key].Year) {
countryLatestYear[key] = d
}
}
// Get top resource-dependent countries
const topResourceCountries = Object.values(countryLatestYear)
.sort((a, b) => b.TotalRents - a.TotalRents)
.slice(0, 15)
// Create long-form data for stacked bar chart
const longFormData = []
for (const d of topResourceCountries) {
if (d.OilRents > 0) {
longFormData.push({
Country: d["Country Name"],
Region: d.Region,
IncomeGroup: d.IncomeGroup,
RentType: "Oil Rents",
Value: d.OilRents
})
}
if (d.GasRents > 0) {
longFormData.push({
Country: d["Country Name"],
Region: d.Region,
IncomeGroup: d.IncomeGroup,
RentType: "Gas Rents",
Value: d.GasRents
})
}
if (d.MineralRents > 0) {
longFormData.push({
Country: d["Country Name"],
Region: d.Region,
IncomeGroup: d.IncomeGroup,
RentType: "Mineral Rents",
Value: d.MineralRents
})
}
}
// Create direct JSON specification
const spec = {
"$schema": "https://vega.github.io/schema/vega-lite/v5.json",
"title": "Top 15 Countries by Natural Resource Dependency (Latest Available Data)",
"width": 600,
"height": 400,
"data": {
"values": longFormData
},
"mark": "bar",
"encoding": {
"y": {
"field": "Country",
"type": "nominal",
"title": null,
"sort": "-x"
},
"x": {
"field": "Value",
"type": "quantitative",
"title": "Resource Rents (% of GDP)"
},
"color": {
"field": "RentType",
"type": "nominal",
"scale": {
"domain": ["Oil Rents", "Gas Rents", "Mineral Rents"],
"range": ["#e15759", "#76b7b2", "#4e79a7"]
}
},
"tooltip": [
{"field": "Country", "type": "nominal"},
{"field": "Region", "type": "nominal"},
{"field": "RentType", "type": "nominal"},
{"field": "Value", "type": "quantitative", "format": ".1f", "title": "% of GDP"}
]
}
}
return vegalite(spec)
}

Insert cell
electricityMixViz = {
// Filter data for most recent years
const mixData = energyMining.filter(d =>
+d.Year >= 2015 &&
d["average_value_Electricity production from coal sources (% of total)"] !== "" &&
d["average_value_Electricity production from natural gas sources (% of total)"] !== "" &&
d["average_value_Electricity production from oil sources (% of total)"] !== "" &&
d["average_value_Electricity production from nuclear sources (% of total)"] !== "" &&
d["average_value_Electricity production from hydroelectric sources (% of total)"] !== "" &&
d["average_value_Electricity production from renewable sources, excluding hydroelectric (% of total)"] !== ""
)
// Add region information
const withRegion = mixData.map(d => ({
"Country Code": d["Country Code"],
"Country Name": d["Country Name"],
Year: +d.Year,
Region: countryInfo[d["Country Code"]]?.Region || "Unknown",
Coal: +d["average_value_Electricity production from coal sources (% of total)"],
NaturalGas: +d["average_value_Electricity production from natural gas sources (% of total)"],
Oil: +d["average_value_Electricity production from oil sources (% of total)"],
Nuclear: +d["average_value_Electricity production from nuclear sources (% of total)"],
Hydro: +d["average_value_Electricity production from hydroelectric sources (% of total)"],
OtherRenewable: +d["average_value_Electricity production from renewable sources, excluding hydroelectric (% of total)"]
})).filter(d => d.Region !== "Unknown")
// Find most recent year for each country
const countryLatestYear = {}
for (const d of withRegion) {
const key = d["Country Code"]
if (!countryLatestYear[key] || +d.Year > +countryLatestYear[key].Year) {
countryLatestYear[key] = d
}
}
// Group by region
const byRegion = {}
for (const d of Object.values(countryLatestYear)) {
if (!byRegion[d.Region]) {
byRegion[d.Region] = {
Region: d.Region,
coal: [],
gas: [],
oil: [],
nuclear: [],
hydro: [],
renewable: []
}
}
if (!isNaN(d.Coal)) byRegion[d.Region].coal.push(d.Coal)
if (!isNaN(d.NaturalGas)) byRegion[d.Region].gas.push(d.NaturalGas)
if (!isNaN(d.Oil)) byRegion[d.Region].oil.push(d.Oil)
if (!isNaN(d.Nuclear)) byRegion[d.Region].nuclear.push(d.Nuclear)
if (!isNaN(d.Hydro)) byRegion[d.Region].hydro.push(d.Hydro)
if (!isNaN(d.OtherRenewable)) byRegion[d.Region].renewable.push(d.OtherRenewable)
}
// Calculate regional averages
const regionAverages = []
for (const region in byRegion) {
const r = byRegion[region]
const avg = {
Region: r.Region,
Coal: r.coal.length ? r.coal.reduce((sum, val) => sum + val, 0) / r.coal.length : 0,
NaturalGas: r.gas.length ? r.gas.reduce((sum, val) => sum + val, 0) / r.gas.length : 0,
Oil: r.oil.length ? r.oil.reduce((sum, val) => sum + val, 0) / r.oil.length : 0,
Nuclear: r.nuclear.length ? r.nuclear.reduce((sum, val) => sum + val, 0) / r.nuclear.length : 0,
Hydro: r.hydro.length ? r.hydro.reduce((sum, val) => sum + val, 0) / r.hydro.length : 0,
OtherRenewable: r.renewable.length ? r.renewable.reduce((sum, val) => sum + val, 0) / r.renewable.length : 0
}
regionAverages.push(avg)
}
// Convert to long form
const longFormData = []
for (const r of regionAverages) {
longFormData.push({Region: r.Region, Source: "Coal", Value: r.Coal / 100})
longFormData.push({Region: r.Region, Source: "Natural Gas", Value: r.NaturalGas / 100})
longFormData.push({Region: r.Region, Source: "Oil", Value: r.Oil / 100})
longFormData.push({Region: r.Region, Source: "Nuclear", Value: r.Nuclear / 100})
longFormData.push({Region: r.Region, Source: "Hydroelectric", Value: r.Hydro / 100})
longFormData.push({Region: r.Region, Source: "Other Renewable", Value: r.OtherRenewable / 100})
}
// Create direct JSON specification
const spec = {
"$schema": "https://vega.github.io/schema/vega-lite/v5.json",
"title": "Regional Electricity Production Mix (Latest Available Data)",
"width": 600,
"height": 400,
"data": {
"values": longFormData
},
"mark": "bar",
"encoding": {
"x": {
"field": "Region",
"type": "nominal",
"title": null
},
"y": {
"field": "Value",
"type": "quantitative",
"title": "Electricity Production (%)",
"stack": "normalize",
"axis": {"format": "%"}
},
"color": {
"field": "Source",
"type": "nominal",
"scale": {
"domain": ["Coal", "Natural Gas", "Oil", "Nuclear", "Hydroelectric", "Other Renewable"],
"range": ["#333333", "#f28e2c", "#d08c60", "#b392ac", "#4e79a7", "#7bccc4"]
}
},
"tooltip": [
{"field": "Region", "type": "nominal"},
{"field": "Source", "type": "nominal"},
{"field": "Value", "type": "quantitative", "format": ".1%"}
]
}
}
return vegalite(spec)
}
Insert cell
viewof emissionsYear = Inputs.range(
[2000, 2015],
{value: 2010, step: 5, label: "Select Year"}
)
Insert cell

emissionsAnalysisViz = {
// Filter data for selected year with good coverage
const emissionsData = energyMining.filter(d =>
+d.Year === emissionsYear &&
d["average_value_CO2 emissions from liquid fuel consumption (kt)"] !== "" &&
d["average_value_Energy use (kg of oil equivalent per capita)"] !== "" &&
d["average_value_GDP per unit of energy use (constant 2017 PPP $ per kg of oil equivalent)"] !== ""
)
// Add region and income group information
const withMetadata = emissionsData.map(d => ({
"Country Code": d["Country Code"],
"Country Name": d["Country Name"],
Year: +d.Year,
Region: countryInfo[d["Country Code"]]?.Region || "Unknown",
IncomeGroup: countryInfo[d["Country Code"]]?.IncomeGroup || "Unknown",
CO2Emissions: +d["average_value_CO2 emissions from liquid fuel consumption (kt)"],
EnergyUse: +d["average_value_Energy use (kg of oil equivalent per capita)"],
EnergyEfficiency: +d["average_value_GDP per unit of energy use (constant 2017 PPP $ per kg of oil equivalent)"]
})).filter(d => d.Region !== "Unknown" && d.IncomeGroup !== "Unknown")
// Calculate proper y-axis maximum based on actual data
const maxEfficiency = Math.max(...withMetadata.map(d => d.EnergyEfficiency))
// Set a reasonable maximum - either the actual max + 10% or 20, whichever is smaller
const yAxisMax = Math.min(maxEfficiency * 1.1, 20)
// Create direct JSON specification with adjusted scale for y-axis
const spec = {
"$schema": "https://vega.github.io/schema/vega-lite/v5.json",
"title": `Energy Efficiency, Consumption & CO2 Emissions (${emissionsYear})`,
"width": 750,
"height": 450,
"data": {
"values": withMetadata
},
"mark": {"type": "circle", "opacity": 0.7},
"encoding": {
"x": {
"field": "EnergyUse",
"type": "quantitative",
"scale": {"type": "log"},
"title": "Energy Use per Capita (kg oil equiv., log scale)"
},
"y": {
"field": "EnergyEfficiency",
"type": "quantitative",
"scale": {"domain": [0, yAxisMax]},
"title": "Energy Efficiency (GDP per unit of energy)"
},
"size": {
"field": "CO2Emissions",
"type": "quantitative",
"title": "CO2 Emissions (kt)",
"scale": {"range": [20, 2000]}
},
"color": {
"field": "IncomeGroup",
"type": "nominal"
},
"tooltip": [
{"field": "Country Name", "type": "nominal"},
{"field": "Region", "type": "nominal"},
{"field": "IncomeGroup", "type": "nominal"},
{"field": "EnergyUse", "type": "quantitative", "format": ",.0f", "title": "Energy Use per Capita"},
{"field": "EnergyEfficiency", "type": "quantitative", "format": ",.1f", "title": "Energy Efficiency"},
{"field": "CO2Emissions", "type": "quantitative", "format": ",.0f", "title": "CO2 Emissions (kt)"}
]
}
}
return vegalite(spec)
}

Insert cell
tradeDependencyViz = {
// Extract fuel export percentages from the energy mining dataset (not raw data)
const fuelExportData = energyMining.filter(d =>
+d.Year >= 2010 &&
d["average_value_Fuel exports (% of merchandise exports)"] !== ""
)
// Find latest year for each country
const latestData = {}
for (const d of fuelExportData) {
const key = d["Country Code"]
if (!latestData[key] || +d.Year > +latestData[key].Year) {
latestData[key] = {
"Country Code": d["Country Code"],
"Country Name": d["Country Name"],
"Year": +d.Year,
"Region": countryInfo[d["Country Code"]]?.Region || "Unknown",
"IncomeGroup": countryInfo[d["Country Code"]]?.IncomeGroup || "Unknown",
"FuelExports": +d["average_value_Fuel exports (% of merchandise exports)"]
}
}
}
// Sort countries by fuel exports percentage and get top 20
const topExporters = Object.values(latestData)
.filter(d => d.Region !== "Unknown" && d.FuelExports > 0)
.sort((a, b) => b.FuelExports - a.FuelExports)
.slice(0, 20)
// Create direct JSON specification
const spec = {
"$schema": "https://vega.github.io/schema/vega-lite/v5.json",
"title": "Top 20 Countries by Fuel Export Dependency (Latest Available Data)",
"width": 600,
"height": 400,
"data": {
"values": topExporters
},
"mark": "bar",
"encoding": {
"y": {
"field": "Country Name",
"type": "nominal",
"title": null,
"sort": "-x"
},
"x": {
"field": "FuelExports",
"type": "quantitative",
"title": "Fuel Exports (% of merchandise exports)"
},
"color": {
"field": "Region",
"type": "nominal"
},
"tooltip": [
{"field": "Country Name", "type": "nominal"},
{"field": "Region", "type": "nominal"},
{"field": "IncomeGroup", "type": "nominal"},
{"field": "Year", "type": "quantitative", "title": "Data Year"},
{"field": "FuelExports", "type": "quantitative", "format": ".1f", "title": "Fuel Exports (%)"}
]
}
}
return vegalite(spec)
}

Insert cell
ruralUrbanGapViz = {
// Filter for most recent year with good data coverage
const accessData = energyMining.filter(d =>
+d.Year >= 2015 &&
d["average_value_Access to electricity, rural (% of rural population)"] !== "" &&
d["average_value_Access to electricity, urban (% of urban population)"] !== ""
)
// Add region and income group information
const withMetadata = accessData.map(d => ({
"Country Code": d["Country Code"],
"Country Name": d["Country Name"],
Year: +d.Year,
Region: countryInfo[d["Country Code"]]?.Region || "Unknown",
IncomeGroup: countryInfo[d["Country Code"]]?.IncomeGroup || "Unknown",
RuralAccess: +d["average_value_Access to electricity, rural (% of rural population)"],
UrbanAccess: +d["average_value_Access to electricity, urban (% of urban population)"],
Gap: +d["average_value_Access to electricity, urban (% of urban population)"] -
+d["average_value_Access to electricity, rural (% of rural population)"]
})).filter(d => d.Region !== "Unknown" && d.IncomeGroup !== "Unknown")
// Find most recent year for each country
const countryLatestYear = {}
for (const d of withMetadata) {
const key = d["Country Code"]
if (!countryLatestYear[key] || +d.Year > +countryLatestYear[key].Year) {
countryLatestYear[key] = d
}
}
// Get countries with significant gaps
const countriesWithGaps = Object.values(countryLatestYear)
.filter(d => d.Gap > 5)
.sort((a, b) => b.Gap - a.Gap)
// Create reference line data for the diagonal
const referenceLine = [
{"x": 0, "y": 0},
{"x": 100, "y": 100}
]
// Using pure vegalite for the merged visualization
// First prepare combined data with a "source" field to distinguish the two datasets
const combinedData = [
...referenceLine.map(d => ({ ...d, source: "referenceLine" })),
...countriesWithGaps.map(d => ({
x: d.RuralAccess,
y: d.UrbanAccess,
source: "countryData",
...d
}))
]
// Create spec with conditional encoding based on the source field
const spec = {
"$schema": "https://vega.github.io/schema/vega-lite/v5.json",
"title": "Urban-Rural Electricity Access Gap (Latest Available Data)",
"width": 600,
"height": 600,
"data": {
"values": combinedData
},
"transform": [
{"calculate": "datum.source == 'referenceLine' ? 0 : 1", "as": "order"}
],
"layer": [
{
"transform": [{"filter": "datum.source == 'referenceLine'"}],
"mark": {"type": "line", "color": "#888", "strokeDash": [5, 5]},
"encoding": {
"x": {"field": "x", "type": "quantitative", "scale": {"domain": [0, 100]}, "title": "Rural Access to Electricity (%)"},
"y": {"field": "y", "type": "quantitative", "scale": {"domain": [0, 100]}, "title": "Urban Access to Electricity (%)"},
"order": {"field": "order"}
}
},
{
"transform": [{"filter": "datum.source == 'countryData'"}],
"mark": {"type": "circle", "opacity": 0.7},
"encoding": {
"x": {"field": "x", "type": "quantitative"},
"y": {"field": "y", "type": "quantitative"},
"size": {"field": "Gap", "type": "quantitative", "title": "Urban-Rural Gap (%)"},
"color": {"field": "Region", "type": "nominal"},
"tooltip": [
{"field": "Country Name", "type": "nominal"},
{"field": "Region", "type": "nominal"},
{"field": "IncomeGroup", "type": "nominal"},
{"field": "RuralAccess", "type": "quantitative", "format": ".1f", "title": "Rural Access (%)"},
{"field": "UrbanAccess", "type": "quantitative", "format": ".1f", "title": "Urban Access (%)"},
{"field": "Gap", "type": "quantitative", "format": ".1f", "title": "Urban-Rural Gap (%)"}
],
"order": {"field": "order"}
}
}
]
}
return vegalite(spec)
}
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