{"spec_id":"timeline-basic","library":"muix","language":"javascript","code":"// anyplot.ai\n// timeline-basic: Event Timeline\n// Library: muix 7.29.1 | JavaScript 22.23.2\n// Quality: 91/100 | Created: 2026-09-09\nimport { ScatterChart } from \"@mui/x-charts/ScatterChart\";\nimport { ChartsReferenceLine } from \"@mui/x-charts/ChartsReferenceLine\";\nimport { ChartsText } from \"@mui/x-charts/ChartsText\";\nimport { useXScale, useYScale } 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 — real public mission dates) -----------\n// A science/exploration timeline rather than a project-management one: dates\n// double as historical record, which is one of the spec's named applications.\nconst MILESTONES = [\n  { date: new Date(2020, 6, 30), event: \"Launch from Cape Canaveral\", category: \"Launch\" },\n  { date: new Date(2021, 1, 18), event: \"Landing in Jezero Crater\", category: \"Landing\" },\n  { date: new Date(2021, 3, 19), event: \"First Powered Flight\", category: \"Exploration\" },\n  { date: new Date(2021, 8, 6), event: \"First Rock Sample Collected\", category: \"Science\" },\n  { date: new Date(2022, 0, 27), event: \"10th Sample Cached\", category: \"Science\" },\n  { date: new Date(2022, 7, 8), event: \"Reached River Delta Region\", category: \"Exploration\" },\n  { date: new Date(2023, 2, 22), event: \"Sample Depot Completed\", category: \"Science\" },\n  { date: new Date(2023, 6, 18), event: \"Crater Rim Ascent Begins\", category: \"Exploration\" },\n];\n\n// First-appearance order fixes the brand-green anchor on \"Launch\" — the\n// canonical Imprint order, not cherry-picked for aesthetics.\nconst CATEGORY_ORDER = [\"Launch\", \"Landing\", \"Exploration\", \"Science\"];\nconst categoryColor: Record<string, string> = {\n  Launch: t.palette[0],\n  Landing: t.palette[1],\n  Exploration: t.palette[2],\n  Science: t.palette[3],\n};\n\n// --- Event labels: MUI X community has no per-point label primitive for\n// scatter series — draw them against the shared xAxis/yAxis scale via\n// useXScale/useYScale, the documented composition pattern for marks outside\n// the plain chart surface. Labels alternate above/below the spine so\n// adjacent events never collide.\nfunction EventLabels({ stemLength }: { stemLength: number }) {\n  const xScale = useXScale() as any;\n  const yScale = useYScale() as any;\n  if (!xScale || !yScale) return null;\n\n  const textGap = 10;\n\n  return (\n    <g>\n      {MILESTONES.map((m, i) => {\n        const x = xScale(m.date);\n        const spineY = yScale(0);\n        const above = i % 2 === 0;\n        const stemEnd = above ? spineY - stemLength : spineY + stemLength;\n        const color = categoryColor[m.category];\n        // Center labels by default, but anchor the first/last event's text\n        // inward so it can't run off the canvas edge.\n        const textAnchor = i === 0 ? \"start\" : i === MILESTONES.length - 1 ? \"end\" : \"middle\";\n\n        return (\n          <g key={m.event}>\n            <line x1={x} y1={spineY} x2={x} y2={stemEnd} stroke={color} strokeWidth={2} />\n            <ChartsText\n              x={x}\n              y={above ? stemEnd - textGap : stemEnd + textGap}\n              text={m.event}\n              style={{\n                fontSize: 15,\n                fontWeight: 600,\n                fill: t.ink,\n                textAnchor,\n                dominantBaseline: above ? \"auto\" : \"hanging\",\n              }}\n            />\n          </g>\n        );\n      })}\n    </g>\n  );\n}\n\nconst TITLE = \"Perseverance Rover Mission · timeline-basic · 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 = TITLE.length > 67 ? Math.max(14, Math.round((22 * 67) / TITLE.length)) : 22;\n\n  // Tightened bands: the chart plot area (spine + labels) should dominate\n  // the canvas rather than leaving large blank margins above/below the\n  // label rows (prior attempt left ~38%/~31% of the height empty).\n  const TITLE_H = 56;\n  const LEGEND_H = 44;\n  const chartH = H - TITLE_H - LEGEND_H;\n  const MARGIN_TOP = 28;\n  const MARGIN_BOTTOM = 56;\n\n  // Derive the stem length from the actual plot area so the label rows sit\n  // close to the title/axis edges instead of clustering in a thin strip at\n  // the vertical center — LABEL_CLEARANCE reserves room for the label text\n  // itself plus a small breathing gap from the title/axis.\n  const plotH = chartH - MARGIN_TOP - MARGIN_BOTTOM;\n  const LABEL_CLEARANCE = 48;\n  const stemLength = Math.max(40, plotH / 2 - LABEL_CLEARANCE);\n\n  const series = CATEGORY_ORDER.map((cat) => ({\n    id: cat,\n    label: cat,\n    color: categoryColor[cat],\n    markerSize: 20,\n    data: MILESTONES.filter((m) => m.category === cat).map((m, i) => ({\n      x: m.date,\n      y: 0,\n      id: `${cat}-${i}`,\n    })),\n  }));\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 sx={{ height: TITLE_H, display: \"flex\", alignItems: \"center\", justifyContent: \"center\" }}>\n        <Typography sx={{ color: t.ink, fontSize: titleSize, fontWeight: 600 }}>{TITLE}</Typography>\n      </Box>\n\n      <ScatterChart\n        width={W}\n        height={chartH}\n        skipAnimation\n        series={series}\n        xAxis={[\n          {\n            scaleType: \"point\",\n            data: MILESTONES.map((m) => m.date),\n            valueFormatter: (d: Date) => d.toLocaleDateString(\"en-US\", { month: \"short\", year: \"numeric\" }),\n            tickLabelStyle: { fontSize: 14, fill: t.inkSoft },\n          },\n        ]}\n        yAxis={[{ min: -1, max: 1, domainLimit: \"strict\" }]}\n        leftAxis={null}\n        grid={{ horizontal: false, vertical: false }}\n        margin={{ top: MARGIN_TOP, right: 60, bottom: MARGIN_BOTTOM, left: 60 }}\n        slotProps={{ legend: { hidden: true } }}\n        sx={{\n          \"& .MuiChartsAxis-line\": { stroke: t.inkSoft, strokeOpacity: 0.25 },\n          \"& .MuiChartsAxis-tick\": { stroke: t.inkSoft, strokeOpacity: 0.25 },\n        }}\n      >\n        <ChartsReferenceLine y={0} lineStyle={{ stroke: t.grid, strokeWidth: 2 }} />\n        <EventLabels stemLength={stemLength} />\n      </ScatterChart>\n\n      <Box sx={{ height: LEGEND_H, display: \"flex\", alignItems: \"center\", justifyContent: \"center\", gap: \"24px\" }}>\n        {CATEGORY_ORDER.map((cat) => (\n          <Box key={cat} sx={{ display: \"flex\", alignItems: \"center\", gap: \"8px\" }}>\n            <svg width={14} height={14}>\n              <circle cx={7} cy={7} r={7} fill={categoryColor[cat]} />\n            </svg>\n            <Typography sx={{ color: t.inkSoft, fontSize: 13, fontWeight: 500 }}>{cat}</Typography>\n          </Box>\n        ))}\n      </Box>\n    </Box>\n  );\n}\n"}