{"spec_id":"area-stacked-confidence","library":"muix","language":"javascript","code":"// anyplot.ai\n// area-stacked-confidence: Stacked Area Chart with Confidence Bands\n// Library: muix 7.29.1 | JavaScript 22.23.2\n// Quality: 90/100 | Created: 2026-08-26\nimport { LineChart } from \"@mui/x-charts/LineChart\";\n\nconst t = window.ANYPLOT_TOKENS;\n\n// --- Data: quarterly revenue forecast by product line, 5 years (20 quarters) ---\n// A 90% prediction interval widens with forecast horizon — the further out a\n// quarter is, the less certain the number, the classic \"fan out\" behavior of\n// a real revenue forecast. Ordered largest-revenue-first so Hardware anchors\n// the stack base.\nfunction lcg(seed) {\n  let state = seed;\n  return () => {\n    state = (state * 1103515245 + 12345) % 2147483648;\n    return state / 2147483648;\n  };\n}\nconst random = lcg(42);\n\nconst QUARTERS = 20; // Q1 2024 .. Q4 2028\nconst quarterDates = Array.from(\n  { length: QUARTERS },\n  (_, i) => new Date(2024 + Math.floor(i / 4), (i % 4) * 3, 1),\n);\nconst quarterLabel = (date) => `Q${Math.floor(date.getMonth() / 3) + 1} ${date.getFullYear()}`;\n\nconst PRODUCT_LINES = [\n  {\n    id: \"hardware\",\n    label: \"Hardware\",\n    paletteIndex: 0,\n    base: 42,\n    trend: -0.35,\n    seasonAmp: 3.0,\n    seasonPhase: 0,\n    uncertaintyBase: 0.04,\n    uncertaintyGrowth: 0.006,\n  },\n  {\n    id: \"software\",\n    label: \"Software\",\n    paletteIndex: 1,\n    base: 18,\n    trend: 1.15,\n    seasonAmp: 1.6,\n    seasonPhase: 1.4,\n    uncertaintyBase: 0.05,\n    uncertaintyGrowth: 0.009,\n  },\n  {\n    id: \"services\",\n    label: \"Services\",\n    paletteIndex: 2,\n    base: 10,\n    trend: 0.55,\n    seasonAmp: 0.9,\n    seasonPhase: 2.6,\n    uncertaintyBase: 0.045,\n    uncertaintyGrowth: 0.007,\n  },\n];\n\nconst seriesData = PRODUCT_LINES.map((line) => {\n  const center = Array.from({ length: QUARTERS }, (_, i) =>\n    Math.max(\n      1,\n      line.base +\n        line.trend * i +\n        line.seasonAmp * Math.sin((i * Math.PI) / 2 + line.seasonPhase) +\n        (random() - 0.5) * 1.6,\n    ),\n  );\n  const fraction = Array.from(\n    { length: QUARTERS },\n    (_, i) => line.uncertaintyBase + i * line.uncertaintyGrowth,\n  );\n  const lower = center.map((v, i) => v * (1 - fraction[i]));\n  const upper = center.map((v, i) => v * (1 + fraction[i]));\n  return { ...line, center, lower, upper };\n});\n\n// Running cumulative total *below* each layer — the stack offset both the\n// central area and its band must share (\"stack order consistent between\n// central values and their bands\").\nlet cumulativeBelow = new Array(QUARTERS).fill(0);\nseriesData.forEach((s) => {\n  s.baseBelow = cumulativeBelow;\n  cumulativeBelow = cumulativeBelow.map((v, i) => v + s.center[i]);\n});\n\nconst mainSeries = seriesData.map((s) => ({\n  id: s.id,\n  label: s.label,\n  data: s.center,\n  stack: \"revenue\",\n  area: true,\n  showMark: false,\n  color: t.palette[s.paletteIndex],\n  curve: \"monotoneX\",\n}));\n\n// Stacked-band trick (per series): an invisible series carries the offset\n// (baseBelow + lower bound), and a second series stacked on top of it\n// supplies only the band's own width (upper - lower). The visible area then\n// spans exactly [baseBelow + lower, baseBelow + upper] — straddling that\n// series' own boundary in the stack, in the same order as the main areas.\nconst bandSeries = seriesData.flatMap((s) => [\n  {\n    id: `${s.id}-band-base`,\n    data: s.baseBelow.map((v, i) => v + s.lower[i]),\n    stack: `band-${s.id}`,\n    showMark: false,\n    color: t.pageBg,\n  },\n  {\n    id: `${s.id}-band-width`,\n    data: s.upper.map((v, i) => v - s.lower[i]),\n    stack: `band-${s.id}`,\n    area: true,\n    showMark: false,\n    color: t.palette[s.paletteIndex],\n  },\n]);\n\n// Per-series sx overrides for the band trick: hide the invisible base line,\n// hide the band's own stroke, and set the band fill to a lighter, translucent\n// shade of that series' color.\nconst bandSx = seriesData.reduce((acc, s) => {\n  acc[`& .MuiLineElement-series-${s.id}-band-base`] = { display: \"none\" };\n  acc[`& .MuiLineElement-series-${s.id}-band-width`] = { stroke: \"none\" };\n  acc[`& .MuiAreaElement-series-${s.id}-band-width`] = {\n    fill: t.palette[s.paletteIndex],\n    fillOpacity: 0.32,\n  };\n  return acc;\n}, {});\n\nconst TITLE =\n  \"Quarterly Revenue Forecast · area-stacked-confidence · javascript · muix · anyplot.ai\";\nconst TITLE_FONT_DEFAULT = 22;\nconst titleFontSize =\n  TITLE.length > 67 ? Math.round(TITLE_FONT_DEFAULT * (67 / TITLE.length)) : TITLE_FONT_DEFAULT;\nconst TITLE_H = 42;\nconst LEGEND_H = 34;\n\nfunction Legend() {\n  return (\n    <div style={{ height: LEGEND_H, display: \"flex\", alignItems: \"center\", gap: \"20px\", flexWrap: \"wrap\" }}>\n      {seriesData.map((s) => (\n        <div key={s.id} style={{ display: \"flex\", alignItems: \"center\", gap: \"7px\" }}>\n          <span\n            style={{\n              width: \"16px\",\n              height: \"16px\",\n              backgroundColor: t.palette[s.paletteIndex],\n              display: \"inline-block\",\n            }}\n          />\n          <span style={{ fontSize: \"14px\", color: t.inkSoft }}>{s.label}</span>\n        </div>\n      ))}\n      <div style={{ display: \"flex\", alignItems: \"center\", gap: \"7px\" }}>\n        <span style={{ width: \"16px\", height: \"16px\", backgroundColor: t.ink, opacity: 0.22, display: \"inline-block\" }} />\n        <span style={{ fontSize: \"14px\", color: t.inkSoft }}>90% prediction interval (per line)</span>\n      </div>\n    </div>\n  );\n}\n\n// --- Chart (default-exported component — the harness mounts it) ------------\nexport default function Chart() {\n  const { width, height } = window.ANYPLOT_SIZE;\n  const chartHeight = height - TITLE_H - LEGEND_H;\n\n  // MUI X's built-in y-axis `label` sits at a fixed offset derived from the\n  // deprecated `tickFontSize` prop, not the actual `tickLabelStyle.fontSize`\n  // — at this canvas size that formula places the rotated title *inside* the\n  // \"$NN\" tick label text. A hand-rolled label column sidesteps that offset\n  // math entirely and guarantees no overlap.\n  const Y_LABEL_W = 34;\n  const chartWidth = width - Y_LABEL_W;\n\n  return (\n    <div style={{ width, height, display: \"flex\", flexDirection: \"column\", backgroundColor: t.pageBg }}>\n      <div style={{ paddingLeft: \"84px\" }}>\n        <div\n          style={{\n            height: `${TITLE_H}px`,\n            lineHeight: `${TITLE_H}px`,\n            fontSize: `${titleFontSize}px`,\n            fontWeight: 600,\n            color: t.ink,\n          }}\n        >\n          {TITLE}\n        </div>\n        <Legend />\n      </div>\n      <div style={{ display: \"flex\", width, height: chartHeight }}>\n        <div\n          style={{\n            width: Y_LABEL_W,\n            display: \"flex\",\n            alignItems: \"center\",\n            justifyContent: \"center\",\n          }}\n        >\n          <span\n            style={{\n              display: \"inline-block\",\n              transform: \"rotate(-90deg)\",\n              whiteSpace: \"nowrap\",\n              fontSize: \"16px\",\n              color: t.ink,\n            }}\n          >\n            Revenue ($M)\n          </span>\n        </div>\n        <LineChart\n          width={chartWidth}\n          height={chartHeight}\n          skipAnimation\n          grid={{ horizontal: true }}\n          xAxis={[\n            {\n              data: quarterDates,\n              scaleType: \"time\",\n              valueFormatter: quarterLabel,\n              tickNumber: 10,\n              label: \"Quarter\",\n              labelStyle: { fontSize: 16 },\n              tickLabelStyle: { fontSize: 13 },\n            },\n          ]}\n          yAxis={[\n            {\n              min: 0,\n              tickLabelStyle: { fontSize: 14 },\n              valueFormatter: (v: number) => `$${Math.round(v)}`,\n            },\n          ]}\n          series={[...mainSeries, ...bandSeries]}\n          margin={{ top: 20, right: 40, bottom: 70, left: 65 }}\n          slotProps={{ legend: { hidden: true } }}\n          sx={{\n            \"& .MuiAreaElement-root\": { fillOpacity: 0.85 },\n            \"& .MuiLineElement-root\": { strokeWidth: 2 },\n            \"& .MuiChartsAxis-tickLabel\": { fill: t.inkSoft },\n            \"& .MuiChartsAxis-line\": { stroke: t.inkSoft },\n            \"& .MuiChartsGrid-line\": { stroke: t.grid, strokeWidth: 1 },\n            ...bandSx,\n          }}\n        />\n      </div>\n    </div>\n  );\n}\n"}