The world's power plants
About 35,000 power plants in 167 countries, from coal and gas plants to wind farms and dams, sized by how much power they can make. Turn each kind of fuel on and off, and click a plant for its name and capacity.
Build it yourself
A Node.js script turns the World Resources Institute's database from CSV into GeoJSON, and a page draws it as one circle layer, sized by capacity and colored by fuel. It was tested with MapLibre GL JS 6.15 in Chromium and Node.js 22.
What you need
- Node.js 18 or later.
- The map tiles and the database need no key.
Collect the plants
Make a folder for the map, and in it a file called power-plants.mjs:
import { writeFile } from "node:fs/promises";
const url = "https://raw.githubusercontent.com/wri/global-power-plant-database/master/output_database/global_power_plant_database.csv";
const response = await fetch(url);
if (!response.ok) throw new Error(`GitHub returned ${response.status}`);
const csv = await response.text();
// A row's fields, where a quoted field can hold commas and doubled quotes.
function fields(row) {
return [...row.matchAll(/("(?:[^"]|"")*"|[^,]*)(?:,|$)/g)]
.slice(0, -1)
.map(([, field]) => (field.startsWith('"') ? field.slice(1, -1).replaceAll('""', '"') : field));
}
const [header, ...rows] = csv.trim().split("\n").map(fields);
const column = Object.fromEntries(header.map((name, i) => [name, i]));
const features = rows
.map((row) => ({
type: "Feature",
properties: { name: row[column.name], fuel: row[column.primary_fuel], mw: Number(row[column.capacity_mw]) },
geometry: { type: "Point", coordinates: [Number(row[column.longitude]), Number(row[column.latitude])] },
}))
.sort((a, b) => a.properties.mw - b.properties.mw);
await writeFile("power-plants.geojson", JSON.stringify({ type: "FeatureCollection", features }));
console.log(`Saved ${features.length} power plants`);
- Some plant names have commas in them, inside quotes, so
fieldsreads each row properly instead of splitting on every comma. - The plants are sorted smallest first, so the biggest draw last, on top.
Run it:
node power-plants.mjs
It prints Saved 34936 power plants and writes power-plants.geojson, about 5 MB.
Make the page
Next to power-plants.geojson, make index.html:
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>The world's power plants</title>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/maplibre-gl@6.15.0/dist/maplibre-gl.css">
</head>
<body style="margin: 0">
<div style="position: absolute; z-index: 1; top: 10px; left: 10px">
<button data-fuels="Coal,Gas,Oil,Petcoke,Cogeneration">Coal, gas, and oil</button>
<button data-fuels="Hydro,Wind,Solar,Geothermal,Biomass,Wave and Tidal">Renewables</button>
<button data-fuels="Nuclear">Nuclear</button>
<button data-fuels="">Everything</button>
</div>
<div id="map" style="height: 100vh"></div>
<script type="module">
import * as maplibregl from "https://cdn.jsdelivr.net/npm/maplibre-gl@6.15.0/+esm";
const map = new maplibregl.Map({
container: "map",
style: "https://tiles.mapricot.com/styles/grayscale.json",
center: [10, 30],
zoom: 1.2,
});
await map.once("load");
map.addSource("plants", {
type: "geojson",
data: "power-plants.geojson",
attribution: '<a href="https://datasets.wri.org/dataset/globalpowerplantdatabase">WRI</a>',
});
map.addLayer({
id: "plants",
type: "circle",
source: "plants",
paint: {
"circle-radius": [
"interpolate", ["linear"], ["zoom"],
1, ["interpolate", ["linear"], ["sqrt", ["get", "mw"]], 0, 1, 70, 7],
8, ["interpolate", ["linear"], ["sqrt", ["get", "mw"]], 0, 3, 70, 28],
],
"circle-color": [
"match", ["get", "fuel"],
["Coal", "Gas", "Oil", "Petcoke", "Cogeneration"], "#eb6834",
["Hydro", "Wind", "Solar", "Geothermal", "Biomass", "Wave and Tidal"], "#1baf7a",
"Nuclear", "#2a78d6",
"#868e96",
],
"circle-opacity": 0.75,
},
});
for (const button of document.querySelectorAll("[data-fuels]")) {
button.addEventListener("click", () => {
const fuels = button.dataset.fuels.split(",");
map.setFilter("plants", button.dataset.fuels ? ["in", ["get", "fuel"], ["literal", fuels]] : null);
});
}
map.on("click", "plants", (event) => {
const { name, fuel, mw } = event.features[0].properties;
new maplibregl.Popup().setLngLat(event.lngLat).setText(`${name}: ${fuel}, ${mw} MW`).addTo(map);
});
map.on("mouseenter", "plants", () => (map.getCanvas().style.cursor = "pointer"));
map.on("mouseleave", "plants", () => (map.getCanvas().style.cursor = ""));
</script>
</body>
</html>
- The circle's area follows capacity, through the square root, and the circles grow as you zoom in.
matchgives each group of fuels a color, with gray for the rest.- Each button sets a filter on the layer, so it redraws without loading the file again.
Open it
Serve the folder with npx serve or python3 -m http.server, and open the address it prints. Opened straight from the file, the map stays blank, as Open the page explains. You should see the world's plants on a gray map, with coal in China and India, hydro along the rivers, and wind across Europe.
The database's last release, 1.3.0, came out in 2021, so newer plants, solar farms especially, are missing. The map above saves the plants as short rows instead of GeoJSON, a third of the size, and turns them into GeoJSON when they load. For many more points than this, clustering or a heatmap keeps the map readable when zoomed out.
Data
- Power plants: Global Power Plant Database 1.3.0, World Resources Institute (CC BY 4.0)
- Map: OpenStreetMap contributors (ODbL), in Mapricot's map tiles