Gallery

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

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`);

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>

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