{"spec_id":"line-timeseries","library":"muix","language":"javascript","code":"// anyplot.ai\n// line-timeseries: Time Series Line Plot\n// Library: muix 7.29.1 | JavaScript 22.23.2\n// Quality: 89/100 | Created: 2026-09-05\n\nimport { LineChart } from \"@mui/x-charts/LineChart\";\nimport { useXScale, useDrawingArea } from \"@mui/x-charts/hooks\";\nimport Box from \"@mui/material/Box\";\nimport Typography from \"@mui/material/Typography\";\n\nconst t = window.ANYPLOT_TOKENS;\n\n// --- Data (in-memory, deterministic LCG PRNG — no fetch, no Math.random) ----\nlet seed = 11;\nfunction rand() {\n  seed = (seed * 1103515245 + 12345) & 0x7fffffff;\n  return seed / 0x7fffffff;\n}\n\nconst DAYS = 7;\nconst HOURS = DAYS * 24; // hourly rooftop sensor readings for one week\nconst timestamps: Date[] = [];\nconst start = new Date(2024, 5, 3, 0, 0); // Mon Jun 3, 2024, 00:00\nfor (let i = 0; i < HOURS; i++) {\n  timestamps.push(new Date(start.getTime() + i * 3_600_000));\n}\n\n// Diurnal cycle (peak mid-afternoon, trough before dawn) plus a gentle\n// week-long warm-up and hour-to-hour sensor noise.\nconst temperatures: number[] = [];\nfor (let i = 0; i < HOURS; i++) {\n  const hourOfDay = i % 24;\n  const dayIndex = Math.floor(i / 24);\n  const diurnal = 8 * Math.sin(((hourOfDay - 9) / 24) * 2 * Math.PI);\n  const weekWarmup = dayIndex * 0.7;\n  const noise = (rand() - 0.5) * 2.2;\n  temperatures.push(Math.round((61 + diurnal + weekWarmup + noise) * 10) / 10);\n}\n\nconst minTemp = Math.min(...temperatures);\nconst maxTemp = Math.max(...temperatures);\nconst tempRange = maxTemp - minTemp;\nconst Y_MIN = Math.floor(minTemp - tempRange * 0.15);\nconst Y_MAX = Math.ceil(maxTemp + tempRange * 0.15);\n\n// Midnight and noon get a fine \"%H:%M\" tick — enough to anchor the diurnal\n// shape without the row collapsing into an unreadable smear once the PNG is\n// downscaled for mobile/thumbnail previews.\nconst HOUR_TICK_STEP = 12;\n\n// Day-boundary indices anchor the coarser weekday row drawn below the axis.\nconst dayStartIndices = timestamps.map((_, i) => i).filter((i) => i % 24 === 0);\n\n// Two-tier date axis: MUI X community has no built-in multi-scale date\n// formatter (the kind d3-time-format's `multiFormat` gives you), so the\n// intelligent adaptation the spec calls for — fine \"%H:%M\" ticks nested\n// under a coarser weekday/date row — is composed by hand against the shared\n// xAxis scale via useXScale/useDrawingArea, the documented ChartContainer\n// composition pattern for marks outside the community surface.\nfunction DayBoundaries() {\n  const xScale = useXScale() as any;\n  const drawingArea = useDrawingArea();\n  if (!xScale) return null;\n\n  const bottom = drawingArea.top + drawingArea.height;\n\n  return (\n    <g>\n      {dayStartIndices.map((dayStart) => {\n        const dayEnd = Math.min(dayStart + 23, HOURS - 1);\n        const xStart = xScale(timestamps[dayStart]);\n        const xEnd = xScale(timestamps[dayEnd]);\n        const label = timestamps[dayStart].toLocaleDateString(\"en-US\", {\n          weekday: \"short\",\n          month: \"numeric\",\n          day: \"numeric\",\n        });\n\n        return (\n          <g key={dayStart}>\n            {dayStart > 0 && (\n              <line\n                x1={xStart}\n                y1={drawingArea.top}\n                x2={xStart}\n                y2={bottom + 34}\n                stroke={t.grid}\n                strokeWidth={1}\n                strokeDasharray=\"3 3\"\n              />\n            )}\n            <text\n              x={(xStart + xEnd) / 2}\n              y={bottom + 58}\n              textAnchor=\"middle\"\n              fontSize={14}\n              fontWeight={600}\n              fill={t.ink}\n            >\n              {label}\n            </text>\n          </g>\n        );\n      })}\n    </g>\n  );\n}\n\nconst TITLE =\n  \"Rooftop Sensor Temperature · line-timeseries · javascript · muix · anyplot.ai\";\n\n// --- Chart (default-exported component — the harness mounts it) -----------\nexport default function Chart() {\n  const W = window.ANYPLOT_SIZE.width;\n  const H = window.ANYPLOT_SIZE.height;\n  const titleSize =\n    TITLE.length > 67 ? Math.max(14, Math.round((22 * 67) / TITLE.length)) : 22;\n\n  const TITLE_H = 60;\n  const chartH = H - TITLE_H;\n\n  return (\n    <Box\n      sx={{\n        width: W,\n        height: H,\n        bgcolor: t.pageBg,\n        display: \"flex\",\n        flexDirection: \"column\",\n        fontFamily: \"'Roboto', 'Helvetica Neue', Arial, sans-serif\",\n        boxSizing: \"border-box\",\n      }}\n    >\n      <Box\n        sx={{\n          height: TITLE_H,\n          display: \"flex\",\n          alignItems: \"center\",\n          justifyContent: \"center\",\n        }}\n      >\n        <Typography sx={{ color: t.ink, fontSize: titleSize, fontWeight: 600 }}>\n          {TITLE}\n        </Typography>\n      </Box>\n\n      <LineChart\n        width={W}\n        height={chartH}\n        skipAnimation\n        series={[\n          {\n            id: \"temperature\",\n            data: temperatures,\n            showMark: false,\n            area: true,\n            color: t.palette[0],\n            valueFormatter: (v: number | null) =>\n              v == null ? \"\" : `${v.toFixed(1)}°F`,\n          },\n        ]}\n        xAxis={[\n          {\n            data: timestamps,\n            scaleType: \"point\",\n            valueFormatter: (d: Date) =>\n              d.toLocaleTimeString(\"en-GB\", {\n                hour: \"2-digit\",\n                minute: \"2-digit\",\n              }),\n            tickInterval: (_value: Date, index: number) =>\n              index % HOUR_TICK_STEP === 0,\n            tickLabelStyle: { fontSize: 13, fill: t.inkSoft },\n          },\n        ]}\n        yAxis={[\n          {\n            label: \"Temperature (°F)\",\n            min: Y_MIN,\n            max: Y_MAX,\n            valueFormatter: (v: number) => `${Math.round(v)}°`,\n            labelStyle: { fontSize: 15, fill: t.ink },\n            tickLabelStyle: { fontSize: 13, fill: t.inkSoft },\n          },\n        ]}\n        grid={{ horizontal: true, vertical: true }}\n        slotProps={{ legend: { hidden: true } }}\n        margin={{ top: 24, right: 40, bottom: 112, left: 84 }}\n        sx={{\n          \"& .MuiLineElement-root\": { strokeWidth: 3 },\n          \"& .MuiAreaElement-root\": { fill: t.palette[0], fillOpacity: 0.12 },\n          \"& .MuiChartsAxis-line\": { stroke: t.inkSoft, strokeOpacity: 0.25 },\n          \"& .MuiChartsGrid-line\": { stroke: t.grid, strokeOpacity: 0.5 },\n        }}\n      >\n        <DayBoundaries />\n      </LineChart>\n    </Box>\n  );\n}\n"}