Walter-Lieth Climate Diagram — MUI X Charts

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 MUI X Charts

JavaScript source (MUI X Charts)

// anyplot.ai
// climograph-walter-lieth: Walter-Lieth Climate Diagram
// Library: muix 7.29.1 | JavaScript 22.22.3
// Quality: 89/100 | Created: 2026-06-15

import { ChartContainer } from "@mui/x-charts/ChartContainer";
import { LinePlot } from "@mui/x-charts/LineChart";
import { ChartsXAxis } from "@mui/x-charts/ChartsXAxis";
import { ChartsYAxis } from "@mui/x-charts/ChartsYAxis";
import { ChartsGrid } from "@mui/x-charts/ChartsGrid";
import { useDrawingArea, useXScale, useYScale } from "@mui/x-charts/hooks";
import Box from "@mui/material/Box";
import Typography from "@mui/material/Typography";

const t = window.ANYPLOT_TOKENS;

// Station: Lisbon, Portugal — Mediterranean climate (1991–2020 normals)
const ELEVATION = 77;
const months = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
const temperature = [11.3, 12.4, 14.2, 16.0, 18.5, 21.7, 24.2, 24.5, 22.3, 18.6, 14.8, 11.7];
const precipitation = [77, 62, 54, 42, 36, 13, 4, 5, 25, 65, 93, 91];

// Walter-Lieth 2:1 scaling: precipitation/2 plotted on temperature axis
// This makes 10°C align with 20 mm so the curves are directly comparable
const precipScaled = precipitation.map((p) => p / 2);

const annualMeanTemp = (temperature.reduce((a, b) => a + b, 0) / 12).toFixed(1);
const annualTotalPrecip = precipitation.reduce((a, b) => a + b, 0);

const TEMP_AXIS_ID = "tempY";
// Imprint palette: blue (#4467A3) for humid, matte red (#AE3030) for arid
// These follow semantic conventions: water=blue, heat/drought=red
const HUMID_FILL = "#4467A3";
const ARID_FILL = "#AE3030";
// Dark blue for frost month indicator bands (mean temp < 0°C)
const FROST_COLOR = "#2B4B8C";

// Draws blue/red fills between the temperature and precipitation curves,
// plus frost month indicator bands below the x-axis for months with mean temp < 0°C
function WalterLiethFill() {
  const { left, top, height } = useDrawingArea();
  const xScale = useXScale();
  const yScale = useYScale(TEMP_AXIS_ID);

  if (!xScale || !yScale || !xScale.bandwidth) return null;

  const bw = xScale.bandwidth();
  const getX = (i) => (xScale(months[i]) ?? 0) + bw / 2;
  const getTY = (i) => yScale(temperature[i]) ?? 0;
  const getPY = (i) => yScale(precipScaled[i]) ?? 0;

  const elems = [];

  // Humid/arid fills between temperature and precipitation curves
  for (let i = 0; i < months.length - 1; i++) {
    const x0 = getX(i);
    const x1 = getX(i + 1);
    const ty0 = getTY(i);
    const ty1 = getTY(i + 1);
    const py0 = getPY(i);
    const py1 = getPY(i + 1);
    const h0 = precipScaled[i] > temperature[i];
    const h1 = precipScaled[i + 1] > temperature[i + 1];

    if (h0 === h1) {
      elems.push(
        <polygon
          key={`f${i}`}
          points={`${x0},${py0} ${x1},${py1} ${x1},${ty1} ${x0},${ty0}`}
          fill={h0 ? HUMID_FILL : ARID_FILL}
          fillOpacity={0.38}
        />
      );
    } else {
      // Curves cross — compute intersection by linear interpolation in SVG space
      const d = (py1 - py0) - (ty1 - ty0);
      const frac = Math.abs(d) > 0.01 ? (ty0 - py0) / d : 0.5;
      const xc = x0 + (x1 - x0) * frac;
      const yc = ty0 + (ty1 - ty0) * frac;
      elems.push(
        <polygon
          key={`f${i}a`}
          points={`${x0},${py0} ${xc},${yc} ${x0},${ty0}`}
          fill={h0 ? HUMID_FILL : ARID_FILL}
          fillOpacity={0.38}
        />,
        <polygon
          key={`f${i}b`}
          points={`${xc},${yc} ${x1},${py1} ${x1},${ty1}`}
          fill={h1 ? HUMID_FILL : ARID_FILL}
          fillOpacity={0.38}
        />
      );
    }
  }

  // Frost month indicator: solid band just below x-axis for months with mean temp < 0°C
  const frostBandH = 6;
  const frostGap = 2;
  for (let i = 0; i < months.length; i++) {
    if (temperature[i] < 0) {
      const xi = getX(i);
      elems.push(
        <rect
          key={`frost${i}`}
          x={xi - bw / 2}
          y={height + frostGap}
          width={bw}
          height={frostBandH}
          fill={FROST_COLOR}
          fillOpacity={0.85}
        />
      );
    }
  }

  return <g transform={`translate(${left},${top})`}>{elems}</g>;
}

const CHART_SERIES = [
  {
    id: "temp",
    label: "Temperature (°C)",
    data: temperature,
    type: "line",
    color: "#AE3030",
    yAxisId: TEMP_AXIS_ID,
    showMark: false,
    curve: "catmullRom",
  },
  {
    id: "precip",
    label: "Precipitation (mm)",
    data: precipScaled,
    type: "line",
    color: "#4467A3",
    yAxisId: TEMP_AXIS_ID,
    showMark: false,
    curve: "catmullRom",
  },
];

export default function Chart() {
  const W = window.ANYPLOT_SIZE.width;
  const H = window.ANYPLOT_SIZE.height;

  const title = "climograph-walter-lieth · javascript · muix · anyplot.ai";
  const titleFontSize = Math.max(11, Math.round(22 * Math.min(1, 67 / title.length)));

  const chartH = H - 158;
  const chartW = W - 48;

  const hasFrostMonths = temperature.some((t) => t < 0);
  const legendItems = [
    { color: "#AE3030", isLine: true, label: "Temperature (°C)" },
    { color: "#4467A3", isLine: true, label: "Precipitation (mm)" },
    { color: "rgba(68,103,163,0.38)", border: "#4467A3", label: "Humid period" },
    { color: "rgba(174,48,48,0.38)", border: "#AE3030", label: "Arid period" },
    ...(hasFrostMonths
      ? [{ color: "rgba(43,75,140,0.85)", border: FROST_COLOR, label: "Frost month" }]
      : []),
  ];

  return (
    <Box
      sx={{
        width: W,
        height: H,
        bgcolor: t.pageBg,
        display: "flex",
        flexDirection: "column",
        px: "24px",
        pt: "18px",
        pb: "14px",
        boxSizing: "border-box",
      }}
    >
      {/* Station header */}
      <Box sx={{ textAlign: "center", mb: "10px" }}>
        <Typography
          sx={{ color: t.ink, fontWeight: 700, fontSize: 20, lineHeight: 1.3 }}
        >
          Lisbon, Portugal — {ELEVATION} m a.s.l.
        </Typography>
        <Typography sx={{ color: t.inkSoft, fontSize: 13, mt: "3px" }}>
          Annual mean {annualMeanTemp} °C · Annual total {annualTotalPrecip} mm · 1991–2020 climate normals
        </Typography>
      </Box>

      {/* Chart */}
      <ChartContainer
        width={chartW}
        height={chartH}
        series={CHART_SERIES}
        xAxis={[
          {
            id: "x",
            scaleType: "band",
            data: months,
            tickLabelStyle: { fontSize: 13 },
          },
        ]}
        yAxis={[
          {
            id: TEMP_AXIS_ID,
            min: 0,
            max: 50,
            tickInterval: 10,
            tickLabelStyle: { fontSize: 13 },
          },
          {
            id: "precipR",
            position: "right",
            min: 0,
            max: 50,
            tickInterval: 10,
            valueFormatter: (v) => `${v * 2}`,
            tickLabelStyle: { fontSize: 13 },
          },
        ]}
        margin={{ top: 15, right: 90, bottom: 48, left: 78 }}
        skipAnimation
        sx={{
          // Suppress right spine — keep left + bottom L-shape only
          "& .MuiChartsAxis-right .MuiChartsAxis-line": { display: "none" },
        }}
      >
        <ChartsGrid horizontal />
        <WalterLiethFill />
        <LinePlot />
        <ChartsXAxis axisId="x" position="bottom" />
        <ChartsYAxis
          axisId={TEMP_AXIS_ID}
          position="left"
          label="Temperature (°C)"
          labelStyle={{ fontSize: 14 }}
        />
        <ChartsYAxis
          axisId="precipR"
          position="right"
          label="Precipitation (mm)"
          labelStyle={{ fontSize: 14 }}
          disableLine
        />
      </ChartContainer>

      {/* Legend */}
      <Box
        sx={{
          display: "flex",
          justifyContent: "center",
          alignItems: "center",
          gap: "28px",
          mt: "6px",
          mb: "6px",
          flexWrap: "wrap",
        }}
      >
        {legendItems.map((item) => (
          <Box
            key={item.label}
            sx={{ display: "flex", alignItems: "center", gap: "7px" }}
          >
            {item.isLine ? (
              <Box
                sx={{
                  width: 26,
                  height: 3,
                  bgcolor: item.color,
                  borderRadius: "2px",
                  flexShrink: 0,
                }}
              />
            ) : (
              <Box
                sx={{
                  width: 22,
                  height: 14,
                  bgcolor: item.color,
                  border: `1.5px solid ${item.border}`,
                  borderRadius: "3px",
                  flexShrink: 0,
                }}
              />
            )}
            <Typography sx={{ fontSize: 13, color: t.inkSoft }}>
              {item.label}
            </Typography>
          </Box>
        ))}
      </Box>

      {/* Plot title */}
      <Typography
        sx={{ color: t.inkSoft, textAlign: "center", fontSize: titleFontSize, lineHeight: 1.3 }}
      >
        {title}
      </Typography>
    </Box>
  );
}

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

Other implementations