{"spec_id":"climograph-walter-lieth","library":"muix","language":"javascript","code":"// anyplot.ai\n// climograph-walter-lieth: Walter-Lieth Climate Diagram\n// Library: muix 9.16.0 | JavaScript 22.23.3\n// Quality: 87/100 | Updated: 2026-10-24\n\nimport { ChartsContainer } from \"@mui/x-charts/ChartsContainer\";\nimport { LinePlot } from \"@mui/x-charts/LineChart\";\nimport { ChartsXAxis } from \"@mui/x-charts/ChartsXAxis\";\nimport { ChartsYAxis } from \"@mui/x-charts/ChartsYAxis\";\nimport { ChartsGrid } from \"@mui/x-charts/ChartsGrid\";\nimport { useDrawingArea, useXScale, useYScale } from \"@mui/x-charts/hooks\";\nimport Box from \"@mui/material/Box\";\nimport Typography from \"@mui/material/Typography\";\n\nconst t = window.ANYPLOT_TOKENS;\n\n// Constants\nconst TITLE = \"climograph-walter-lieth · javascript · muix · anyplot.ai\";\nconst TEMP_AXIS_ID = \"tempY\";\nconst PRECIP_AXIS_ID = \"precipR\";\nconst WL_PRECIP_RATIO = 2;\nconst PRECIP_COMPRESS_MM = 100;\nconst COMPRESS_RATIO = 10;\nconst COMPRESS_Y = PRECIP_COMPRESS_MM / WL_PRECIP_RATIO;\nconst Y_PAD_TOP = 0.075;\nconst TICK_STEP = 10;\nconst HUMID_FILL = t.palette[2];\nconst ARID_FILL = t.palette[4];\nconst FROST_COLOR = t.palette[5];\n\n// Columns: set these to your data's column names\nconst STATION = \"Station name\"; // role: station_name\nconst ELEVATION = \"Elevation (m a.s.l.)\"; // role: elevation\nconst PERIOD = \"Normals period\"; // role: extra\nconst MONTH = \"Month\"; // role: month\nconst TEMPERATURE = \"Temperature (°C)\"; // role: temperature\nconst PRECIPITATION = \"Precipitation (mm)\"; // role: precipitation\n\n// Data: replace this block with your own records\nconst data = [\n  { [STATION]: \"Lisbon, Portugal\", [ELEVATION]: 77, [PERIOD]: \"1991–2020\", [MONTH]: \"Jan\", [TEMPERATURE]: 11.6, [PRECIPITATION]: 99 },\n  { [STATION]: \"Lisbon, Portugal\", [ELEVATION]: 77, [PERIOD]: \"1991–2020\", [MONTH]: \"Feb\", [TEMPERATURE]: 12.6, [PRECIPITATION]: 67 },\n  { [STATION]: \"Lisbon, Portugal\", [ELEVATION]: 77, [PERIOD]: \"1991–2020\", [MONTH]: \"Mar\", [TEMPERATURE]: 14.7, [PRECIPITATION]: 52 },\n  { [STATION]: \"Lisbon, Portugal\", [ELEVATION]: 77, [PERIOD]: \"1991–2020\", [MONTH]: \"Apr\", [TEMPERATURE]: 16.0, [PRECIPITATION]: 64 },\n  { [STATION]: \"Lisbon, Portugal\", [ELEVATION]: 77, [PERIOD]: \"1991–2020\", [MONTH]: \"May\", [TEMPERATURE]: 18.3, [PRECIPITATION]: 48 },\n  { [STATION]: \"Lisbon, Portugal\", [ELEVATION]: 77, [PERIOD]: \"1991–2020\", [MONTH]: \"Jun\", [TEMPERATURE]: 21.0, [PRECIPITATION]: 14 },\n  { [STATION]: \"Lisbon, Portugal\", [ELEVATION]: 77, [PERIOD]: \"1991–2020\", [MONTH]: \"Jul\", [TEMPERATURE]: 23.0, [PRECIPITATION]: 4 },\n  { [STATION]: \"Lisbon, Portugal\", [ELEVATION]: 77, [PERIOD]: \"1991–2020\", [MONTH]: \"Aug\", [TEMPERATURE]: 23.5, [PRECIPITATION]: 6 },\n  { [STATION]: \"Lisbon, Portugal\", [ELEVATION]: 77, [PERIOD]: \"1991–2020\", [MONTH]: \"Sep\", [TEMPERATURE]: 21.9, [PRECIPITATION]: 34 },\n  { [STATION]: \"Lisbon, Portugal\", [ELEVATION]: 77, [PERIOD]: \"1991–2020\", [MONTH]: \"Oct\", [TEMPERATURE]: 19.1, [PRECIPITATION]: 103 },\n  { [STATION]: \"Lisbon, Portugal\", [ELEVATION]: 77, [PERIOD]: \"1991–2020\", [MONTH]: \"Nov\", [TEMPERATURE]: 14.9, [PRECIPITATION]: 120 },\n  { [STATION]: \"Lisbon, Portugal\", [ELEVATION]: 77, [PERIOD]: \"1991–2020\", [MONTH]: \"Dec\", [TEMPERATURE]: 12.2, [PRECIPITATION]: 113 },\n];\n\n// Plot: reads only data and the constants above\nconst ELEVATION_UNIT = ELEVATION.match(/\\(([^)]+)\\)/)?.[1] ?? \"\";\nconst TEMP_UNIT = TEMPERATURE.match(/\\(([^)]+)\\)/)?.[1] ?? \"\";\nconst PRECIP_UNIT = PRECIPITATION.match(/\\(([^)]+)\\)/)?.[1] ?? \"\";\n\nconst stationName = String(data[0][STATION]);\nconst elevation = Number(data[0][ELEVATION]);\nconst normalsPeriod = String(data[0][PERIOD]);\nconst months = data.map((d) => String(d[MONTH]));\nconst temperature = data.map((d) => Number(d[TEMPERATURE]));\nconst precipitation = data.map((d) => Number(d[PRECIPITATION]));\nconst precipScaled = precipitation.map((p) =>\n  p <= PRECIP_COMPRESS_MM\n    ? p / WL_PRECIP_RATIO\n    : COMPRESS_Y + (p - PRECIP_COMPRESS_MM) / (WL_PRECIP_RATIO * COMPRESS_RATIO),\n);\n\nconst annualMeanTemp = (temperature.reduce((a, b) => a + b, 0) / data.length).toFixed(1);\nconst annualTotalPrecip = precipitation.reduce((a, b) => a + b, 0);\n\nconst minDataVal = Math.min(0, ...temperature, ...precipScaled);\nconst maxDataVal = Math.max(...temperature, ...precipScaled);\nconst yMin = minDataVal < 0 ? minDataVal * (1 + Y_PAD_TOP) : 0;\nconst yMax = maxDataVal * (1 + Y_PAD_TOP);\n// Conventional Walter-Lieth ticks: every 10 °C on the left, i.e. every 20 mm on the right\nconst yTicks = [];\nfor (let v = Math.ceil(yMin / TICK_STEP) * TICK_STEP; v <= yMax; v += TICK_STEP) yTicks.push(v);\n\nconst hasFrostMonths = temperature.some((temp) => temp < 0);\nconst hasPerhumidMonths = precipitation.some((p) => p > PRECIP_COMPRESS_MM);\nconst legendItems = [\n  { color: ARID_FILL, isLine: true, label: TEMPERATURE },\n  { color: HUMID_FILL, isLine: true, label: PRECIPITATION },\n  { color: `${HUMID_FILL}61`, border: HUMID_FILL, label: \"Humid period\" },\n  { color: `${ARID_FILL}61`, border: ARID_FILL, label: \"Arid period\" },\n  ...(hasPerhumidMonths\n    ? [{ color: `${HUMID_FILL}D9`, border: HUMID_FILL, label: `Perhumid (> ${PRECIP_COMPRESS_MM} ${PRECIP_UNIT}, ${COMPRESS_RATIO}:1 scale)` }]\n    : []),\n  ...(hasFrostMonths\n    ? [{ color: `${FROST_COLOR}D9`, border: FROST_COLOR, label: `Frost month (< 0 ${TEMP_UNIT})` }]\n    : []),\n];\n\nconst CHART_SERIES = [\n  {\n    id: \"temp\",\n    label: TEMPERATURE,\n    data: temperature,\n    type: \"line\" as const,\n    color: ARID_FILL,\n    yAxisId: TEMP_AXIS_ID,\n    showMark: false,\n    curve: \"linear\" as const,\n  },\n  {\n    id: \"precip\",\n    label: PRECIPITATION,\n    data: precipScaled,\n    type: \"line\" as const,\n    color: HUMID_FILL,\n    yAxisId: TEMP_AXIS_ID,\n    showMark: false,\n    curve: \"linear\" as const,\n  },\n];\n\nfunction WalterLiethFill() {\n  const { top, height } = useDrawingArea();\n  const xScale = useXScale(\"x\") as any;\n  const yScale = useYScale(TEMP_AXIS_ID) as any;\n\n  const bw = xScale.bandwidth();\n  const xs = months.map((m) => xScale(m) + bw / 2);\n  const tys = temperature.map((v) => yScale(v));\n  const pys = precipScaled.map((v) => yScale(v));\n  const compressY = yScale(COMPRESS_Y);\n\n  const elems = [];\n\n  for (let i = 0; i < months.length - 1; i++) {\n    const x0 = xs[i];\n    const x1 = xs[i + 1];\n    const ty0 = tys[i];\n    const ty1 = tys[i + 1];\n    const py0 = pys[i];\n    const py1 = pys[i + 1];\n    const h0 = precipScaled[i] > temperature[i];\n    const h1 = precipScaled[i + 1] > temperature[i + 1];\n\n    if (h0 === h1) {\n      elems.push(\n        <polygon\n          key={`f${i}`}\n          points={`${x0},${py0} ${x1},${py1} ${x1},${ty1} ${x0},${ty0}`}\n          fill={h0 ? HUMID_FILL : ARID_FILL}\n          fillOpacity={0.38}\n        />,\n      );\n    } else {\n      const denom = py1 - py0 - (ty1 - ty0);\n      const frac = (ty0 - py0) / denom;\n      const xc = x0 + (x1 - x0) * frac;\n      const yc = ty0 + (ty1 - ty0) * frac;\n      elems.push(\n        <polygon\n          key={`f${i}a`}\n          points={`${x0},${py0} ${xc},${yc} ${x0},${ty0}`}\n          fill={h0 ? HUMID_FILL : ARID_FILL}\n          fillOpacity={0.38}\n        />,\n        <polygon\n          key={`f${i}b`}\n          points={`${xc},${yc} ${x1},${py1} ${x1},${ty1}`}\n          fill={h1 ? HUMID_FILL : ARID_FILL}\n          fillOpacity={0.38}\n        />,\n      );\n    }\n\n    const p0Above = precipScaled[i] > COMPRESS_Y;\n    const p1Above = precipScaled[i + 1] > COMPRESS_Y;\n    if (p0Above && p1Above) {\n      elems.push(\n        <polygon\n          key={`ph${i}`}\n          points={`${x0},${py0} ${x1},${py1} ${x1},${compressY} ${x0},${compressY}`}\n          fill={HUMID_FILL}\n          fillOpacity={0.85}\n        />,\n      );\n    } else if (p0Above || p1Above) {\n      const frac = (COMPRESS_Y - precipScaled[i]) / (precipScaled[i + 1] - precipScaled[i]);\n      const xc = x0 + (x1 - x0) * frac;\n      if (p0Above) {\n        elems.push(\n          <polygon\n            key={`ph${i}a`}\n            points={`${x0},${py0} ${xc},${compressY} ${x0},${compressY}`}\n            fill={HUMID_FILL}\n            fillOpacity={0.85}\n          />,\n        );\n      } else {\n        elems.push(\n          <polygon\n            key={`ph${i}b`}\n            points={`${xc},${compressY} ${x1},${py1} ${x1},${compressY}`}\n            fill={HUMID_FILL}\n            fillOpacity={0.85}\n          />,\n        );\n      }\n    }\n  }\n\n  for (let i = 0; i < months.length; i++) {\n    if (temperature[i] < 0) {\n      const xi = xs[i];\n      elems.push(\n        <rect\n          key={`frost${i}`}\n          x={xi - bw / 2}\n          y={top + height + 2}\n          width={bw}\n          height={6}\n          fill={FROST_COLOR}\n          fillOpacity={0.85}\n        />,\n      );\n    }\n  }\n\n  return <g>{elems}</g>;\n}\n\nexport default function Chart() {\n  const { width, height } = window.ANYPLOT_SIZE;\n  const chartH = height - 172;\n  const chartW = width - 48;\n\n  return (\n    <Box\n      sx={{\n        width,\n        height,\n        bgcolor: t.pageBg,\n        display: \"flex\",\n        flexDirection: \"column\",\n        px: \"24px\",\n        pt: \"18px\",\n        pb: \"14px\",\n        boxSizing: \"border-box\",\n      }}\n    >\n      <Box sx={{ textAlign: \"center\", mb: \"10px\" }}>\n        <Typography sx={{ color: t.ink, fontWeight: 700, fontSize: 22, lineHeight: 1.3 }}>\n          {TITLE}\n        </Typography>\n        <Typography sx={{ color: t.inkSoft, fontSize: 15, mt: \"4px\" }}>\n          {`${stationName} (${elevation} ${ELEVATION_UNIT}) · Annual mean ${annualMeanTemp} ${TEMP_UNIT} · Annual total ${annualTotalPrecip} ${PRECIP_UNIT} · ${PERIOD}: ${normalsPeriod}`}\n        </Typography>\n      </Box>\n\n      <ChartsContainer\n        width={chartW}\n        height={chartH}\n        series={CHART_SERIES}\n        xAxis={[\n          {\n            id: \"x\",\n            scaleType: \"band\",\n            data: months,\n            height: 44,\n            tickLabelStyle: { fontSize: 14, fill: t.inkSoft },\n          },\n        ]}\n        yAxis={[\n          {\n            id: TEMP_AXIS_ID,\n            position: \"left\",\n            min: yMin,\n            max: yMax,\n            width: 76,\n            tickInterval: yTicks,\n            label: TEMPERATURE,\n            labelStyle: { fontSize: 15, fill: t.ink },\n            tickLabelStyle: { fontSize: 14, fill: t.inkSoft },\n          },\n          {\n            id: PRECIP_AXIS_ID,\n            position: \"right\",\n            min: yMin,\n            max: yMax,\n            width: 84,\n            tickInterval: yTicks,\n            label: PRECIPITATION,\n            valueFormatter: (v: number) =>\n              `${\n                v <= COMPRESS_Y\n                  ? Math.round(v * WL_PRECIP_RATIO)\n                  : Math.round(PRECIP_COMPRESS_MM + (v - COMPRESS_Y) * WL_PRECIP_RATIO * COMPRESS_RATIO)\n              }`,\n            labelStyle: { fontSize: 15, fill: t.ink },\n            tickLabelStyle: { fontSize: 14, fill: t.inkSoft },\n          },\n        ]}\n        margin={{ top: 16, right: 24, bottom: 16, left: 24 }}\n        skipAnimation\n        sx={{\n          \"& .MuiChartsAxis-line\": { stroke: t.inkSoft },\n          \"& .MuiChartsAxis-tick\": { stroke: t.inkSoft },\n          \"& .MuiChartsGrid-line\": { stroke: t.grid },\n          \"& .MuiLineChart-line\": { strokeWidth: 3, strokeLinecap: \"round\" },\n        }}\n      >\n        <ChartsGrid horizontal />\n        <WalterLiethFill />\n        <LinePlot />\n        <ChartsXAxis axisId=\"x\" />\n        <ChartsYAxis axisId={TEMP_AXIS_ID} />\n        <ChartsYAxis axisId={PRECIP_AXIS_ID} disableLine />\n      </ChartsContainer>\n\n      <Box\n        sx={{\n          display: \"flex\",\n          justifyContent: \"center\",\n          alignItems: \"center\",\n          gap: \"28px\",\n          mt: \"8px\",\n          flexWrap: \"wrap\",\n        }}\n      >\n        {legendItems.map((item) => (\n          <Box key={item.label} sx={{ display: \"flex\", alignItems: \"center\", gap: \"8px\" }}>\n            {item.isLine ? (\n              <Box\n                sx={{\n                  width: 26,\n                  height: 3.5,\n                  bgcolor: item.color,\n                  borderRadius: \"2px\",\n                  flexShrink: 0,\n                }}\n              />\n            ) : (\n              <Box\n                sx={{\n                  width: 22,\n                  height: 14,\n                  bgcolor: item.color,\n                  border: `1.5px solid ${item.border}`,\n                  borderRadius: \"3px\",\n                  flexShrink: 0,\n                }}\n              />\n            )}\n            <Typography sx={{ fontSize: 14, color: t.inkSoft }}>{item.label}</Typography>\n          </Box>\n        ))}\n      </Box>\n    </Box>\n  );\n}\n"}