{"spec_id":"line-timeseries-rolling","library":"muix","language":"javascript","code":"// anyplot.ai\n// line-timeseries-rolling: Time Series with Rolling Average Overlay\n// Library: muix 7.29.1 | JavaScript 22.23.2\n// Quality: 94/100 | Created: 2026-09-05\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 LCG PRNG — no fetch, no Math.random) ----\nlet seed = 42;\nfunction rand() {\n  seed = (seed * 1103515245 + 12345) & 0x7fffffff;\n  return seed / 0x7fffffff;\n}\n\nconst NUM_DAYS = 120;\nconst WINDOW = 7;\nconst START_DATE = new Date(2026, 0, 1);\n\nconst dates = Array.from({ length: NUM_DAYS }, (_, day) => {\n  const d = new Date(START_DATE);\n  d.setDate(d.getDate() + day);\n  return d;\n});\n\n// Daily unique visitors to a blog: weekday/weekend seasonality, a slow\n// baseline climb, a two-week traffic surge around a viral post, plus\n// day-to-day noise — exactly the volatility a rolling average smooths.\nconst SURGE_START_DAY = 58;\nconst SURGE_END_DAY = 74;\nconst rawVisitors = dates.map((date, day) => {\n  const weekday = date.getDay();\n  const weekendDip = weekday === 0 || weekday === 6 ? 0.62 : 1;\n  const trend = 1 + day * 0.004;\n  const surge =\n    day >= SURGE_START_DAY && day <= SURGE_END_DAY\n      ? 1 + 0.5 * Math.sin(((day - SURGE_START_DAY) / (SURGE_END_DAY - SURGE_START_DAY)) * Math.PI)\n      : 1;\n  const noise = 1 + (rand() - 0.5) * 0.22;\n  const baseline = 2200;\n  return Math.max(300, Math.round(baseline * weekendDip * trend * surge * noise));\n});\n\n// Trailing WINDOW-day rolling average — null until a full window of raw data\n// is available, so the smoothed line starts WINDOW-1 days after the raw one.\nconst rollingAvg = rawVisitors.map((_, i) => {\n  if (i < WINDOW - 1) return null;\n  let sum = 0;\n  for (let k = i - WINDOW + 1; k <= i; k += 1) sum += rawVisitors[k];\n  return Math.round(sum / WINDOW);\n});\n\nconst TITLE = \"Daily Unique Visitors · line-timeseries-rolling · javascript · muix · anyplot.ai\";\n\n// --- Chart (default-exported component — the harness mounts it) -----------\nexport default function Chart() {\n  const size = window.ANYPLOT_SIZE;\n  const titleSize = TITLE.length > 67 ? Math.max(14, Math.round((22 * 67) / TITLE.length)) : 22;\n  const padding = { top: 28, right: 40, bottom: 24, left: 40 };\n  const titleBlockHeight = 56;\n  // MUI X's y-axis `label` offsets itself from a hardcoded tickFontSize guess\n  // rather than the tick labels' real measured width, so a 4-digit visitor\n  // count collides with it. A hand-rotated label in its own flex column\n  // sidesteps that and gives predictable, collision-free spacing.\n  const yLabelWidth = 34;\n  const chartWidth = size.width - padding.left - padding.right - yLabelWidth;\n  const chartHeight = size.height - padding.top - padding.bottom - titleBlockHeight;\n\n  return (\n    <Box\n      sx={{\n        width: size.width,\n        height: size.height,\n        boxSizing: \"border-box\",\n        padding: `${padding.top}px ${padding.right}px ${padding.bottom}px ${padding.left}px`,\n        display: \"flex\",\n        flexDirection: \"column\",\n      }}\n    >\n      <Typography sx={{ fontSize: titleSize, fontWeight: 600, color: \"text.primary\", mb: \"20px\", lineHeight: 1 }}>\n        {TITLE}\n      </Typography>\n      <Box sx={{ display: \"flex\", flexDirection: \"row\", height: chartHeight }}>\n        <Box sx={{ width: yLabelWidth, display: \"flex\", alignItems: \"center\", justifyContent: \"center\" }}>\n          <Typography sx={{ fontSize: 16, color: \"text.secondary\", whiteSpace: \"nowrap\", transform: \"rotate(-90deg)\" }}>\n            Unique Visitors\n          </Typography>\n        </Box>\n        <LineChart\n          width={chartWidth}\n          height={chartHeight}\n          skipAnimation\n          series={[\n            {\n              id: \"raw\",\n              label: \"Raw Data\",\n              data: rawVisitors,\n              showMark: false,\n              color: t.palette[0],\n              valueFormatter: (v) => (v == null ? \"\" : `${v.toLocaleString(\"en-US\")} visitors`),\n            },\n            {\n              id: \"rolling\",\n              label: `Rolling Average (${WINDOW}-Day)`,\n              data: rollingAvg,\n              // Only mark the latest point — a subtle callout of the current\n              // trend value without cluttering the smoothed line.\n              showMark: ({ index }) => index === rollingAvg.length - 1,\n              color: t.palette[1],\n              valueFormatter: (v) => (v == null ? \"\" : `${v.toLocaleString(\"en-US\")} visitors`),\n            },\n          ]}\n          xAxis={[\n            {\n              data: dates,\n              scaleType: \"time\",\n              label: \"Date\",\n              valueFormatter: (date) => date.toLocaleDateString(\"en-US\", { month: \"short\", day: \"numeric\" }),\n              tickLabelStyle: { fontSize: 14 },\n              labelStyle: { fontSize: 16 },\n            },\n          ]}\n          yAxis={[\n            {\n              valueFormatter: (value) => value.toLocaleString(\"en-US\"),\n              tickLabelStyle: { fontSize: 14 },\n            },\n          ]}\n          grid={{ vertical: true, horizontal: true }}\n          slotProps={{\n            legend: {\n              direction: \"row\",\n              labelStyle: { fontSize: 14 },\n              itemMarkWidth: 18,\n              itemMarkHeight: 10,\n              markGap: 8,\n            },\n          }}\n          sx={{\n            \"& .MuiLineElement-series-raw\": { strokeWidth: 1.5, strokeOpacity: 0.5 },\n            \"& .MuiLineElement-series-rolling\": { strokeWidth: 3.5 },\n            \"& .MuiMarkElement-series-rolling\": { r: 6, strokeWidth: 2.5 },\n            \"& .MuiChartsGrid-line\": { strokeDasharray: \"4 3\" },\n          }}\n        >\n          {/* Call out the viral-post surge — the chart's clearest story beat —\n              with a bracketed reference-line pair in the amber \"caution/notable\n              event\" anchor, distinct from both data-series colors. */}\n          <ChartsReferenceLine\n            x={dates[SURGE_START_DAY]}\n            label=\"Viral post surge\"\n            labelAlign=\"start\"\n            lineStyle={{ stroke: t.amber, strokeDasharray: \"5 4\", strokeWidth: 1.5 }}\n            labelStyle={{ fontSize: 13, fontWeight: 600, fill: t.amber }}\n            spacing={{ x: 6, y: 6 }}\n          />\n          <ChartsReferenceLine\n            x={dates[SURGE_END_DAY]}\n            lineStyle={{ stroke: t.amber, strokeDasharray: \"5 4\", strokeWidth: 1.5 }}\n          />\n        </LineChart>\n      </Box>\n    </Box>\n  );\n}\n"}