{"spec_id":"line-cycle-seasonal","library":"chartjs","language":"javascript","code":"// anyplot.ai\n// line-cycle-seasonal: Cycle Plot (Seasonal Subseries)\n// Library: chartjs 4.4.7 | JavaScript 22.22.3\n// Quality: 92/100 | Created: 2026-06-15\n//# anyplot-orientation: landscape\n\nconst t = window.ANYPLOT_TOKENS;\n\n// --- Data -------------------------------------------------------------------\nconst MONTHS = [\"Jan\",\"Feb\",\"Mar\",\"Apr\",\"May\",\"Jun\",\"Jul\",\"Aug\",\"Sep\",\"Oct\",\"Nov\",\"Dec\"];\nconst BASE_TEMPS = [2.1, 4.3, 8.7, 14.2, 19.1, 23.8, 26.2, 25.4, 20.3, 13.9, 7.8, 2.9];\nconst N_YEARS = 20;\nconst YEAR_START = 2000;\nconst GROUP_W = N_YEARS;\nconst GAP = 3;\n\n// Park-Miller LCG — deterministic, no Date/Math.random\nlet _seed = 12345;\nconst rng = () => { _seed = (_seed * 48271) % 2147483647; return _seed / 2147483647; };\n\nconst gs = m => m * (GROUP_W + GAP);\n\n// Monthly average temperatures across 20 years with warming trend + noise\nconst temps = MONTHS.map((_, m) =>\n  Array.from({ length: N_YEARS }, (_, y) => {\n    const trend = (y / (N_YEARS - 1)) * 1.8;\n    const noise = (rng() - 0.5) * 2.4;\n    return +(BASE_TEMPS[m] + trend + noise).toFixed(2);\n  })\n);\n\nconst means = temps.map(arr => arr.reduce((a, b) => a + b, 0) / arr.length);\n\n// --- Mount ------------------------------------------------------------------\nconst canvas = document.createElement(\"canvas\");\ndocument.getElementById(\"container\").appendChild(canvas);\n\n// --- Datasets ---------------------------------------------------------------\n// One scatter+line dataset per month (subseries)\nconst subDatasets = MONTHS.map((_, m) => ({\n  label: MONTHS[m],\n  data: temps[m].map((v, y) => ({ x: gs(m) + y, y: v })),\n  showLine: true,\n  tension: 0,\n  borderColor: t.palette[0],\n  backgroundColor: t.palette[0],\n  borderWidth: 2,\n  pointRadius: 3.5,\n  pointHoverRadius: 0,\n}));\n\n// One 2-point line per month for the horizontal mean reference\nconst meanDatasets = MONTHS.map((_, m) => ({\n  label: `_mean${m}`,\n  data: [\n    { x: gs(m), y: means[m] },\n    { x: gs(m) + N_YEARS - 1, y: means[m] },\n  ],\n  showLine: true,\n  tension: 0,\n  borderColor: t.inkSoft,\n  backgroundColor: \"transparent\",\n  borderWidth: 2.5,\n  pointRadius: 0,\n}));\n\n// --- Title ------------------------------------------------------------------\nconst TITLE = \"Monthly Temperature Cycles · line-cycle-seasonal · javascript · chartjs · anyplot.ai\";\nconst titleSize = Math.max(13, Math.round(22 * 67 / TITLE.length));\n\n// --- Plugin: range bands (behind data), separators, month labels ------------\nconst groupPlugin = {\n  id: \"cycleGroups\",\n  // Draw min-max range envelope behind the subseries lines\n  beforeDatasetsDraw(chart) {\n    const { ctx } = chart;\n    const xScale = chart.scales.x;\n    const yScale = chart.scales.y;\n    ctx.save();\n    MONTHS.forEach((_, m) => {\n      const vals = temps[m];\n      const minV = Math.min(...vals);\n      const maxV = Math.max(...vals);\n      const x0 = xScale.getPixelForValue(gs(m));\n      const x1 = xScale.getPixelForValue(gs(m) + N_YEARS - 1);\n      const yTop = yScale.getPixelForValue(maxV);\n      const yBot = yScale.getPixelForValue(minV);\n      ctx.fillStyle = \"rgba(0,158,115,0.08)\";\n      ctx.fillRect(x0, yTop, x1 - x0, yBot - yTop);\n    });\n    ctx.restore();\n  },\n  afterDraw(chart) {\n    const { ctx, chartArea } = chart;\n    const xScale = chart.scales.x;\n    ctx.save();\n\n    MONTHS.forEach((month, m) => {\n      const x0 = xScale.getPixelForValue(gs(m));\n      const x1 = xScale.getPixelForValue(gs(m) + N_YEARS - 1);\n\n      // Subtle vertical separator between groups\n      if (m > 0) {\n        const prevX1 = xScale.getPixelForValue(gs(m - 1) + N_YEARS - 1);\n        const sepX = (prevX1 + x0) / 2;\n        ctx.beginPath();\n        ctx.strokeStyle = t.grid;\n        ctx.lineWidth = 1;\n        ctx.moveTo(sepX, chartArea.top);\n        ctx.lineTo(sepX, chartArea.bottom);\n        ctx.stroke();\n      }\n\n      // Month label drawn below the x-axis tick labels\n      ctx.font = \"bold 14px system-ui, sans-serif\";\n      ctx.fillStyle = t.ink;\n      ctx.textAlign = \"center\";\n      ctx.textBaseline = \"top\";\n      ctx.fillText(month, (x0 + x1) / 2, xScale.bottom + 6);\n    });\n\n    ctx.restore();\n  },\n};\n\n// --- Chart ------------------------------------------------------------------\nnew Chart(canvas, {\n  type: \"scatter\",\n  plugins: [groupPlugin],\n  data: { datasets: [...subDatasets, ...meanDatasets] },\n  options: {\n    responsive: true,\n    maintainAspectRatio: false,\n    animation: false,\n    layout: { padding: { top: 16, right: 30, bottom: 60, left: 10 } },\n    plugins: {\n      title: {\n        display: true,\n        text: TITLE,\n        color: t.ink,\n        font: { size: titleSize, weight: \"600\" },\n        padding: { top: 8, bottom: 4 },\n      },\n      subtitle: {\n        display: true,\n        text: \"Upward slopes in every group reveal consistent +1.8°C warming across all seasons (2000–2019)\",\n        color: t.inkSoft,\n        font: { size: Math.max(11, Math.round(titleSize * 0.78)), style: \"italic\" },\n        padding: { bottom: 14 },\n      },\n      legend: {\n        display: true,\n        labels: {\n          color: t.ink,\n          font: { size: 13 },\n          boxWidth: 30,\n          padding: 16,\n          generateLabels: () => [\n            {\n              text: \"Annual subseries\",\n              fillStyle: t.palette[0],\n              strokeStyle: t.palette[0],\n              fontColor: t.ink,\n              lineWidth: 2,\n              hidden: false,\n              datasetIndex: 0,\n            },\n            {\n              text: \"Monthly mean\",\n              fillStyle: \"transparent\",\n              strokeStyle: t.inkSoft,\n              fontColor: t.ink,\n              lineWidth: 2.5,\n              hidden: false,\n              datasetIndex: 12,\n            },\n          ],\n        },\n      },\n      tooltip: { enabled: false },\n    },\n    scales: {\n      x: {\n        type: \"linear\",\n        min: gs(0) - 1,\n        max: gs(11) + N_YEARS,\n        afterBuildTicks(scale) {\n          const ticks = [];\n          for (let m = 0; m < 12; m++) {\n            ticks.push({ value: gs(m) });\n          }\n          scale.ticks = ticks;\n        },\n        ticks: {\n          color: t.inkSoft,\n          font: { size: 13 },\n          maxRotation: 0,\n          callback(value) {\n            for (let m = 0; m < 12; m++) {\n              if (value === gs(m)) return `${YEAR_START}`;\n            }\n            return null;\n          },\n        },\n        grid: { display: false },\n        border: { display: false },\n      },\n      y: {\n        ticks: {\n          color: t.inkSoft,\n          font: { size: 13 },\n          callback: v => `${v.toFixed(0)}°C`,\n        },\n        grid: { color: t.grid },\n        title: {\n          display: true,\n          text: \"Avg Monthly Temperature (°C)\",\n          color: t.ink,\n          font: { size: 14 },\n        },\n        border: { display: false },\n      },\n    },\n  },\n});\n"}