Chris Parmer — home

Climate spiral

NASA GISTEMP monthly anomalies wound around a polar axis

Example from Plotly for highly customizable print-ready data visualization · shared helpers

Climate spiral — NASA GISTEMP monthly anomalies wound around a polar axis

Python Code

"""Climate spiral — NASA GISTEMP monthly anomalies wound around a polar axis.

Ed Hawkins' spiral: one loop per year, twelve months around the circle,
radius = anomaly, colour = year. Dark paper, hand-placed rings for 0, 1.5
and 2 °C, and no polar chrome at all — every gridline is drawn by hand.
"""
from pathlib import Path

from _shared import fetch_text, save, FONT

import io
import numpy as np
import pandas as pd
import plotly.graph_objects as go
import plotly.colors as pc

CHART_NUM = 2
URL = "https://data.giss.nasa.gov/gistemp/tabledata_v4/GLB.Ts+dSST.csv"
MONTHS = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"]
PAPER = "#15161d"
OFFSET = 1.2          # radius = anomaly + OFFSET, so the coldest years still have r > 0
PREIND = 0.26         # 1951–80 → 1850–1900 baseline shift (IPCC AR6 figure)


def generate():
    df = pd.read_csv(io.StringIO(fetch_text(URL)), skiprows=1, na_values="***")
    monthly = df.set_index("Year")[MONTHS].astype(float)

    fig = go.Figure()
    years = monthly.index.to_numpy()
    ramp = pc.sample_colorscale("Turbo", np.linspace(0.05, 0.95, len(years)))
    theta_month = np.arange(12) * 30

    prev_last = None
    for yr, color in zip(years, ramp):
        vals = monthly.loc[yr].to_numpy()
        ok = ~np.isnan(vals)
        r = vals[ok] + PREIND + OFFSET
        th = theta_month[ok]
        # Stitch December of the previous year to January so the spiral is continuous.
        if prev_last is not None:
            r, th = np.r_[prev_last, r], np.r_[-30, th]
        prev_last = r[-1]
        fig.add_trace(go.Scatterpolar(
            r=r, theta=th, mode="lines", name=str(yr),
            line=dict(color=color, width=1.3, shape="spline", smoothing=0.6),
            opacity=0.9, hovertemplate=f"{yr} %{{theta}}: %{{r:.2f}}<extra></extra>",
        ))

    # Reference rings, drawn as their own traces so they sit above the data.
    for lvl, label in [(0, "0 °C"), (1.5, "1.5 °C"), (2.0, "2 °C")]:
        fig.add_trace(go.Scatterpolar(
            r=[lvl + OFFSET] * 361, theta=np.arange(361), mode="lines",
            line=dict(color="white", width=1.2 if lvl else 0.8, dash="solid" if lvl else "dot"),
            hoverinfo="skip",
        ))
        fig.add_annotation(
            x=0.5, y=0.465 + 0.465 * (lvl + OFFSET) / (2.3 + OFFSET), xref="paper", yref="paper",
            text=label, showarrow=False, font=dict(size=13, color="white", family=FONT),
            bgcolor=PAPER, borderpad=2,
        )

    last = years[-1]
    fig.add_annotation(x=0.5, y=0.465, xref="paper", yref="paper", text=f"<b>{years[0]}–{last}</b>",
                       showarrow=False, font=dict(size=22, color="white", family=FONT))

    fig.update_layout(
        paper_bgcolor=PAPER, plot_bgcolor=PAPER, showlegend=False,
        font=dict(family=FONT, color="white"),
        margin=dict(l=60, r=60, t=100, b=60),
        polar=dict(
            bgcolor=PAPER, domain=dict(x=[0.035, 0.965], y=[0.0, 0.93]),
            radialaxis=dict(visible=False, range=[0, 2.3 + OFFSET]),
            angularaxis=dict(direction="clockwise", rotation=90, tickmode="array",
                             tickvals=theta_month, ticktext=MONTHS, showgrid=False,
                             showline=False, tickfont=dict(size=14, color="#c9cbd3"), ticks=""),
        ),
        annotations=list(fig.layout.annotations) + [
            dict(x=0, y=1.0, xref="paper", yref="paper", xanchor="left", yanchor="bottom", yshift=28,
                 text="<b>Global temperature change, month by month</b>", showarrow=False,
                 font=dict(size=22, color="white")),
            dict(x=0, y=1.0, xref="paper", yref="paper", xanchor="left", yanchor="bottom", yshift=8,
                 text="Each loop is one year, 1880 to the present, coloured from blue to red. "
                      "Radius is the monthly anomaly relative to 1850–1900.", showarrow=False,
                 font=dict(size=14, color="#c9cbd3")),
            dict(x=0, y=0, xref="paper", yref="paper", xanchor="left", yanchor="top", yshift=-28,
                 text="Source: NASA GISTEMP v4 monthly means (1951–1980 base) shifted by +0.26 °C to the "
                      "IPCC pre-industrial baseline. After Ed Hawkins' climate spiral.",
                 showarrow=False, font=dict(size=11, color="#8b8e99")),
        ],
    )
    save(CHART_NUM, fig, width=1000, height=1000)

Made with Plotly