{"spec_id":"area-stacked-confidence","library":"chartjs","language":"javascript","code":"// anyplot.ai\n// area-stacked-confidence: Stacked Area Chart with Confidence Bands\n// Library: chartjs 4.4.7 | JavaScript 22.23.2\n// Quality: 93/100 | Created: 2026-08-26\n\n//# anyplot-orientation: landscape\nconst t = window.ANYPLOT_TOKENS;\n\n// --- Reproducible pseudo-randomness (browser has no seeded RNG) ------------\nfunction lcg(seed) {\n  let state = seed;\n  return () => {\n    state = (state * 1664525 + 1013904223) % 4294967296;\n    return state / 4294967296;\n  };\n}\nconst rand = lcg(42);\n\nfunction 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\n// --- Data: quarterly revenue forecast by product line, with 90% prediction\n// intervals that widen further into the forecast horizon -------------------\nconst quarterCount = 20;\nconst labels = Array.from({ length: quarterCount }, (_, q) => {\n  const year = 2024 + Math.floor(q / 4);\n  const quarter = (q % 4) + 1;\n  return `Q${quarter} ${year}`;\n});\n\nconst productLines = [\n  { name: \"Hardware\", base: 38, growth: 0.028 },\n  { name: \"Software\", base: 24, growth: 0.052 },\n  { name: \"Services\", base: 14, growth: 0.038 },\n];\n\n// Central forecast values per product line, with mild multiplicative noise.\nconst centralValues = productLines.map((line) =>\n  Array.from({ length: quarterCount }, (_, q) => {\n    const trend = line.base * Math.pow(1 + line.growth, q);\n    const noise = 1 + (rand() - 0.5) * 0.06;\n    return trend * noise;\n  })\n);\n\n// Prediction-interval half-width grows with forecast horizon (more\n// uncertainty further out) and scales with the series' own magnitude.\nconst bandHalfWidths = centralValues.map((series) =>\n  series.map((value, q) => value * (0.05 + 0.009 * q))\n);\n\n// Stack the central values, then carry the same cumulative base into each\n// series' band so bounds stay consistent with the stack order (Notes).\nconst cumulativeBase = productLines.map(() => new Array(quarterCount).fill(0));\nfor (let q = 0; q < quarterCount; q += 1) {\n  let running = 0;\n  for (let i = 0; i < productLines.length; i += 1) {\n    cumulativeBase[i][q] = running;\n    running += centralValues[i][q];\n  }\n}\n\nconst cumulativeCentral = productLines.map((_, i) =>\n  centralValues[i].map((value, q) => cumulativeBase[i][q] + value)\n);\nconst cumulativeLower = productLines.map((_, i) =>\n  centralValues[i].map((value, q) => cumulativeBase[i][q] + value - bandHalfWidths[i][q])\n);\nconst cumulativeUpper = productLines.map((_, i) =>\n  centralValues[i].map((value, q) => cumulativeBase[i][q] + value + bandHalfWidths[i][q])\n);\n\n// --- Mount -------------------------------------------------------------\nconst canvas = document.createElement(\"canvas\");\ndocument.getElementById(\"container\").appendChild(canvas);\n\n// A vertical gradient (per spec: \"gradient fills from lower to upper bound\")\n// so each band tapers instead of reading as one flat alpha.\nfunction bandGradient(hex) {\n  return (ctx) => {\n    const { chartArea, ctx: canvasCtx } = ctx.chart;\n    if (!chartArea) return hexToRgba(hex, 0.3);\n    const gradient = canvasCtx.createLinearGradient(0, chartArea.top, 0, chartArea.bottom);\n    gradient.addColorStop(0, hexToRgba(hex, 0.15));\n    gradient.addColorStop(1, hexToRgba(hex, 0.4));\n    return gradient;\n  };\n}\n\n// --- Datasets: each series is a solid \"confident\" slice from the previous\n// series' band top up to this series' own lower bound, topped by a translucent\n// band from lower to upper bound. Because the solid slice stops exactly where\n// the band starts (and the next series' solid slice starts exactly where this\n// band ends), the bands never bleed into a neighboring series' stacked region.\n// The central value is kept as a plain border line on top for the visible\n// stack boundary. ------------------------------------------------------------\nconst datasets = [];\nlet prevBandTop = \"origin\";\n\nproductLines.forEach((line, i) => {\n  const solidIndex = datasets.length;\n  datasets.push({\n    label: line.name,\n    data: cumulativeLower[i],\n    borderWidth: 0,\n    pointRadius: 0,\n    backgroundColor: hexToRgba(t.palette[i], 0.85),\n    fill: prevBandTop,\n    isBand: false,\n  });\n\n  datasets.push({\n    label: `${line.name} central`,\n    data: cumulativeCentral[i],\n    borderColor: t.palette[i],\n    borderWidth: 2.5,\n    pointRadius: 0,\n    tension: 0,\n    fill: false,\n    isBand: true,\n  });\n\n  const bandTopIndex = datasets.length;\n  datasets.push({\n    label: `${line.name} interval`,\n    data: cumulativeUpper[i],\n    borderWidth: 0,\n    pointRadius: 0,\n    backgroundColor: bandGradient(t.palette[i]),\n    fill: solidIndex,\n    isBand: true,\n  });\n\n  prevBandTop = bandTopIndex;\n});\n\n// Legend-only entry so the bands' meaning doesn't rely solely on the subtitle.\ndatasets.push({\n  label: \"90% prediction interval\",\n  data: [],\n  borderWidth: 0,\n  pointRadius: 0,\n  backgroundColor: hexToRgba(t.inkSoft, 0.35),\n  fill: false,\n  isBand: false,\n});\n\n// --- Chart -------------------------------------------------------------\nnew Chart(canvas, {\n  type: \"line\",\n  data: {\n    labels,\n    datasets,\n  },\n  options: {\n    responsive: true,\n    maintainAspectRatio: false,\n    animation: false,\n    interaction: { intersect: false },\n    plugins: {\n      title: {\n        display: true,\n        text: \"area-stacked-confidence · javascript · chartjs · anyplot.ai\",\n        color: t.ink,\n        font: { size: 22, weight: \"500\" },\n        padding: { bottom: 6 },\n      },\n      subtitle: {\n        display: true,\n        text: \"Shaded bands show the 90% prediction interval for each product line\",\n        color: t.inkSoft,\n        font: { size: 14 },\n        padding: { bottom: 16 },\n      },\n      legend: {\n        position: \"top\",\n        align: \"end\",\n        labels: {\n          color: t.ink,\n          font: { size: 16 },\n          filter: (item, data) => !data.datasets[item.datasetIndex].isBand,\n        },\n      },\n      tooltip: { enabled: false },\n    },\n    scales: {\n      x: {\n        ticks: { color: t.inkSoft, font: { size: 14 }, maxRotation: 0, autoSkip: true, maxTicksLimit: 10 },\n        grid: { display: false },\n        title: { display: true, text: \"Fiscal Quarter\", color: t.ink, font: { size: 16 } },\n      },\n      y: {\n        beginAtZero: true,\n        ticks: { color: t.inkSoft, font: { size: 14 } },\n        grid: { color: t.grid },\n        title: { display: true, text: \"Cumulative Revenue ($M)\", color: t.ink, font: { size: 16 } },\n      },\n    },\n  },\n});\n"}