{"spec_id":"line-cycle-seasonal","library":"d3","language":"javascript","code":"// anyplot.ai\n// line-cycle-seasonal: Cycle Plot (Seasonal Subseries)\n// Library: d3 7.9.0 | JavaScript 22.22.3\n// Quality: 89/100 | Created: 2026-06-15\n//# anyplot-orientation: landscape\n// anyplot.ai\n// line-cycle-seasonal: Cycle Plot (Seasonal Subseries)\n// Library: d3 7.9.0 | JavaScript 22\n// Quality: pending | Created: 2026-06-15\n\nconst t = window.ANYPLOT_TOKENS;\nconst { width, height } = window.ANYPLOT_SIZE;\n\nconst margin = { top: 90, right: 55, bottom: 85, left: 90 };\nconst iw = width - margin.left - margin.right;\nconst ih = height - margin.top - margin.bottom;\n\n// --- Data: monthly temperature anomaly (°C), 2004–2023 ----------------------\nconst MONTHS = [\"Jan\",\"Feb\",\"Mar\",\"Apr\",\"May\",\"Jun\",\"Jul\",\"Aug\",\"Sep\",\"Oct\",\"Nov\",\"Dec\"];\nconst N_YEARS = 20;\nconst START_YEAR = 2004;\n\nlet _seed = 42;\nconst rng = () => { _seed = (_seed * 1664525 + 1013904223) & 0xffffffff; return (_seed >>> 0) / 0xffffffff; };\n\n// Seasonal baseline anomaly (°C) and total warming over the 20-year window per month\nconst BASE = [-0.08, 0.02, 0.12, 0.18, 0.15, 0.24, 0.30, 0.28, 0.20, 0.12, 0.04, -0.02];\nconst WARM = [ 0.55, 0.45, 0.42, 0.35, 0.30, 0.22, 0.20, 0.22, 0.35, 0.42, 0.52, 0.62];\n\nconst groups = MONTHS.map((month, mi) => {\n  const pts = Array.from({ length: N_YEARS }, (_, yi) => ({\n    yi,\n    value: BASE[mi] + WARM[mi] * (yi / (N_YEARS - 1)) + (rng() - 0.5) * 0.28,\n  }));\n  return { month, mi, pts, mean: d3.mean(pts, d => d.value) };\n});\n\n// --- SVG mount --------------------------------------------------------------\nconst svg = d3.select(\"#container\").append(\"svg\")\n  .attr(\"width\", width).attr(\"height\", height);\nconst g = svg.append(\"g\").attr(\"transform\", `translate(${margin.left},${margin.top})`);\n\n// --- Scales -----------------------------------------------------------------\nconst xBand = d3.scaleBand()\n  .domain(MONTHS).range([0, iw])\n  .paddingInner(0.12).paddingOuter(0.02);\n\nconst xWithin = d3.scaleLinear()\n  .domain([0, N_YEARS - 1]).range([0, xBand.bandwidth()]);\n\nconst allVals = groups.flatMap(gr => gr.pts.map(d => d.value));\nconst [yMin, yMax] = d3.extent(allVals);\nconst yPad = (yMax - yMin) * 0.12;\nconst y = d3.scaleLinear()\n  .domain([yMin - yPad, yMax + yPad]).nice()\n  .range([ih, 0]);\n\n// --- Y-axis with gridlines --------------------------------------------------\nconst yAx = g.append(\"g\").call(\n  d3.axisLeft(y).ticks(6).tickSize(-iw)\n    .tickFormat(d => (d > 0 ? \"+\" : \"\") + d.toFixed(1) + \"°C\")\n);\nyAx.select(\".domain\").remove();\nyAx.selectAll(\".tick line\").attr(\"stroke\", t.grid).attr(\"stroke-dasharray\", \"4,3\");\nyAx.selectAll(\".tick text\")\n  .attr(\"fill\", t.inkSoft).style(\"font-size\", \"13px\").attr(\"dx\", \"-4\");\n\n// Baseline zero reference\nconst y0 = y(0);\nif (y0 >= 0 && y0 <= ih) {\n  g.append(\"line\")\n    .attr(\"x1\", 0).attr(\"x2\", iw)\n    .attr(\"y1\", y0).attr(\"y2\", y0)\n    .attr(\"stroke\", t.inkSoft).attr(\"stroke-width\", 1)\n    .attr(\"stroke-dasharray\", \"5,4\").attr(\"opacity\", 0.45);\n}\n\n// --- Groups -----------------------------------------------------------------\nconst LINE_COLOR = t.palette[0];  // Imprint green — first categorical series\nconst MEAN_COLOR = t.palette[1];  // Imprint lavender — seasonal mean reference lines\n\nconst lineGen = d3.line()\n  .x(d => xWithin(d.yi)).y(d => y(d.value));\n\ngroups.forEach(gr => {\n  const gx = xBand(gr.month);\n  const gw = xBand.bandwidth();\n  const gap = xBand.step() - gw;\n  const gg = g.append(\"g\").attr(\"transform\", `translate(${gx},0)`);\n\n  // Subtle separator between groups\n  if (gr.mi > 0) {\n    g.append(\"line\")\n      .attr(\"x1\", gx - gap / 2).attr(\"x2\", gx - gap / 2)\n      .attr(\"y1\", 0).attr(\"y2\", ih)\n      .attr(\"stroke\", t.grid).attr(\"opacity\", 0.7);\n  }\n\n  // Subseries line\n  gg.append(\"path\")\n    .datum(gr.pts).attr(\"d\", lineGen)\n    .attr(\"fill\", \"none\").attr(\"stroke\", LINE_COLOR)\n    .attr(\"stroke-width\", 2).attr(\"opacity\", 0.82);\n\n  // Data dots\n  gg.selectAll(\"circle\").data(gr.pts).join(\"circle\")\n    .attr(\"cx\", d => xWithin(d.yi)).attr(\"cy\", d => y(d.value))\n    .attr(\"r\", 3).attr(\"fill\", LINE_COLOR).attr(\"opacity\", 0.72);\n\n  // Seasonal mean line\n  gg.append(\"line\")\n    .attr(\"x1\", 0).attr(\"x2\", gw)\n    .attr(\"y1\", y(gr.mean)).attr(\"y2\", y(gr.mean))\n    .attr(\"stroke\", MEAN_COLOR).attr(\"stroke-width\", 2.5);\n\n  // Month label\n  gg.append(\"text\")\n    .attr(\"x\", gw / 2).attr(\"y\", ih + 30)\n    .attr(\"text-anchor\", \"middle\")\n    .attr(\"fill\", t.inkSoft).style(\"font-size\", \"13px\")\n    .text(gr.month);\n});\n\n// Year range annotations\ng.append(\"text\")\n  .attr(\"x\", 0).attr(\"y\", ih + 56)\n  .attr(\"text-anchor\", \"start\")\n  .attr(\"fill\", t.inkSoft).style(\"font-size\", \"11px\").style(\"font-style\", \"italic\")\n  .text(START_YEAR);\ng.append(\"text\")\n  .attr(\"x\", iw).attr(\"y\", ih + 56)\n  .attr(\"text-anchor\", \"end\")\n  .attr(\"fill\", t.inkSoft).style(\"font-size\", \"11px\").style(\"font-style\", \"italic\")\n  .text(START_YEAR + N_YEARS - 1);\n\n// --- Legend -----------------------------------------------------------------\nconst lgX = iw - 308;\n[\n  { label: \"Annual value\", color: LINE_COLOR, sw: 2 },\n  { label: \"Seasonal mean\", color: MEAN_COLOR, sw: 2.5 },\n].forEach((ld, i) => {\n  const lx = lgX + i * 158;\n  g.append(\"line\")\n    .attr(\"x1\", lx).attr(\"x2\", lx + 22)\n    .attr(\"y1\", -14).attr(\"y2\", -14)\n    .attr(\"stroke\", ld.color).attr(\"stroke-width\", ld.sw);\n  g.append(\"text\")\n    .attr(\"x\", lx + 28).attr(\"y\", -9)\n    .attr(\"fill\", t.inkSoft).style(\"font-size\", \"12px\")\n    .text(ld.label);\n});\n\n// --- Y-axis label -----------------------------------------------------------\nsvg.append(\"text\")\n  .attr(\"transform\", `translate(20,${margin.top + ih / 2}) rotate(-90)`)\n  .attr(\"text-anchor\", \"middle\")\n  .attr(\"fill\", t.inkSoft).style(\"font-size\", \"13px\")\n  .text(\"Temperature Anomaly (°C)\");\n\n// --- Title ------------------------------------------------------------------\nconst TITLE = \"Temperature Anomaly by Month · line-cycle-seasonal · javascript · d3 · anyplot.ai\";\nconst titlePx = Math.max(15, Math.round(22 * 67 / TITLE.length));\nsvg.append(\"text\")\n  .attr(\"x\", width / 2).attr(\"y\", 52)\n  .attr(\"text-anchor\", \"middle\")\n  .attr(\"fill\", t.ink).style(\"font-size\", titlePx + \"px\").style(\"font-weight\", \"600\")\n  .text(TITLE);\n"}