{"spec_id":"area-stacked","library":"chartjs","language":"javascript","code":"// anyplot.ai\n// area-stacked: Stacked Area Chart\n// Library: chartjs 4.4.7 | JavaScript 22.23.2\n// Quality: 94/100 | Created: 2026-08-17\n\nconst t = window.ANYPLOT_TOKENS;\n\n// --- Data (in-memory, deterministic) ----------------------------------------\n// Monthly website traffic by source, Jan 2023 - Dec 2024 (24 months).\n// Series ordered largest-to-smallest so the biggest source anchors the bottom\n// of the stack (spec note: \"order series by size, largest at bottom\").\nlet seed = 42;\nconst lcg = () => {\n  seed = (seed * 1103515245 + 12345) % 2147483648;\n  return seed / 2147483648;\n};\nconst noise = (scale) => (lcg() - 0.5) * scale;\n\nconst monthNames = [\n  \"Jan\",\n  \"Feb\",\n  \"Mar\",\n  \"Apr\",\n  \"May\",\n  \"Jun\",\n  \"Jul\",\n  \"Aug\",\n  \"Sep\",\n  \"Oct\",\n  \"Nov\",\n  \"Dec\",\n];\nconst labels = [2023, 2024].flatMap((year) =>\n  monthNames.map((m) => `${m} '${String(year).slice(2)}`),\n);\n\nconst direct = [];\nconst organicSearch = [];\nconst referral = [];\nconst social = [];\nlabels.forEach((_, i) => {\n  const holidayBump = i % 12 >= 10 ? 200 : 0;\n  direct.push(Math.round(1300 + i * 8 + holidayBump + noise(60)));\n  organicSearch.push(Math.round(700 + i * 22 + noise(50)));\n  referral.push(Math.round(380 + i * 3 + noise(30)));\n  // Kept below referral's growth rate throughout the series so the largest-\n  // at-bottom stacking order never crosses over.\n  social.push(Math.round(180 + i * 6 + noise(25)));\n});\n\nconst holidayIndex = 10; // Nov '23 — first holiday-season traffic bump\n\n// Organic Search grew fastest of the four sources (steepest slope above);\n// surface that story explicitly instead of leaving it for the viewer to infer.\nconst organicGrowthPct = Math.round(\n  ((organicSearch[organicSearch.length - 1] - organicSearch[0]) /\n    organicSearch[0]) *\n    100,\n);\nconst organicCalloutIndex = labels.length - 5; // clear of the right-edge padding\n\nconst hexToRgba = (hex, alpha) => {\n  const r = parseInt(hex.slice(1, 3), 16);\n  const g = parseInt(hex.slice(3, 5), 16);\n  const b = parseInt(hex.slice(5, 7), 16);\n  return `rgba(${r}, ${g}, ${b}, ${alpha})`;\n};\n\nconst series = [\n  { label: \"Direct\", data: direct, color: t.palette[0] },\n  { label: \"Organic Search\", data: organicSearch, color: t.palette[1] },\n  { label: \"Referral\", data: referral, color: t.palette[2] },\n  { label: \"Social\", data: social, color: t.palette[3] },\n];\n\n// --- Custom plugins ------------------------------------------------------------\n// Elevated card background behind the legend, matching the Imprint\n// \"callout box\" treatment instead of a bare default legend row. A hairline\n// border on the card edge separates it from the softer drop shadow alone.\nconst elevatedLegendBg = {\n  id: \"elevatedLegendBg\",\n  beforeDraw(chart) {\n    const { legend, ctx } = chart;\n    if (!legend) return;\n    const pad = 10;\n    const x = legend.left - pad;\n    const y = legend.top - pad;\n    const w = legend.right - legend.left + pad * 2;\n    const h = legend.bottom - legend.top + pad * 2;\n    const r = 8;\n    ctx.save();\n    ctx.fillStyle = t.elevatedBg;\n    ctx.shadowColor = \"rgba(0, 0, 0, 0.12)\";\n    ctx.shadowBlur = 6;\n    ctx.shadowOffsetY = 2;\n    ctx.beginPath();\n    ctx.moveTo(x + r, y);\n    ctx.arcTo(x + w, y, x + w, y + h, r);\n    ctx.arcTo(x + w, y + h, x, y + h, r);\n    ctx.arcTo(x, y + h, x, y, r);\n    ctx.arcTo(x, y, x + w, y, r);\n    ctx.closePath();\n    ctx.fill();\n    ctx.shadowColor = \"transparent\";\n    ctx.strokeStyle = t.grid;\n    ctx.lineWidth = 1;\n    ctx.stroke();\n    ctx.restore();\n  },\n};\n\n// Factory for the dashed-leader/dot/label callout used to surface a data\n// story directly on the chart instead of leaving it for the viewer to infer.\n// Both callouts below share this drawing logic, parameterized by which top-\n// stack point they anchor to and what label they show.\nconst makeCallout = (id, pointIndex, text) => ({\n  id,\n  afterDatasetsDraw(chart) {\n    const topMeta = chart.getDatasetMeta(series.length - 1);\n    const point = topMeta.data[pointIndex];\n    if (!point) return;\n    const { x, y: yPeak } = point;\n    const yLabel = yPeak - 56;\n\n    const { ctx } = chart;\n    ctx.save();\n    ctx.strokeStyle = t.inkSoft;\n    ctx.lineWidth = 1;\n    ctx.setLineDash([3, 3]);\n    ctx.beginPath();\n    ctx.moveTo(x, yPeak - 4);\n    ctx.lineTo(x, yLabel + 14);\n    ctx.stroke();\n    ctx.setLineDash([]);\n\n    ctx.fillStyle = t.ink;\n    ctx.beginPath();\n    ctx.arc(x, yPeak, 3, 0, Math.PI * 2);\n    ctx.fill();\n\n    ctx.font = \"13px sans-serif\";\n    ctx.fillStyle = t.inkSoft;\n    ctx.textAlign = \"center\";\n    ctx.fillText(text, x, yLabel);\n    ctx.restore();\n  },\n});\n\n// Nov '23 holiday-season traffic bump on the top edge of the stack.\nconst holidayCallout = makeCallout(\n  \"holidayCallout\",\n  holidayIndex,\n  \"Holiday traffic bump\",\n);\n\n// Organic Search's two-year growth rate — the fastest-growing source in the\n// stack.\nconst organicGrowthCallout = makeCallout(\n  \"organicGrowthCallout\",\n  organicCalloutIndex,\n  `Organic Search +${organicGrowthPct}% since Jan '23`,\n);\n\n// --- Mount -------------------------------------------------------------------\nconst canvas = document.createElement(\"canvas\");\ndocument.getElementById(\"container\").appendChild(canvas);\n\n// --- Chart ---------------------------------------------------------------------\nnew Chart(canvas, {\n  type: \"line\",\n  data: {\n    labels,\n    datasets: series.map((s, i) => ({\n      label: s.label,\n      data: s.data,\n      borderColor: s.color,\n      backgroundColor: hexToRgba(s.color, 0.75),\n      borderWidth: 2,\n      pointRadius: 0,\n      tension: 0,\n      fill: i === 0 ? \"origin\" : \"-1\",\n    })),\n  },\n  plugins: [elevatedLegendBg, holidayCallout, organicGrowthCallout],\n  options: {\n    responsive: true,\n    maintainAspectRatio: false,\n    animation: false,\n    interaction: { mode: \"index\", intersect: false },\n    layout: { padding: { right: 40 } },\n    plugins: {\n      title: {\n        display: true,\n        text: \"area-stacked · javascript · chartjs · anyplot.ai\",\n        color: t.ink,\n        font: { size: 22 },\n        padding: { bottom: 20 },\n      },\n      legend: {\n        position: \"top\",\n        labels: { color: t.ink, font: { size: 16 }, boxWidth: 24, padding: 20 },\n      },\n    },\n    scales: {\n      x: {\n        ticks: {\n          color: t.inkSoft,\n          font: { size: 14 },\n          maxRotation: 0,\n          autoSkip: true,\n          maxTicksLimit: 12,\n        },\n        grid: { display: false },\n        title: {\n          display: true,\n          text: \"Month\",\n          color: t.ink,\n          font: { size: 16 },\n        },\n      },\n      y: {\n        stacked: true,\n        beginAtZero: true,\n        ticks: { color: t.inkSoft, font: { size: 14 } },\n        grid: { color: t.grid },\n        title: {\n          display: true,\n          text: \"Website Visits (thousands)\",\n          color: t.ink,\n          font: { size: 16 },\n        },\n      },\n    },\n  },\n});\n"}