Walter-Lieth Climate Diagram in MUI X Charts (JavaScript)

The same plot in 14 other libraries — JavaScript: Chart.js, D3.js, Apache ECharts, Highcharts; Python: Altair, Bokeh, lets-plot, Matplotlib, Plotly, plotnine, Pygal, Seaborn; R: ggplot2; Julia: Makie.jl. Compare all 15 side by side: Walter-Lieth Climate Diagram in Python, R, Julia and JavaScript.

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

Renders

JavaScript source (MUI X Charts)

// anyplot.ai
// climograph-walter-lieth: Walter-Lieth Climate Diagram
// Library: muix 9.16.0 | JavaScript 22.23.3
// Quality: 87/100 | Updated: 2026-10-24

import { ChartsContainer } from "@mui/x-charts/ChartsContainer";
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;

// Constants
const TITLE = "climograph-walter-lieth · javascript · muix · anyplot.ai";
const TEMP_AXIS_ID = "tempY";
const PRECIP_AXIS_ID = "precipR";
const WL_PRECIP_RATIO = 2;
const PRECIP_COMPRESS_MM = 100;
const COMPRESS_RATIO = 10;
const COMPRESS_Y = PRECIP_COMPRESS_MM / WL_PRECIP_RATIO;
const Y_PAD_TOP = 0.075;
const TICK_STEP = 10;
const HUMID_FILL = t.palette[2];
const ARID_FILL = t.palette[4];
const FROST_COLOR = t.palette[5];

// Columns: set these to your data's column names
const STATION = "Station name"; // role: station_name
const ELEVATION = "Elevation (m a.s.l.)"; // role: elevation
const PERIOD = "Normals period"; // role: extra
const MONTH = "Month"; // role: month
const TEMPERATURE = "Temperature (°C)"; // role: temperature
const PRECIPITATION = "Precipitation (mm)"; // role: precipitation

// Data: replace this block with your own records
const data = [
  { [STATION]: "Lisbon, Portugal", [ELEVATION]: 77, [PERIOD]: "1991–2020", [MONTH]: "Jan", [TEMPERATURE]: 11.6, [PRECIPITATION]: 99 },
  { [STATION]: "Lisbon, Portugal", [ELEVATION]: 77, [PERIOD]: "1991–2020", [MONTH]: "Feb", [TEMPERATURE]: 12.6, [PRECIPITATION]: 67 },
  { [STATION]: "Lisbon, Portugal", [ELEVATION]: 77, [PERIOD]: "1991–2020", [MONTH]: "Mar", [TEMPERATURE]: 14.7, [PRECIPITATION]: 52 },
  { [STATION]: "Lisbon, Portugal", [ELEVATION]: 77, [PERIOD]: "1991–2020", [MONTH]: "Apr", [TEMPERATURE]: 16.0, [PRECIPITATION]: 64 },
  { [STATION]: "Lisbon, Portugal", [ELEVATION]: 77, [PERIOD]: "1991–2020", [MONTH]: "May", [TEMPERATURE]: 18.3, [PRECIPITATION]: 48 },
  { [STATION]: "Lisbon, Portugal", [ELEVATION]: 77, [PERIOD]: "1991–2020", [MONTH]: "Jun", [TEMPERATURE]: 21.0, [PRECIPITATION]: 14 },
  { [STATION]: "Lisbon, Portugal", [ELEVATION]: 77, [PERIOD]: "1991–2020", [MONTH]: "Jul", [TEMPERATURE]: 23.0, [PRECIPITATION]: 4 },
  { [STATION]: "Lisbon, Portugal", [ELEVATION]: 77, [PERIOD]: "1991–2020", [MONTH]: "Aug", [TEMPERATURE]: 23.5, [PRECIPITATION]: 6 },
  { [STATION]: "Lisbon, Portugal", [ELEVATION]: 77, [PERIOD]: "1991–2020", [MONTH]: "Sep", [TEMPERATURE]: 21.9, [PRECIPITATION]: 34 },
  { [STATION]: "Lisbon, Portugal", [ELEVATION]: 77, [PERIOD]: "1991–2020", [MONTH]: "Oct", [TEMPERATURE]: 19.1, [PRECIPITATION]: 103 },
  { [STATION]: "Lisbon, Portugal", [ELEVATION]: 77, [PERIOD]: "1991–2020", [MONTH]: "Nov", [TEMPERATURE]: 14.9, [PRECIPITATION]: 120 },
  { [STATION]: "Lisbon, Portugal", [ELEVATION]: 77, [PERIOD]: "1991–2020", [MONTH]: "Dec", [TEMPERATURE]: 12.2, [PRECIPITATION]: 113 },
];

// Plot: reads only data and the constants above
const ELEVATION_UNIT = ELEVATION.match(/\(([^)]+)\)/)?.[1] ?? "";
const TEMP_UNIT = TEMPERATURE.match(/\(([^)]+)\)/)?.[1] ?? "";
const PRECIP_UNIT = PRECIPITATION.match(/\(([^)]+)\)/)?.[1] ?? "";

const stationName = String(data[0][STATION]);
const elevation = Number(data[0][ELEVATION]);
const normalsPeriod = String(data[0][PERIOD]);
const months = data.map((d) => String(d[MONTH]));
const temperature = data.map((d) => Number(d[TEMPERATURE]));
const precipitation = data.map((d) => Number(d[PRECIPITATION]));
const precipScaled = precipitation.map((p) =>
  p <= PRECIP_COMPRESS_MM
    ? p / WL_PRECIP_RATIO
    : COMPRESS_Y + (p - PRECIP_COMPRESS_MM) / (WL_PRECIP_RATIO * COMPRESS_RATIO),
);

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

const minDataVal = Math.min(0, ...temperature, ...precipScaled);
const maxDataVal = Math.max(...temperature, ...precipScaled);
const yMin = minDataVal < 0 ? minDataVal * (1 + Y_PAD_TOP) : 0;
const yMax = maxDataVal * (1 + Y_PAD_TOP);
// Conventional Walter-Lieth ticks: every 10 °C on the left, i.e. every 20 mm on the right
const yTicks = [];
for (let v = Math.ceil(yMin / TICK_STEP) * TICK_STEP; v <= yMax; v += TICK_STEP) yTicks.push(v);

const hasFrostMonths = temperature.some((temp) => temp < 0);
const hasPerhumidMonths = precipitation.some((p) => p > PRECIP_COMPRESS_MM);
const legendItems = [
  { color: ARID_FILL, isLine: true, label: TEMPERATURE },
  { color: HUMID_FILL, isLine: true, label: PRECIPITATION },
  { color: `${HUMID_FILL}61`, border: HUMID_FILL, label: "Humid period" },
  { color: `${ARID_FILL}61`, border: ARID_FILL, label: "Arid period" },
  ...(hasPerhumidMonths
    ? [{ color: `${HUMID_FILL}D9`, border: HUMID_FILL, label: `Perhumid (> ${PRECIP_COMPRESS_MM} ${PRECIP_UNIT}, ${COMPRESS_RATIO}:1 scale)` }]
    : []),
  ...(hasFrostMonths
    ? [{ color: `${FROST_COLOR}D9`, border: FROST_COLOR, label: `Frost month (< 0 ${TEMP_UNIT})` }]
    : []),
];

const CHART_SERIES = [
  {
    id: "temp",
    label: TEMPERATURE,
    data: temperature,
    type: "line" as const,
    color: ARID_FILL,
    yAxisId: TEMP_AXIS_ID,
    showMark: false,
    curve: "linear" as const,
  },
  {
    id: "precip",
    label: PRECIPITATION,
    data: precipScaled,
    type: "line" as const,
    color: HUMID_FILL,
    yAxisId: TEMP_AXIS_ID,
    showMark: false,
    curve: "linear" as const,
  },
];

function WalterLiethFill() {
  const { top, height } = useDrawingArea();
  const xScale = useXScale("x") as any;
  const yScale = useYScale(TEMP_AXIS_ID) as any;

  const bw = xScale.bandwidth();
  const xs = months.map((m) => xScale(m) + bw / 2);
  const tys = temperature.map((v) => yScale(v));
  const pys = precipScaled.map((v) => yScale(v));
  const compressY = yScale(COMPRESS_Y);

  const elems = [];

  for (let i = 0; i < months.length - 1; i++) {
    const x0 = xs[i];
    const x1 = xs[i + 1];
    const ty0 = tys[i];
    const ty1 = tys[i + 1];
    const py0 = pys[i];
    const py1 = pys[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 {
      const denom = py1 - py0 - (ty1 - ty0);
      const frac = (ty0 - py0) / denom;
      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}
        />,
      );
    }

    const p0Above = precipScaled[i] > COMPRESS_Y;
    const p1Above = precipScaled[i + 1] > COMPRESS_Y;
    if (p0Above && p1Above) {
      elems.push(
        <polygon
          key={`ph${i}`}
          points={`${x0},${py0} ${x1},${py1} ${x1},${compressY} ${x0},${compressY}`}
          fill={HUMID_FILL}
          fillOpacity={0.85}
        />,
      );
    } else if (p0Above || p1Above) {
      const frac = (COMPRESS_Y - precipScaled[i]) / (precipScaled[i + 1] - precipScaled[i]);
      const xc = x0 + (x1 - x0) * frac;
      if (p0Above) {
        elems.push(
          <polygon
            key={`ph${i}a`}
            points={`${x0},${py0} ${xc},${compressY} ${x0},${compressY}`}
            fill={HUMID_FILL}
            fillOpacity={0.85}
          />,
        );
      } else {
        elems.push(
          <polygon
            key={`ph${i}b`}
            points={`${xc},${compressY} ${x1},${py1} ${x1},${compressY}`}
            fill={HUMID_FILL}
            fillOpacity={0.85}
          />,
        );
      }
    }
  }

  for (let i = 0; i < months.length; i++) {
    if (temperature[i] < 0) {
      const xi = xs[i];
      elems.push(
        <rect
          key={`frost${i}`}
          x={xi - bw / 2}
          y={top + height + 2}
          width={bw}
          height={6}
          fill={FROST_COLOR}
          fillOpacity={0.85}
        />,
      );
    }
  }

  return <g>{elems}</g>;
}

export default function Chart() {
  const { width, height } = window.ANYPLOT_SIZE;
  const chartH = height - 172;
  const chartW = width - 48;

  return (
    <Box
      sx={{
        width,
        height,
        bgcolor: t.pageBg,
        display: "flex",
        flexDirection: "column",
        px: "24px",
        pt: "18px",
        pb: "14px",
        boxSizing: "border-box",
      }}
    >
      <Box sx={{ textAlign: "center", mb: "10px" }}>
        <Typography sx={{ color: t.ink, fontWeight: 700, fontSize: 22, lineHeight: 1.3 }}>
          {TITLE}
        </Typography>
        <Typography sx={{ color: t.inkSoft, fontSize: 15, mt: "4px" }}>
          {`${stationName} (${elevation} ${ELEVATION_UNIT}) · Annual mean ${annualMeanTemp} ${TEMP_UNIT} · Annual total ${annualTotalPrecip} ${PRECIP_UNIT} · ${PERIOD}: ${normalsPeriod}`}
        </Typography>
      </Box>

      <ChartsContainer
        width={chartW}
        height={chartH}
        series={CHART_SERIES}
        xAxis={[
          {
            id: "x",
            scaleType: "band",
            data: months,
            height: 44,
            tickLabelStyle: { fontSize: 14, fill: t.inkSoft },
          },
        ]}
        yAxis={[
          {
            id: TEMP_AXIS_ID,
            position: "left",
            min: yMin,
            max: yMax,
            width: 76,
            tickInterval: yTicks,
            label: TEMPERATURE,
            labelStyle: { fontSize: 15, fill: t.ink },
            tickLabelStyle: { fontSize: 14, fill: t.inkSoft },
          },
          {
            id: PRECIP_AXIS_ID,
            position: "right",
            min: yMin,
            max: yMax,
            width: 84,
            tickInterval: yTicks,
            label: PRECIPITATION,
            valueFormatter: (v: number) =>
              `${
                v <= COMPRESS_Y
                  ? Math.round(v * WL_PRECIP_RATIO)
                  : Math.round(PRECIP_COMPRESS_MM + (v - COMPRESS_Y) * WL_PRECIP_RATIO * COMPRESS_RATIO)
              }`,
            labelStyle: { fontSize: 15, fill: t.ink },
            tickLabelStyle: { fontSize: 14, fill: t.inkSoft },
          },
        ]}
        margin={{ top: 16, right: 24, bottom: 16, left: 24 }}
        skipAnimation
        sx={{
          "& .MuiChartsAxis-line": { stroke: t.inkSoft },
          "& .MuiChartsAxis-tick": { stroke: t.inkSoft },
          "& .MuiChartsGrid-line": { stroke: t.grid },
          "& .MuiLineChart-line": { strokeWidth: 3, strokeLinecap: "round" },
        }}
      >
        <ChartsGrid horizontal />
        <WalterLiethFill />
        <LinePlot />
        <ChartsXAxis axisId="x" />
        <ChartsYAxis axisId={TEMP_AXIS_ID} />
        <ChartsYAxis axisId={PRECIP_AXIS_ID} disableLine />
      </ChartsContainer>

      <Box
        sx={{
          display: "flex",
          justifyContent: "center",
          alignItems: "center",
          gap: "28px",
          mt: "8px",
          flexWrap: "wrap",
        }}
      >
        {legendItems.map((item) => (
          <Box key={item.label} sx={{ display: "flex", alignItems: "center", gap: "8px" }}>
            {item.isLine ? (
              <Box
                sx={{
                  width: 26,
                  height: 3.5,
                  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: 14, color: t.inkSoft }}>{item.label}</Typography>
          </Box>
        ))}
      </Box>
    </Box>
  );
}

Retrieve this implementation

Runnable source as JSON, for any HTTP client: https://api.anyplot.ai/specs/climograph-walter-lieth/muix/code. Any spec id and library id listed in llms-full.txt fit the same URL shape; every URL below is complete and callable.

{
  "spec_id": "climograph-walter-lieth",
  "language": "javascript",
  "library": "muix",
  "page": "https://anyplot.ai/climograph-walter-lieth/javascript/muix",
  "hub": "https://anyplot.ai/climograph-walter-lieth",
  "code_json": "https://api.anyplot.ai/specs/climograph-walter-lieth/muix/code",
  "spec_json": "https://api.anyplot.ai/specs/climograph-walter-lieth",
  "render_light_png": "https://storage.googleapis.com/anyplot-images/plots/climograph-walter-lieth/javascript/muix/plot-light.png",
  "render_dark_png": "https://storage.googleapis.com/anyplot-images/plots/climograph-walter-lieth/javascript/muix/plot-dark.png",
  "interactive_light_html": "https://storage.googleapis.com/anyplot-images/plots/climograph-walter-lieth/javascript/muix/plot-light.html",
  "interactive_dark_html": "https://storage.googleapis.com/anyplot-images/plots/climograph-walter-lieth/javascript/muix/plot-dark.html",
  "quality_score": 87.0,
  "license": "MIT",
  "guide": "https://anyplot.ai/llms.txt"
}

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

Other implementations