{"spec_id":"lightcurve-transit","library":"muix","language":"javascript","code":"// anyplot.ai\n// lightcurve-transit: Astronomical Light Curve\n// Library: muix 7.29.1 | JavaScript 22.22.3\n// Quality: 92/100 | Created: 2026-06-20\n//# anyplot-orientation: landscape\n// anyplot.ai\n// lightcurve-transit: Astronomical Light Curve\n// Library: MUI X Charts | React | Node 22\n// License: @mui/x-charts — MIT (community). Pro/Premium are out of scope.\n// Quality: pending | Created: 2026-06-20\n\nimport { ChartContainer } from \"@mui/x-charts/ChartContainer\";\nimport { LinePlot } from \"@mui/x-charts/LineChart\";\nimport { ChartsXAxis } from \"@mui/x-charts/ChartsXAxis\";\nimport { ChartsYAxis } from \"@mui/x-charts/ChartsYAxis\";\nimport { ChartsReferenceLine } from \"@mui/x-charts/ChartsReferenceLine\";\nimport { useDrawingArea } from \"@mui/x-charts/hooks\";\nimport Box from \"@mui/material/Box\";\nimport Typography from \"@mui/material/Typography\";\n\nconst t = window.ANYPLOT_TOKENS;\nconst FONT = \"system-ui, -apple-system, sans-serif\";\n\n// Deterministic LCG — no seeded Math.random in the browser\nfunction makeLCG(seed) {\n  let s = seed >>> 0;\n  return () => {\n    s = (Math.imul(1664525, s) + 1013904223) >>> 0;\n    return s / 0x100000000;\n  };\n}\n\n// Smooth limb-darkened transit model using smoothstep ingress/egress\nconst N_PTS = 280;\nconst TRANSIT_CENTER = 0.5;     // transit centered at mid-phase\nconst TRANSIT_HALF_DUR = 0.05;  // full duration = 0.10 phase units (0.45–0.55)\nconst TRANSIT_DEPTH = 0.01;     // 1% flux dip relative to out-of-transit baseline\n\nfunction smoothstep(x) { return x * x * (3 - 2 * x); }\n\nfunction computeModel(ph) {\n  const dt = Math.abs(ph - TRANSIT_CENTER);\n  if (dt >= TRANSIT_HALF_DUR) return 1.0;\n  return 1 - TRANSIT_DEPTH * smoothstep(1 - dt / TRANSIT_HALF_DUR);\n}\n\n// Simulated TESS-like photometry: 280 phase-folded observations\nconst rng = makeLCG(42);\nconst phases = Array.from({ length: N_PTS }, (_, i) => i / (N_PTS - 1));\nconst modelFlux = phases.map(computeModel);\nconst NOISE_SIGMA = 0.0015;\nconst observedFlux = phases.map((_, i) => modelFlux[i] + (rng() * 2 - 1) * NOISE_SIGMA * 2.5);\nconst fluxErrors = phases.map(() => NOISE_SIGMA * (1 + rng() * 0.35));\n\n// Explicit axis bounds — must match the coordinate mapping inside DataLayer\nconst FLUX_MIN = 0.983;\nconst FLUX_MAX = 1.007;\n\n// Renders observed data points: vertical error bars + measurement dots\n// Uses useDrawingArea to map data coordinates to SVG pixel positions.\n// FLUX_MIN/FLUX_MAX must match the yAxis min/max above for correct alignment.\nfunction DataLayer() {\n  const { left, top, width, height } = useDrawingArea();\n  const toX = (ph) => left + ph * width;\n  const toY = (fl) => top + (FLUX_MAX - fl) / (FLUX_MAX - FLUX_MIN) * height;\n  const toH = (e) => e / (FLUX_MAX - FLUX_MIN) * height;\n  const C = t.palette[0];  // Imprint brand green — first categorical series\n\n  return (\n    <g>\n      {/* Error bars (drawn behind dots) */}\n      {phases.map((ph, i) => {\n        const x = toX(ph);\n        const y = toY(observedFlux[i]);\n        const ey = toH(fluxErrors[i]);\n        return (\n          <line\n            key={i}\n            x1={x} y1={y - ey} x2={x} y2={y + ey}\n            stroke={C} strokeWidth={1.2} opacity={0.52}\n          />\n        );\n      })}\n      {/* Data dots */}\n      {phases.map((ph, i) => (\n        <circle\n          key={`d${i}`}\n          cx={toX(ph)} cy={toY(observedFlux[i])} r={3.5}\n          fill={C} opacity={0.82}\n        />\n      ))}\n    </g>\n  );\n}\n\n// Inline legend positioned in the right margin\nfunction LegendPanel() {\n  const { left, top, width } = useDrawingArea();\n  const lx = left + width + 20;\n  const ly = top + 60;\n\n  return (\n    <g fontFamily={FONT} fontSize={15} fill={t.inkSoft}>\n      <circle cx={lx + 7} cy={ly} r={5} fill={t.palette[0]} opacity={0.85} />\n      <text x={lx + 20} y={ly + 5}>Observed Flux</text>\n      <line\n        x1={lx} y1={ly + 36} x2={lx + 20} y2={ly + 36}\n        stroke={t.palette[1]} strokeWidth={3}\n      />\n      <text x={lx + 26} y={ly + 41}>Transit Model</text>\n    </g>\n  );\n}\n\nexport default function Chart() {\n  const { width, height } = window.ANYPLOT_SIZE;\n  const titleH = 50;\n\n  return (\n    <Box\n      sx={{\n        width,\n        height,\n        bgcolor: t.pageBg,\n        display: \"flex\",\n        flexDirection: \"column\",\n        pt: \"14px\",\n      }}\n    >\n      <Typography\n        sx={{\n          color: t.ink,\n          fontSize: 21,\n          fontWeight: 500,\n          textAlign: \"center\",\n          lineHeight: 1.2,\n          mb: \"4px\",\n          fontFamily: \"inherit\",\n        }}\n      >\n        Exoplanet Transit · lightcurve-transit · javascript · muix · anyplot.ai\n      </Typography>\n\n      <ChartContainer\n        width={width}\n        height={height - titleH}\n        margin={{ left: 138, right: 172, top: 28, bottom: 72 }}\n        series={[\n          {\n            type: \"line\",\n            id: \"model\",\n            data: modelFlux,\n            label: \"Transit Model\",\n            color: t.palette[1],\n            showMark: false,\n            curve: \"natural\",\n          },\n        ]}\n        xAxis={[{ data: phases, scaleType: \"linear\", min: 0, max: 1 }]}\n        yAxis={[{ min: FLUX_MIN, max: FLUX_MAX, valueFormatter: (v) => v.toFixed(3) }]}\n      >\n        {/* Dashed reference at out-of-transit baseline (flux = 1.0) */}\n        <ChartsReferenceLine\n          y={1.0}\n          lineStyle={{ stroke: t.inkSoft, strokeDasharray: \"5 4\", strokeWidth: 1 }}\n        />\n\n        {/* Observed flux: error bars + dots (rendered before model curve) */}\n        <DataLayer />\n\n        {/* Smooth transit model overlaid on top of scatter data */}\n        <LinePlot skipAnimation />\n\n        <ChartsXAxis\n          label=\"Orbital Phase\"\n          tickLabelStyle={{ fontSize: 14, fill: t.inkSoft, fontFamily: FONT }}\n          labelStyle={{ fontSize: 16, fontWeight: \"500\", fill: t.inkSoft, fontFamily: FONT }}\n        />\n        <ChartsYAxis\n          label=\"Relative Flux\"\n          tickLabelStyle={{ fontSize: 14, fill: t.inkSoft, fontFamily: FONT }}\n          labelStyle={{ fontSize: 16, fontWeight: \"500\", fill: t.inkSoft, fontFamily: FONT }}\n        />\n\n        <LegendPanel />\n      </ChartContainer>\n    </Box>\n  );\n}\n"}