Walter-Lieth Climate Diagram — Seaborn

A Walter-Lieth climate diagram (climograph) is the canonical visualization of a location's annual climate, plotting monthly mean temperature and monthly precipitation over the twelve months on a single panel. It uses the classic 1:2 axis-scaling convention where 10 °C on the temperature axis aligns with 20 mm on the precipitation axis, so the relationship between the two curves directly reveals water availability: where precipitation exceeds the temperature curve the period is humid (filled blue/hatched), and where the temperature curve rises above precipitation the period is arid (filled red/dotted). A header carries station metadata and annual means, while frost indicators along the baseline mark cold months.

Walter-Lieth Climate Diagram rendered with Seaborn

Python source (Seaborn)

""" anyplot.ai
climograph-walter-lieth: Walter-Lieth Climate Diagram
Library: seaborn 0.13.2 | Python 3.13.13
Quality: 88/100 | Created: 2026-06-15
"""

import os
import sys


# Prevent the script directory from shadowing the seaborn package on sys.path
_this_dir = os.path.abspath(os.path.dirname(__file__))
sys.path = [p for p in sys.path if os.path.abspath(p) != _this_dir]

import matplotlib.lines as mlines
import matplotlib.patches as mpatches
import matplotlib.pyplot as plt
import numpy as np
import seaborn as sns


# Theme tokens
THEME = os.getenv("ANYPLOT_THEME", "light")
PAGE_BG = "#FAF8F1" if THEME == "light" else "#1A1A17"
ELEVATED_BG = "#FFFDF6" if THEME == "light" else "#242420"
INK = "#1A1A17" if THEME == "light" else "#F0EFE8"
INK_SOFT = "#4A4A44" if THEME == "light" else "#B8B7B0"

# Imprint palette — semantic assignments
# Temperature → matte red (Imprint slot 5, semantic: heat)
# Precipitation → blue (Imprint slot 3, semantic: water/sky)
TEMP_COLOR = "#AE3030"
PRECIP_COLOR = "#4467A3"

sns.set_theme(
    style="ticks",
    rc={
        "figure.facecolor": PAGE_BG,
        "axes.facecolor": PAGE_BG,
        "axes.edgecolor": INK_SOFT,
        "axes.labelcolor": INK,
        "text.color": INK,
        "xtick.color": INK_SOFT,
        "ytick.color": INK_SOFT,
        "grid.color": INK,
        "grid.alpha": 0.15,
        "legend.facecolor": ELEVATED_BG,
        "legend.edgecolor": INK_SOFT,
    },
)

# Station: Pyrenean highland — demonstrates all Walter-Lieth features:
# frost months (Jan/Feb/Dec), summer arid period (Jun/Jul/Aug), perhumid autumn (Oct)
STATION = "Montsec Ridge"
ELEVATION = 1240
PERIOD = "1991–2020"

MONTHS = ["J", "F", "M", "A", "M", "J", "J", "A", "S", "O", "N", "D"]
TEMPERATURE = np.array([-4.0, -2.0, 4.0, 10.0, 15.0, 22.0, 27.0, 26.0, 20.0, 12.0, 4.0, -2.0])
PRECIP = np.array([30, 25, 55, 75, 65, 20, 8, 12, 85, 120, 90, 40])

T_MEAN = round(TEMPERATURE.mean(), 1)
P_TOTAL = int(PRECIP.sum())

# Walter-Lieth dual-scale: 10°C = 20 mm below 100 mm (2:1); above 100 mm compressed 10:1
# P ≤ 100 mm → precip_t = P / 2
# P > 100 mm → precip_t = 50 + (P − 100) / 10
PERHUMID_THRESHOLD = 100.0
PERHUMID_Y = PERHUMID_THRESHOLD / 2.0  # = 50.0 on temp axis

precip_t = np.where(PRECIP <= PERHUMID_THRESHOLD, PRECIP / 2.0, PERHUMID_Y + (PRECIP - PERHUMID_THRESHOLD) / 10.0)
x = np.arange(12)

Y_MIN = -10
Y_MAX = max(TEMPERATURE.max(), precip_t.max()) + 6  # ~58

# Figure (landscape 3200×1800)
fig, ax = plt.subplots(figsize=(8, 4.5), dpi=400, facecolor=PAGE_BG)
ax.set_facecolor(PAGE_BG)

# Theme-adaptive fill alphas: arid fill is nearly invisible on dark near-black surface
ARID_ALPHA = 0.45 if THEME == "dark" else 0.22
HUMID_ALPHA = 0.40 if THEME == "dark" else 0.26

# Humid fill (blue): where precip_t >= temperature, capped at perhumid threshold
ax.fill_between(
    x,
    TEMPERATURE,
    np.minimum(precip_t, PERHUMID_Y),
    where=precip_t >= TEMPERATURE,
    color=PRECIP_COLOR,
    alpha=HUMID_ALPHA,
    interpolate=True,
    zorder=1,
)

# Perhumid fill (solid blue): P > 100 mm — solid to mark "very wet" zone above 100 mm
perhumid_mask = PRECIP > PERHUMID_THRESHOLD
ax.fill_between(
    x, PERHUMID_Y, precip_t, where=perhumid_mask, color=PRECIP_COLOR, alpha=0.90, interpolate=True, zorder=1
)

# Arid fill (red): where temperature exceeds precipitation curve
ax.fill_between(
    x,
    TEMPERATURE,
    precip_t,
    where=TEMPERATURE >= precip_t,
    color=TEMP_COLOR,
    alpha=ARID_ALPHA,
    interpolate=True,
    zorder=1,
)

# Frost month indicators: solid blue bar from Y_MIN to 0 for months with mean T < 0°C
for i, t in enumerate(TEMPERATURE):
    if t < 0:
        ax.fill_between([i - 0.45, i + 0.45], [Y_MIN, Y_MIN], [0, 0], color=PRECIP_COLOR, alpha=0.50, zorder=2)

# Temperature and precipitation lines via seaborn
sns.lineplot(x=x, y=TEMPERATURE, ax=ax, color=TEMP_COLOR, linewidth=2.5, zorder=3)
sns.lineplot(x=x, y=precip_t, ax=ax, color=PRECIP_COLOR, linewidth=2.5, zorder=3)

# Perhumid threshold line — marks scale change at 100 mm
ax.axhline(PERHUMID_Y, color=PRECIP_COLOR, linewidth=0.9, linestyle="--", alpha=0.55, zorder=2)

# Zero-degree reference line
ax.axhline(0, color=INK_SOFT, linewidth=0.8, linestyle="--", alpha=0.5, zorder=1)

# Left (temperature) axis styling
ax.set_ylim(Y_MIN, Y_MAX)
ax.set_xlim(-0.5, 11.5)
ax.set_xticks(x)
ax.set_xticklabels(MONTHS, fontsize=8)
ax.set_yticks([0, 10, 20, 30])
ax.tick_params(axis="y", colors=TEMP_COLOR, labelsize=8)
ax.set_ylabel("Temperature (°C)", fontsize=10, color=TEMP_COLOR, labelpad=6)
ax.spines["left"].set_color(TEMP_COLOR)
ax.spines["bottom"].set_color(INK_SOFT)
ax.yaxis.grid(True, alpha=0.12, linewidth=0.8, color=INK, zorder=0)

# Remove top and right spines — seaborn-idiomatic
sns.despine(ax=ax, right=True, top=True)

# Right precipitation axis with dual-scale tick labels (0, 20, 40, 60, 100, 200 mm)
ax2 = ax.twinx()
ax2.set_ylim(Y_MIN, Y_MAX)

tick_mm = np.array([0.0, 20.0, 40.0, 60.0, 100.0, 200.0])
tick_pos = np.where(tick_mm <= PERHUMID_THRESHOLD, tick_mm / 2.0, PERHUMID_Y + (tick_mm - PERHUMID_THRESHOLD) / 10.0)
ax2.set_yticks(tick_pos)
ax2.set_yticklabels([str(int(p)) for p in tick_mm])
ax2.tick_params(axis="y", labelsize=8, colors=PRECIP_COLOR)
ax2.set_ylabel("Precipitation (mm)", fontsize=10, color=PRECIP_COLOR, labelpad=8)
ax2.spines["right"].set_color(PRECIP_COLOR)
ax2.spines["top"].set_visible(False)
ax2.spines["bottom"].set_visible(False)
ax2.spines["left"].set_visible(False)

# Legend with proxy artists
leg = ax.legend(
    handles=[
        mlines.Line2D([], [], color=TEMP_COLOR, linewidth=2.5, label="Temperature"),
        mlines.Line2D([], [], color=PRECIP_COLOR, linewidth=2.5, label="Precipitation"),
        mpatches.Patch(facecolor=PRECIP_COLOR, alpha=0.40, label="Humid period"),
        mpatches.Patch(facecolor=PRECIP_COLOR, alpha=0.90, label="Perhumid >100 mm"),
        mpatches.Patch(facecolor=TEMP_COLOR, alpha=0.40, label="Arid period"),
    ],
    fontsize=7.5,
    loc="upper left",
    bbox_to_anchor=(0.01, 0.83),
    facecolor=ELEVATED_BG,
    edgecolor=INK_SOFT,
    framealpha=0.9,
)
for text in leg.get_texts():
    text.set_color(INK)

# Station header
header = f"{STATION}  ·  {ELEVATION} m  ·  {PERIOD}    T = {T_MEAN} °C  ·  P = {P_TOTAL} mm"
ax.text(
    0.5,
    0.97,
    header,
    transform=ax.transAxes,
    ha="center",
    va="top",
    fontsize=8.5,
    color=INK,
    bbox={"boxstyle": "round,pad=0.35", "facecolor": ELEVATED_BG, "edgecolor": INK_SOFT, "alpha": 0.9},
)

# Title
title = "climograph-walter-lieth · python · seaborn · anyplot.ai"
n = len(title)
ratio = 67 / n if n > 67 else 1.0
title_fontsize = max(8, round(12 * ratio))
ax.set_title(title, fontsize=title_fontsize, fontweight="medium", color=INK, pad=10)

# Layout and save
fig.subplots_adjust(left=0.09, right=0.88, bottom=0.09, top=0.84)
plt.savefig(f"plot-{THEME}.png", dpi=400, facecolor=PAGE_BG)
plt.close()

Part of Walter-Lieth Climate Diagram on anyplot.ai.

Other implementations