{"spec_id":"climograph-walter-lieth","library":"d3","language":"javascript","code":"// anyplot.ai\n// climograph-walter-lieth: Walter-Lieth Climate Diagram\n// Library: d3 7.9.0 | JavaScript 22.22.3\n// Quality: 93/100 | Created: 2026-06-15\n\nconst t = window.ANYPLOT_TOKENS;\nconst { width, height } = window.ANYPLOT_SIZE;\n\n// Palermo, Sicily — classic Mediterranean climate, 1991–2020 normals\nconst station = {\n  name: \"Palermo\",\n  region: \"Sicily, Italy\",\n  elevation: 15,\n  tempMean: 18.3,\n  precipTotal: 551,\n  period: \"1991–2020\",\n};\nconst MONTHS  = [\"Jan\",\"Feb\",\"Mar\",\"Apr\",\"May\",\"Jun\",\"Jul\",\"Aug\",\"Sep\",\"Oct\",\"Nov\",\"Dec\"];\nconst temps   = [11.1, 11.8, 13.2, 15.7, 19.3, 23.6, 26.7, 27.1, 24.1, 19.8, 15.4, 12.2];\nconst precips = [  67,   57,   47,   36,   20,    7,    3,   14,   38,   85,   91,   86];\nconst data    = MONTHS.map((month, i) => ({ month, temp: temps[i], precip: precips[i] }));\n\n// Walter-Lieth convention: 10 °C ↔ 20 mm (2:1 ratio)\n// Bottom = 0 °C / 0 mm; top = 50 °C / 100 mm (perhumid threshold)\nconst TEMP_MIN = 0;\nconst TEMP_MAX = 50;\n\nconst margin = { top: 145, right: 115, bottom: 62, left: 82 };\nconst iw = width  - margin.left - margin.right;\nconst ih = height - margin.top  - margin.bottom;\n\n// Semantic colors: temperature → Imprint red, precipitation → Imprint blue\nconst TEMP_COLOR = \"#AE3030\";\nconst PRCP_COLOR = \"#4467A3\";\n\n// ── SVG root ────────────────────────────────────────────────────────────────\nconst svg = d3.select(\"#container\").append(\"svg\")\n  .attr(\"width\", width).attr(\"height\", height);\n\nconst g = svg.append(\"g\").attr(\"transform\", `translate(${margin.left},${margin.top})`);\n\n// ── Scales ──────────────────────────────────────────────────────────────────\nconst xScale  = d3.scaleBand().domain(MONTHS).range([0, iw]).padding(0);\nconst yTemp   = d3.scaleLinear().domain([TEMP_MIN, TEMP_MAX]).range([ih, 0]);\n// Right-axis scale mirrors temperature scale at 2:1 ratio (0–100 mm = 0–50 °C)\nconst yPrcpAx = d3.scaleLinear().domain([0, 100]).range([ih, 0]);\n\n// Precipitation → pixel (2:1 ratio; above 100 mm compressed to 10:1)\nconst yP = (mm) => mm <= 100 ? yTemp(mm / 2) : yTemp(50 + (mm - 100) * 0.1);\n\n// x center of each month band\nconst xMid = (d) => xScale(d.month) + xScale.bandwidth() / 2;\n\n// Coordinate arrays in group-local space\nconst tPts = data.map((d) => [xMid(d), yTemp(d.temp)]);\nconst pPts = data.map((d) => [xMid(d), yP(d.precip)]);\n\n// ── SVG <defs>: fill patterns + masks ──────────────────────────────────────\nconst defs = svg.append(\"defs\");\n\n// Arid fill pattern — red dots on translucent red ground (Walter-Lieth \"dotted\")\nconst aPat = defs.append(\"pattern\").attr(\"id\", \"aridPat\")\n  .attr(\"patternUnits\", \"userSpaceOnUse\").attr(\"width\", 10).attr(\"height\", 10);\naPat.append(\"rect\").attr(\"width\", 10).attr(\"height\", 10).attr(\"fill\", \"rgba(174,48,48,0.20)\");\naPat.append(\"circle\").attr(\"cx\", 5).attr(\"cy\", 5).attr(\"r\", 1.8)\n  .attr(\"fill\", \"rgba(174,48,48,0.50)\");\n\n// Humid fill pattern — diagonal hatching on translucent blue ground (Walter-Lieth \"hatched\")\nconst hPat = defs.append(\"pattern\").attr(\"id\", \"humidPat\")\n  .attr(\"patternUnits\", \"userSpaceOnUse\").attr(\"width\", 10).attr(\"height\", 10);\nhPat.append(\"rect\").attr(\"width\", 10).attr(\"height\", 10).attr(\"fill\", \"rgba(68,103,163,0.20)\");\n[[-5, 5, 5, -5],[0, 10, 10, 0],[5, 15, 15, 5]].forEach(([x1, y1, x2, y2]) => {\n  hPat.append(\"line\").attr(\"x1\", x1).attr(\"y1\", y1).attr(\"x2\", x2).attr(\"y2\", y2)\n    .attr(\"stroke\", \"rgba(68,103,163,0.55)\").attr(\"stroke-width\", 1.5);\n});\n\n// Closed polygon \"below the curve\", extended to the full plot width\n// Mask coordinates are in group-local space (0→iw, 0→ih)\nconst belowPath = (pts) => {\n  let d = `M0,${ih} L0,${pts[0][1]}`;\n  pts.forEach(([x, y]) => { d += ` L${x},${y}`; });\n  d += ` L${iw},${pts[pts.length - 1][1]} L${iw},${ih} Z`;\n  return d;\n};\n\n// SVG mask: show (below primaryPts) minus (below subtractPts)\nconst addMask = (id, primaryPts, subtractPts) => {\n  const m = defs.append(\"mask\").attr(\"id\", id);\n  m.append(\"rect\").attr(\"x\", 0).attr(\"y\", 0).attr(\"width\", iw).attr(\"height\", ih)\n    .attr(\"fill\", \"black\");\n  m.append(\"path\").attr(\"d\", belowPath(primaryPts)).attr(\"fill\", \"white\");\n  m.append(\"path\").attr(\"d\", belowPath(subtractPts)).attr(\"fill\", \"black\");\n};\n\naddMask(\"aridMask\",  tPts, pPts); // below temp curve AND above precip curve → arid\naddMask(\"humidMask\", pPts, tPts); // below precip curve AND above temp curve → humid\n\n// ── Grid lines ───────────────────────────────────────────────────────────────\ng.append(\"g\").selectAll(\"line\").data([0, 10, 20, 30, 40, 50]).join(\"line\")\n  .attr(\"x1\", 0).attr(\"x2\", iw)\n  .attr(\"y1\", (v) => yTemp(v)).attr(\"y2\", (v) => yTemp(v))\n  .attr(\"stroke\", t.grid).attr(\"stroke-width\", 1);\n\n// Vertical month dividers (subtle)\nMONTHS.forEach((m) => {\n  g.append(\"line\")\n    .attr(\"x1\", xScale(m)).attr(\"x2\", xScale(m))\n    .attr(\"y1\", 0).attr(\"y2\", ih)\n    .attr(\"stroke\", t.grid).attr(\"stroke-width\", 0.5);\n});\n\n// ── Filled areas (pattern + mask) ────────────────────────────────────────────\ng.append(\"rect\").attr(\"width\", iw).attr(\"height\", ih)\n  .attr(\"fill\", \"url(#aridPat)\").attr(\"mask\", \"url(#aridMask)\");\ng.append(\"rect\").attr(\"width\", iw).attr(\"height\", ih)\n  .attr(\"fill\", \"url(#humidPat)\").attr(\"mask\", \"url(#humidMask)\");\n\n// ── Data lines ───────────────────────────────────────────────────────────────\n// Lines extended to full plot width for a clean edge-to-edge look\nconst extLinePath = (pts) => {\n  const ex = [[0, pts[0][1]], ...pts, [iw, pts[pts.length - 1][1]]];\n  return \"M\" + ex.map(([x, y]) => `${x},${y}`).join(\" L\");\n};\n\ng.append(\"path\").attr(\"d\", extLinePath(tPts))\n  .attr(\"fill\", \"none\").attr(\"stroke\", TEMP_COLOR)\n  .attr(\"stroke-width\", 2.5).attr(\"stroke-linejoin\", \"round\");\n\ng.append(\"path\").attr(\"d\", extLinePath(pPts))\n  .attr(\"fill\", \"none\").attr(\"stroke\", PRCP_COLOR)\n  .attr(\"stroke-width\", 2.5).attr(\"stroke-linejoin\", \"round\");\n\n// ── Left axis: Temperature (°C) ──────────────────────────────────────────────\nconst axL = g.append(\"g\").call(\n  d3.axisLeft(yTemp).tickValues([0, 10, 20, 30, 40, 50])\n    .tickSize(5).tickFormat(d3.format(\"d\"))\n);\naxL.selectAll(\"text\").attr(\"fill\", t.inkSoft).style(\"font-size\", \"14px\");\naxL.selectAll(\".tick line\").attr(\"stroke\", t.inkSoft);\naxL.select(\".domain\").attr(\"stroke\", t.inkSoft);\n\ng.append(\"text\")\n  .attr(\"transform\", `translate(-60,${ih / 2}) rotate(-90)`)\n  .attr(\"text-anchor\", \"middle\").attr(\"fill\", t.ink).style(\"font-size\", \"15px\")\n  .text(\"Temperature (°C)\");\n\n// ── Right axis: Precipitation (mm) ───────────────────────────────────────────\nconst axR = g.append(\"g\").attr(\"transform\", `translate(${iw},0)`).call(\n  d3.axisRight(yPrcpAx).tickValues([0, 20, 40, 60, 80, 100])\n    .tickSize(5).tickFormat(d3.format(\"d\"))\n);\naxR.selectAll(\"text\").attr(\"fill\", t.inkSoft).style(\"font-size\", \"14px\");\naxR.selectAll(\".tick line\").attr(\"stroke\", t.inkSoft);\naxR.select(\".domain\").attr(\"stroke\", t.inkSoft);\n\ng.append(\"text\")\n  .attr(\"transform\", `translate(${iw + 74},${ih / 2}) rotate(90)`)\n  .attr(\"text-anchor\", \"middle\").attr(\"fill\", t.ink).style(\"font-size\", \"15px\")\n  .text(\"Precipitation (mm)\");\n\n// ── Bottom axis: Months ───────────────────────────────────────────────────────\nconst axB = g.append(\"g\").attr(\"transform\", `translate(0,${ih})`).call(\n  d3.axisBottom(xScale).tickSize(4)\n);\naxB.selectAll(\"text\").attr(\"fill\", t.inkSoft).style(\"font-size\", \"14px\");\naxB.selectAll(\".tick line\").attr(\"stroke\", t.inkSoft);\naxB.select(\".domain\").attr(\"stroke\", t.inkSoft);\n\n// ── Header ────────────────────────────────────────────────────────────────────\n// Mandated anyplot title\nsvg.append(\"text\").attr(\"x\", width / 2).attr(\"y\", 30)\n  .attr(\"text-anchor\", \"middle\").attr(\"fill\", t.ink)\n  .style(\"font-size\", \"20px\").style(\"font-weight\", \"600\")\n  .text(\"climograph-walter-lieth · javascript · d3 · anyplot.ai\");\n\n// Station name\nsvg.append(\"text\").attr(\"x\", margin.left).attr(\"y\", 70)\n  .attr(\"fill\", t.ink).style(\"font-size\", \"26px\").style(\"font-weight\", \"700\")\n  .text(station.name);\n\n// Location and period\nsvg.append(\"text\").attr(\"x\", margin.left).attr(\"y\", 96)\n  .attr(\"fill\", t.inkSoft).style(\"font-size\", \"13px\")\n  .text(`${station.region}  |  ${station.elevation} m a.s.l.  |  ${station.period}`);\n\n// Annual mean temperature (right side of header)\nconst hx = width - margin.right;\nsvg.append(\"text\").attr(\"x\", hx).attr(\"y\", 70).attr(\"text-anchor\", \"end\")\n  .attr(\"fill\", TEMP_COLOR).style(\"font-size\", \"22px\").style(\"font-weight\", \"700\")\n  .text(`${station.tempMean} °C`);\n\nsvg.append(\"text\").attr(\"x\", hx).attr(\"y\", 96).attr(\"text-anchor\", \"end\")\n  .attr(\"fill\", PRCP_COLOR).style(\"font-size\", \"18px\").style(\"font-weight\", \"600\")\n  .text(`${station.precipTotal} mm/yr`);\n\n// Separator line below header\nsvg.append(\"line\")\n  .attr(\"x1\", margin.left).attr(\"x2\", width - margin.right)\n  .attr(\"y1\", margin.top - 9).attr(\"y2\", margin.top - 9)\n  .attr(\"stroke\", t.grid).attr(\"stroke-width\", 1.5);\n\n// ── Plot border ───────────────────────────────────────────────────────────────\ng.append(\"rect\").attr(\"width\", iw).attr(\"height\", ih)\n  .attr(\"fill\", \"none\").attr(\"stroke\", t.inkSoft).attr(\"stroke-width\", 1);\n\n// ── Legend ────────────────────────────────────────────────────────────────────\nconst lx = iw - 198, ly = 18;\nconst legItems = [\n  { patId: \"aridPat\",  lineColor: TEMP_COLOR, label: \"Arid  (T > P/2)\" },\n  { patId: \"humidPat\", lineColor: PRCP_COLOR, label: \"Humid  (P > 2T)\" },\n];\nlegItems.forEach(({ patId, lineColor, label }, i) => {\n  const y0 = ly + i * 30;\n  g.append(\"rect\").attr(\"x\", lx).attr(\"y\", y0).attr(\"width\", 30).attr(\"height\", 16)\n    .attr(\"fill\", `url(#${patId})`).attr(\"stroke\", lineColor).attr(\"stroke-width\", 1.5)\n    .attr(\"rx\", 2);\n  g.append(\"line\").attr(\"x1\", lx).attr(\"x2\", lx + 30)\n    .attr(\"y1\", y0 + 8).attr(\"y2\", y0 + 8)\n    .attr(\"stroke\", lineColor).attr(\"stroke-width\", 2);\n  g.append(\"text\").attr(\"x\", lx + 38).attr(\"y\", y0 + 12)\n    .attr(\"fill\", t.inkSoft).style(\"font-size\", \"13px\").text(label);\n});\n"}