{"spec_id":"timeseries-decomposition","library":"muix","language":"javascript","code":"// anyplot.ai\n// timeseries-decomposition: Time Series Decomposition Plot\n// Library: muix 7.29.1 | JavaScript 22.23.2\n// Quality: 89/100 | Created: 2026-09-09\nimport { LineChart } from \"@mui/x-charts/LineChart\";\nimport { ChartsReferenceLine } from \"@mui/x-charts/ChartsReferenceLine\";\nimport { Box, Typography } from \"@mui/material\";\n\nconst t = window.ANYPLOT_TOKENS;\n\n// --- Data (in-memory, deterministic) ----------------------------------------\n// Monthly retail sales revenue ($ thousands), 10 years — additive decomposition:\n// original = trend + seasonal + residual\nconst MONTHS = 120;\n\n// Fixed-seed LCG — the browser has no seeded Math.random\nlet seed = 42;\nfunction nextRandom() {\n  seed = (seed * 1103515245 + 12345) % 2147483648;\n  return seed / 2147483648;\n}\n\n// Jan..Dec seasonal index: winter dip, Nov/Dec holiday spike, sums to 0\nconst SEASONAL_INDEX = [-22, -18, -10, -4, 2, 6, 8, 4, -2, -6, 14, 28];\n\nconst dates = [];\nconst trendSeries = [];\nconst seasonalSeries = [];\nconst residualSeries = [];\nconst originalSeries = [];\n\nfor (let i = 0; i < MONTHS; i += 1) {\n  const trendValue = 240 + 1.9 * i + 0.006 * i * i;\n  const seasonalValue = SEASONAL_INDEX[i % 12];\n  const residualValue = (nextRandom() - 0.5) * 24;\n\n  dates.push(new Date(2015, i, 1));\n  trendSeries.push(trendValue);\n  seasonalSeries.push(seasonalValue);\n  residualSeries.push(residualValue);\n  originalSeries.push(trendValue + seasonalValue + residualValue);\n}\n\nconst formatMonth = (date) => date.toLocaleDateString(\"en-US\", { month: \"short\", year: \"numeric\" });\n\n// Canonical Imprint 1->N order, top to bottom: Original, Trend, Seasonal,\n// Residual each take the next palette slot. This also keeps every series a\n// fixed hex that stays identical across themes (no theme-adaptive tokens on\n// data colors).\nconst PANELS = [\n  { label: \"Original\", data: originalSeries, color: t.palette[0], zeroLine: false },\n  { label: \"Trend\", data: trendSeries, color: t.palette[1], zeroLine: false },\n  { label: \"Seasonal\", data: seasonalSeries, color: t.palette[2], zeroLine: true },\n  { label: \"Residual\", data: residualSeries, color: t.palette[3], zeroLine: true },\n];\n\nconst TITLE = \"timeseries-decomposition · javascript · muix · anyplot.ai\";\nconst TITLE_HEIGHT = 46;\n\n// --- Chart (default-exported component — the harness mounts it) ------------\nexport default function Chart() {\n  const { width, height } = window.ANYPLOT_SIZE;\n  const panelHeight = (height - TITLE_HEIGHT) / PANELS.length;\n\n  return (\n    <Box sx={{ width, height, display: \"flex\", flexDirection: \"column\" }}>\n      <Typography\n        sx={{\n          height: TITLE_HEIGHT,\n          lineHeight: `${TITLE_HEIGHT}px`,\n          pl: 1,\n          color: t.ink,\n          fontSize: 22,\n          fontWeight: 500,\n        }}\n      >\n        {TITLE}\n      </Typography>\n      {PANELS.map((panel, index) => {\n        const isLast = index === PANELS.length - 1;\n        return (\n          <Box key={panel.label} sx={{ position: \"relative\" }}>\n            <Typography\n              sx={{\n                position: \"absolute\",\n                top: 6,\n                left: 96,\n                fontSize: 15,\n                fontWeight: 600,\n                color: t.ink,\n                pointerEvents: \"none\",\n              }}\n            >\n              {panel.label}\n            </Typography>\n            {/* Native yAxis.label collides with tick numbers in these short\n                panels regardless of margin, so the units label is drawn by\n                hand instead, spanning the plot area (excludes the x-axis\n                band at the bottom). */}\n            <Box\n              sx={{\n                position: \"absolute\",\n                left: 2,\n                top: 0,\n                bottom: isLast ? 56 : 8,\n                width: 16,\n                display: \"flex\",\n                alignItems: \"center\",\n                justifyContent: \"center\",\n                pointerEvents: \"none\",\n              }}\n            >\n              <Typography\n                sx={{\n                  writingMode: \"vertical-rl\",\n                  transform: \"rotate(180deg)\",\n                  fontSize: 13,\n                  color: t.inkSoft,\n                  whiteSpace: \"nowrap\",\n                }}\n              >\n                $k\n              </Typography>\n            </Box>\n            <LineChart\n              width={width}\n              height={panelHeight}\n              skipAnimation\n              series={[\n                {\n                  data: panel.data,\n                  label: panel.label,\n                  color: panel.color,\n                  showMark: false,\n                  curve: \"monotoneX\",\n                },\n              ]}\n              xAxis={[\n                {\n                  data: dates,\n                  scaleType: \"time\",\n                  valueFormatter: isLast ? formatMonth : () => \"\",\n                },\n              ]}\n              margin={{ left: 88, right: 30, top: 14, bottom: isLast ? 56 : 8 }}\n              grid={{ horizontal: true }}\n              slotProps={{ legend: { hidden: true } }}\n            >\n              {panel.zeroLine ? (\n                <ChartsReferenceLine\n                  y={0}\n                  lineStyle={{ stroke: t.inkSoft, strokeDasharray: \"4 4\", strokeOpacity: 0.6 }}\n                />\n              ) : null}\n            </LineChart>\n          </Box>\n        );\n      })}\n    </Box>\n  );\n}\n"}