Walter-Lieth Climate Diagram — plotnine

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 plotnine

Python source (plotnine)

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

import os
import sys


# Remove this script's own directory from sys.path so that
# `from plotnine import ...` resolves to the installed package, not this file.
_script_dir = os.path.dirname(os.path.abspath(__file__))
sys.path = [
    p for p in sys.path if os.path.normpath(os.path.abspath(p) if p else os.getcwd()) != os.path.normpath(_script_dir)
]

import numpy as np
import pandas as pd
from plotnine import (
    aes,
    annotate,
    element_blank,
    element_line,
    element_rect,
    element_text,
    geom_hline,
    geom_line,
    geom_ribbon,
    geom_segment,
    geom_text,
    ggplot,
    labs,
    scale_color_manual,
    scale_x_continuous,
    scale_y_continuous,
    theme,
)


# 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"
INK_MUTED = "#6B6A63" if THEME == "light" else "#A8A79F"

# Imprint palette — semantic override: temperature=red (heat), precipitation=blue (water)
TEMP_COLOR = "#AE3030"
PRECIP_COLOR = "#4467A3"

# Fill alpha — higher in dark mode to keep fills visible on near-black background
FILL_ALPHA = 0.35 if THEME == "light" else 0.60

# Station: Athens, Greece — Mediterranean 1991–2020 climate normals
months_labels = ["J", "F", "M", "A", "M", "J", "J", "A", "S", "O", "N", "D"]
month_num = np.arange(1, 13)
temperature = np.array([9.9, 10.9, 13.2, 17.2, 22.3, 27.0, 29.5, 29.4, 24.7, 19.7, 15.0, 11.6])
precipitation = np.array([57, 37, 38, 24, 17, 8, 6, 7, 15, 52, 58, 71])

temp_annual = round(float(np.mean(temperature)), 1)
precip_annual = int(np.sum(precipitation))

# Walter-Lieth scaling: 10 °C ≡ 20 mm → divide precip by 2 to plot on temperature axis
precip_scaled = precipitation / 2.0

# Humid ribbon (blue): regions where precip_scaled ≥ temperature
humid_ymin = temperature.astype(float).copy()
humid_ymax = np.maximum(temperature, precip_scaled)
mask_arid = temperature > precip_scaled
humid_ymin[mask_arid] = temperature[mask_arid]
humid_ymax[mask_arid] = temperature[mask_arid]

# Arid ribbon (red): regions where temperature > precip_scaled
arid_ymin = np.minimum(temperature, precip_scaled).astype(float)
arid_ymax = temperature.astype(float).copy()
mask_humid = precip_scaled >= temperature
arid_ymin[mask_humid] = temperature[mask_humid]
arid_ymax[mask_humid] = temperature[mask_humid]

df = pd.DataFrame(
    {
        "month": month_num,
        "temperature": temperature,
        "precip_sc": precip_scaled,
        "hum_lo": humid_ymin,
        "hum_hi": humid_ymax,
        "ari_lo": arid_ymin,
        "ari_hi": arid_ymax,
    }
)

# Long-format dataframe for the two curves — enables a proper legend via color aesthetic
TEMP_LABEL = "Temperature (°C)"
PRECIP_LABEL = "Precipitation (mm)"
df_lines = pd.DataFrame(
    {
        "month": np.tile(month_num, 2),
        "value": np.concatenate([temperature, precip_scaled]),
        "variable": [TEMP_LABEL] * 12 + [PRECIP_LABEL] * 12,
    }
)

# Y and X axis limits — extend Y_MAX to 50 to accommodate the 100 mm right-axis tick
# (Walter-Lieth convention: 100 mm ≡ 50 °C on the temperature scale)
Y_MIN, Y_MAX = -5, 50
X_MAX_EXTENDED = 14.5

# Right-side precipitation axis: spec-mandated ticks 0/20/40/60/100 mm
# In temperature units: 0/10/20/30/50 °C
p_y_ticks = [0, 10, 20, 30, 50]
p_mm_labels = ["0", "20", "40", "60", "100"]

prec_ticks_df = pd.DataFrame(
    {"x": 12.65, "xend": 12.95, "y": p_y_ticks, "yend": p_y_ticks, "lx": 13.15, "label": p_mm_labels}
)
prec_axis_line_df = pd.DataFrame({"x": [12.65], "xend": [12.65], "y": [0], "yend": [50]})

# Title — 67 chars → default size 12
plot_title = "Athens · climograph-walter-lieth · python · plotnine · anyplot.ai"
title_len = len(plot_title)
title_size = max(8, round(12 * 67 / title_len))

subtitle = f"Athens, Greece  ·  107 m a.s.l.  ·  T̅ = {temp_annual} °C  ·  ΣP = {precip_annual} mm  ·  1991–2020"

anyplot_theme = theme(
    figure_size=(8, 4.5),
    plot_background=element_rect(fill=PAGE_BG, color=PAGE_BG),
    panel_background=element_rect(fill=PAGE_BG),
    panel_grid_major_y=element_line(color=INK_SOFT, size=0.3, alpha=0.18),
    panel_grid_major_x=element_blank(),
    panel_grid_minor=element_blank(),
    panel_border=element_blank(),
    axis_title=element_text(size=10, color=INK),
    axis_text=element_text(size=8, color=INK_SOFT),
    axis_line=element_line(color=INK_SOFT, size=0.5),
    axis_ticks=element_line(color=INK_SOFT, size=0.3),
    plot_title=element_text(size=title_size, color=INK, face="bold"),
    plot_subtitle=element_text(size=8, color=INK_SOFT),
    legend_position="bottom",
    legend_background=element_rect(fill=PAGE_BG, color="none"),
    legend_key=element_rect(fill=PAGE_BG),
    legend_text=element_text(size=8, color=INK_SOFT),
    legend_title=element_blank(),
)

plot = (
    ggplot(df, aes(x="month"))
    # Humid fill (blue — wet periods where precip > temp curve)
    + geom_ribbon(aes(ymin="hum_lo", ymax="hum_hi"), fill=PRECIP_COLOR, alpha=FILL_ALPHA)
    # Arid fill (red — dry periods where temp > precip curve)
    + geom_ribbon(aes(ymin="ari_lo", ymax="ari_hi"), fill=TEMP_COLOR, alpha=FILL_ALPHA)
    # 0 °C frost reference line
    + geom_hline(yintercept=0, color=INK_SOFT, size=0.5, linetype="dashed", alpha=0.55)
    # Temperature and precipitation curves — color mapped to variable for legend
    + geom_line(data=df_lines, mapping=aes(x="month", y="value", group="variable", color="variable"), size=1.1)
    + scale_color_manual(values={TEMP_LABEL: TEMP_COLOR, PRECIP_LABEL: PRECIP_COLOR}, name="")
    # Right-side precipitation axis: axis line
    + geom_segment(
        data=prec_axis_line_df, mapping=aes(x="x", xend="xend", y="y", yend="yend"), color=INK_SOFT, size=0.5
    )
    # Right-side precipitation axis: tick marks
    + geom_segment(data=prec_ticks_df, mapping=aes(x="x", xend="xend", y="y", yend="yend"), color=INK_SOFT, size=0.3)
    # Right-side precipitation axis: tick labels (mm)
    + geom_text(
        data=prec_ticks_df, mapping=aes(x="lx", y="y", label="label"), size=3.2, color=INK_SOFT, ha="left", va="center"
    )
    # Right-side precipitation axis: axis title (rotated)
    + annotate("text", x=14.1, y=25, label="Precipitation (mm)", angle=90, size=4, color=INK, ha="center", va="bottom")
    + scale_x_continuous(breaks=month_num.tolist(), labels=months_labels, limits=(0.5, X_MAX_EXTENDED), expand=(0, 0))
    + scale_y_continuous(name="Temperature (°C)", breaks=[0, 10, 20, 30, 40], limits=(Y_MIN, Y_MAX))
    + labs(title=plot_title, subtitle=subtitle, x="Month")
    + anyplot_theme
)

plot.save(f"plot-{THEME}.png", dpi=400, width=8, height=4.5, units="in")

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

Other implementations