The Keeling Curve

Annual mean CO₂ at Mauna Loa, rising from 316 ppm in 1959 to 427 ppm in 2025, crossing 350 ppm around 1988 and 400 ppm in 2015

Annual mean atmospheric CO₂ measured at Mauna Loa Observatory, Hawaii. Data: co2-ppm, from NOAA. Data story #1 — written by hand from an outline; the story skill comes after a couple of these.

The record

In March 1958, Charles David Keeling began measuring the concentration of carbon dioxide in the air at the Mauna Loa Observatory, 3,400 metres up a volcano in Hawaii, far from cities and forests. He kept measuring until he died in 2005; NOAA and the Scripps Institution have continued the record since. It is the longest continuous direct measurement of atmospheric CO₂ that exists.

The chart above is the annual mean from that record, 1959 to 2025.

What it shows

  • 316 ppm in 1959. 427 ppm in 2025 — parts per million of dry air.
  • It has risen every year. The annual mean has not once fallen in 67 years.
  • The rise is getting faster: about 0.9 ppm/year across the 1960s, about 2.6 ppm/year across the last decade.
  • It passed 350 ppm (a figure often cited as a safe ceiling) around 1988, and 400 ppm in 2015.

The seasonal cycle

Zoom in to the monthly data and the line turns into a sawtooth:

Monthly CO₂ at Mauna Loa 2010 to 2026 showing an annual saw-tooth oscillation of about 6 ppm around a steadily rising deseasonalized trend line

CO₂ drops about 6 ppm every northern-hemisphere summer, as plants across the larger northern landmass grow and take up carbon, then climbs back over the winter. Keeling was the first to measure this annual cycle. The smooth line underneath is the same data with the seasonal swing removed — the trend that doesn't reverse.

How this was made

The NOAA source is a text file, co2_mm_mlo.csv. It opens with about 40 comment lines, uses -1 / -9.99 / -0.99 to mean "no measurement" rather than leaving cells blank, and splits the date across a year column and a month column. build.ts strips the comments, converts the sentinels to empty cells, assembles an ISO date, and writes two typed CSVs — monthly and annual. Re-running it produces identical output. Full notes, including how this compares with the older community version of the dataset (whose auto-updater silently mislabelled two columns after NOAA changed the file), are in the dataset README.

Friction notes

For the future story / charting skills:

  • Charting. Both charts here are produced by a hand-written SVG generator, make-charts.mjs — reproducible, renders anywhere, and total control over the look. For now that's the policy: make charts that look good by whatever's easiest (inline SVG, standalone HTML, JS in the page head — Flowershow serves all three), and iterate. Settling a single standard that DataHub / Flowershow support natively is a separate, later investigation (#11).
  • What worked: the clean typed dataset meant each chart was a three-line "read CSV, map two columns". The mess was all upstream of the story.
Built with LogoFlowershow