Chris Parmer — home

Unemployment rate with NBER recession shading

Macroeconomics

Example from the compendium of canonical charts

Macroeconomics — Unemployment rate with NBER recession shading

Python Code

"""Macroeconomics — Unemployment rate with NBER recession shading."""
from pathlib import Path

# ── Palette + theme (matching the Plotly Studio gallery these charts ship in) ──
VIOLET, TEAL, GREEN, PINK, ORANGE = "#845EEE", "#52B3D0", "#55B685", "#DA5597", "#E9A23B"
PRIMARY, SECONDARY = VIOLET, TEAL
COLORWAY = [VIOLET, TEAL, GREEN, PINK, ORANGE]
BG, TEXT, GRID, MUTED = "#ffffff", "#1c2024", "#d9d9e0", "#60646c"
FONT = "Inter, -apple-system, BlinkMacSystemFont, sans-serif"
COLORSCALE = [[0, "rgba(132, 94, 238, 0.05)"], [1, "rgba(132, 94, 238, 0.9)"]]


def apply_theme(fig):
    """Light gallery theme: white background, Inter font, soft gridlines."""
    fig.update_layout(
        paper_bgcolor=BG, plot_bgcolor=BG, colorway=COLORWAY,
        font=dict(family=FONT, color=TEXT, size=12),
        legend=dict(font=dict(color=TEXT)),
        hoverlabel=dict(bgcolor="#f0f0f3", font=dict(color=TEXT, family=FONT), bordercolor=GRID),
    )
    fig.update_xaxes(gridcolor=GRID, linecolor=GRID, zerolinecolor=GRID)
    fig.update_yaxes(gridcolor=GRID, linecolor=GRID, zerolinecolor=GRID)


def fetch_csv(url, **kwargs):
    import io
    import pandas as pd
    import requests
    r = requests.get(url, timeout=60)
    r.raise_for_status()
    return pd.read_csv(io.StringIO(r.text), **kwargs)


def fetch_json(url):
    import requests
    r = requests.get(url, timeout=60)
    r.raise_for_status()
    return r.json()

import pandas as pd
import plotly.graph_objects as go

UNRATE_URL = "https://fred.stlouisfed.org/graph/fredgraph.csv?id=UNRATE"
USREC_URL  = "https://fred.stlouisfed.org/graph/fredgraph.csv?id=USREC"


def generate():
    print("fetching UNRATE and USREC from FRED …")
    unrate = fetch_csv(UNRATE_URL)
    usrec  = fetch_csv(USREC_URL)

    unrate.columns = ["date", "unrate"]
    usrec.columns  = ["date", "rec"]
    unrate["date"] = pd.to_datetime(unrate["date"], errors="coerce")
    usrec["date"]  = pd.to_datetime(usrec["date"],  errors="coerce")

    # Align on common date range
    df = pd.merge(unrate, usrec, on="date", how="inner").sort_values("date")
    df = df.dropna()

    # Find contiguous recession blocks
    in_rec = False
    rec_start = None
    rec_blocks = []
    for _, row in df.iterrows():
        if row["rec"] == 1 and not in_rec:
            in_rec = True
            rec_start = row["date"]
        elif row["rec"] == 0 and in_rec:
            in_rec = False
            rec_blocks.append((rec_start, row["date"]))
    if in_rec:
        rec_blocks.append((rec_start, df["date"].iloc[-1]))

    fig = go.Figure()

    # Recession vrects (add first so they sit behind the line)
    for start, end in rec_blocks:
        fig.add_vrect(
            x0=start, x1=end,
            fillcolor="gray", opacity=0.18,
            line_width=0,
        )

    # UNRATE line
    fig.add_trace(go.Scatter(
        x=df["date"],
        y=df["unrate"],
        mode="lines",
        line=dict(color=VIOLET, width=1.8),
        name="Unemployment Rate",
        hovertemplate="%{x|%b %Y}: %{y:.1f}%<extra></extra>",
    ))

    fig.update_layout(
        xaxis=dict(title=""),
        yaxis=dict(title="Unemployment Rate (%)", ticksuffix="%"),
        legend=dict(orientation="h", y=-0.14),
        margin=dict(t=50, b=70, l=70, r=40),
        annotations=[dict(
            x=0.01, y=0.97,
            xref="paper", yref="paper",
            text="Gray bands = NBER recessions",
            showarrow=False,
            font=dict(size=11, color="#60646c"),
            align="left",
        )],
    )
    apply_theme(fig)
    return fig


fig = generate()
fig.show()

Made with Plotly