{"spec_id":"stock-event-flags","library":"muix","language":"javascript","code":"// anyplot.ai\n// stock-event-flags: Stock Chart with Event Flags\n// Library: muix 7.29.1 | JavaScript 22.23.2\n// Quality: 93/100 | Created: 2026-08-26\nimport { LineChart } from \"@mui/x-charts/LineChart\";\nimport { useXScale, useYScale, 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 = 42;\nfunction rand() {\n  seed = (seed * 1103515245 + 12345) & 0x7fffffff;\n  return seed / 0x7fffffff;\n}\n\nconst TRADING_DAYS = 220; // ~10.5 months of weekday sessions\nconst tradingDates: Date[] = [];\n{\n  const cursor = new Date(2024, 0, 2); // Tue Jan 2, 2024\n  while (tradingDates.length < TRADING_DAYS) {\n    const weekday = cursor.getDay();\n    if (weekday !== 0 && weekday !== 6) tradingDates.push(new Date(cursor));\n    cursor.setDate(cursor.getDate() + 1);\n  }\n}\n\n// Corporate events across the year: quarterly earnings, ex-dividend dates,\n// one stock split, and a product-launch news item. `priceShockPct` nudges\n// that session's drift — earnings beats/misses move price the same day,\n// dividends and splits do not (a split here is assumed already\n// price-adjusted, as most historical series are).\nconst EVENTS = [\n  { day: 18, type: \"news\", label: \"New Product Line\", priceShockPct: 0.03 },\n  { day: 42, type: \"earnings\", label: \"Q1 Earnings Beat\", priceShockPct: 0.06 },\n  { day: 63, type: \"dividend\", label: \"Ex-Div $0.24\", priceShockPct: 0 },\n  { day: 96, type: \"earnings\", label: \"Q2 Earnings Miss\", priceShockPct: -0.07 },\n  { day: 124, type: \"dividend\", label: \"Ex-Div $0.24\", priceShockPct: 0 },\n  { day: 152, type: \"split\", label: \"4-for-1 Split\", priceShockPct: 0 },\n  { day: 176, type: \"earnings\", label: \"Q3 Earnings Beat\", priceShockPct: 0.05 },\n  { day: 208, type: \"earnings\", label: \"Q4 Earnings Beat\", priceShockPct: 0.04 },\n];\nconst shockByDay = new Map(EVENTS.map((e) => [e.day, e.priceShockPct]));\n\nconst closePrices: number[] = [];\nlet price = 148; // opening share price (USD)\nfor (let i = 0; i < TRADING_DAYS; i++) {\n  const drift = 0.0009; // gentle upward bias across the year\n  const noise = (rand() - 0.5) * 0.024;\n  const shock = shockByDay.get(i) ?? 0;\n  price = Math.max(20, price * (1 + drift + noise + shock));\n  closePrices.push(price);\n}\n\n// One tick per calendar month keeps the point-scale x-axis legible instead\n// of a tick for every one of the 220 trading days.\nconst monthStartIndices = new Set<number>();\n{\n  let lastMonth = -1;\n  tradingDates.forEach((d, i) => {\n    if (d.getMonth() !== lastMonth) {\n      monthStartIndices.add(i);\n      lastMonth = d.getMonth();\n    }\n  });\n}\n\nconst minClose = Math.min(...closePrices);\nconst maxClose = Math.max(...closePrices);\nconst closeRange = maxClose - minClose;\nconst Y_MIN = minClose - closeRange * 0.22;\nconst Y_MAX = maxClose + closeRange * 0.5; // headroom for flags pointing up\n\n// Event-type palette, distinct from the brand-green price line. Each type\n// also gets its own icon shape (below) so color isn't the only cue.\nconst EVENT_COLOR: Record<string, string> = {\n  earnings: t.palette[2], // blue\n  dividend: t.palette[3], // ochre — money association\n  split: t.palette[1], // lavender\n  news: t.palette[5], // cyan\n};\nconst EVENT_TYPE_LABEL: Record<string, string> = {\n  earnings: \"Earnings\",\n  dividend: \"Dividend\",\n  split: \"Stock Split\",\n  news: \"News\",\n};\n\nfunction EventIcon({ type, color, size }: { type: string; color: string; size: number }) {\n  const s = size;\n  if (type === \"earnings\") {\n    return (\n      <g>\n        <rect x={0} y={s * 0.55} width={s * 0.22} height={s * 0.45} fill={color} />\n        <rect x={s * 0.36} y={s * 0.28} width={s * 0.22} height={s * 0.72} fill={color} />\n        <rect x={s * 0.72} y={0} width={s * 0.22} height={s} fill={color} />\n      </g>\n    );\n  }\n  if (type === \"dividend\") {\n    return (\n      <g>\n        <circle cx={s / 2} cy={s / 2} r={s / 2} fill={color} />\n        <text x={s / 2} y={s / 2 + 1} textAnchor=\"middle\" dominantBaseline=\"central\" fontSize={s * 0.62} fontWeight={700} fill={t.pageBg}>\n          $\n        </text>\n      </g>\n    );\n  }\n  if (type === \"split\") {\n    return (\n      <g>\n        <line x1={s / 2} y1={0} x2={s / 2} y2={s} stroke={color} strokeWidth={s * 0.14} />\n        <polygon points={`0,${s * 0.22} ${s * 0.34},0 ${s * 0.34},${s * 0.44}`} fill={color} />\n        <polygon points={`${s},${s * 0.78} ${s * 0.66},${s} ${s * 0.66},${s * 0.56}`} fill={color} />\n      </g>\n    );\n  }\n  // news — a five-point star\n  const cx = s / 2;\n  const cy = s / 2;\n  const outerR = s / 2;\n  const innerR = s * 0.22;\n  const pts: string[] = [];\n  for (let i = 0; i < 10; i++) {\n    const r = i % 2 === 0 ? outerR : innerR;\n    const angle = (Math.PI / 5) * i - Math.PI / 2;\n    pts.push(`${cx + r * Math.cos(angle)},${cy + r * Math.sin(angle)}`);\n  }\n  return <polygon points={pts.join(\" \")} fill={color} />;\n}\n\n// --- Event flags: MUI X community has no native annotation-flag primitive\n// — draw against the shared xAxis/yAxis scale via useXScale/useYScale, the\n// documented ChartContainer/LineChart composition pattern for marks outside\n// the community surface. Flags alternate above/below the price line so\n// nearby events don't collide, connect back to their exact trading day with\n// a dashed pole, and get an elbow segment when the box has to be nudged in\n// from the plot edge.\nfunction EventFlags() {\n  const xScale = useXScale() as any;\n  const yScale = useYScale() as any;\n  const drawingArea = useDrawingArea();\n  if (!xScale || !yScale) return null;\n\n  const boxW = 156;\n  const boxH = 50;\n  const poleLen = 78;\n  const iconSize = 16;\n\n  return (\n    <g>\n      {EVENTS.map((event, i) => {\n        const x = xScale(tradingDates[event.day]);\n        const y = yScale(closePrices[event.day]);\n        const up = i % 2 === 0;\n        const anchorY = up ? y - poleLen : y + poleLen;\n        const boxY = up ? anchorY - boxH - 8 : anchorY + 8;\n        const clampedBoxY = Math.min(Math.max(boxY, drawingArea.top + 4), drawingArea.top + drawingArea.height - boxH - 4);\n        const boxCenterX = Math.min(\n          Math.max(x, drawingArea.left + boxW / 2 + 4),\n          drawingArea.left + drawingArea.width - boxW / 2 - 4,\n        );\n        const boxX = boxCenterX - boxW / 2;\n        const color = EVENT_COLOR[event.type];\n\n        return (\n          <g key={`${event.day}-${event.type}`}>\n            <line x1={x} y1={y} x2={x} y2={anchorY} stroke={color} strokeWidth={1.5} strokeDasharray=\"3 3\" strokeOpacity={0.75} />\n            {boxCenterX !== x && (\n              <line x1={x} y1={anchorY} x2={boxCenterX} y2={anchorY} stroke={color} strokeWidth={1.5} strokeDasharray=\"3 3\" strokeOpacity={0.75} />\n            )}\n            <circle cx={x} cy={y} r={5} fill={t.pageBg} stroke={color} strokeWidth={2} />\n\n            <rect x={boxX} y={clampedBoxY} width={boxW} height={boxH} rx={7} fill={t.elevatedBg} stroke={color} strokeWidth={1.5} />\n            <rect x={boxX} y={clampedBoxY} width={4} height={boxH} rx={2} fill={color} />\n            <g transform={`translate(${boxX + 14}, ${clampedBoxY + boxH / 2 - iconSize / 2})`}>\n              <EventIcon type={event.type} color={color} size={iconSize} />\n            </g>\n            <text x={boxX + 14 + iconSize + 8} y={clampedBoxY + boxH * 0.4} fontSize={13} fontWeight={600} fill={t.ink}>\n              {event.label}\n            </text>\n            <text x={boxX + 14 + iconSize + 8} y={clampedBoxY + boxH * 0.74} fontSize={11} fill={t.inkSoft}>\n              {EVENT_TYPE_LABEL[event.type]} · {tradingDates[event.day].toLocaleDateString(\"en-US\", { month: \"short\", day: \"numeric\" })}\n            </text>\n          </g>\n        );\n      })}\n    </g>\n  );\n}\n\nconst TITLE = \"TechCore Inc. Stock Price · stock-event-flags · 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  const TITLE_H = 62;\n  const LEGEND_H = 56;\n  const chartH = H - TITLE_H - LEGEND_H;\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 \"$205\"-wide axis\n  // collides with it. A hand-rotated label in its own flex column sidesteps\n  // that and gives predictable, collision-free spacing.\n  const yLabelWidth = 34;\n  const chartW = W - yLabelWidth;\n\n  const legendTypes = [\"earnings\", \"dividend\", \"split\", \"news\"];\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      <Box sx={{ display: \"flex\", flexDirection: \"row\", height: chartH }}>\n        <Box sx={{ width: yLabelWidth, display: \"flex\", alignItems: \"center\", justifyContent: \"center\" }}>\n          <Typography sx={{ fontSize: 15, color: t.ink, whiteSpace: \"nowrap\", transform: \"rotate(-90deg)\" }}>\n            Share Price (USD)\n          </Typography>\n        </Box>\n        <LineChart\n          width={chartW}\n          height={chartH}\n          skipAnimation\n          series={[\n            {\n              id: \"close\",\n              data: closePrices,\n              showMark: false,\n              area: true,\n              color: t.palette[0],\n              valueFormatter: (v: number | null) => (v == null ? \"\" : `$${v.toFixed(2)}`),\n            },\n          ]}\n          xAxis={[\n            {\n              data: tradingDates,\n              scaleType: \"point\",\n              label: \"Trading Date\",\n              valueFormatter: (d: Date) => d.toLocaleDateString(\"en-US\", { month: \"short\" }),\n              tickInterval: (_value: Date, index: number) => monthStartIndices.has(index),\n              tickLabelStyle: { fontSize: 13, fill: t.inkSoft },\n              labelStyle: { fontSize: 15, fill: t.ink },\n            },\n          ]}\n          yAxis={[\n            {\n              min: Y_MIN,\n              max: Y_MAX,\n              valueFormatter: (v: number) => `$${Math.round(v)}`,\n              tickLabelStyle: { fontSize: 13, fill: t.inkSoft },\n            },\n          ]}\n          grid={{ horizontal: true }}\n          slotProps={{ legend: { hidden: true } }}\n          margin={{ top: 24, right: 32, bottom: 56, left: 66 }}\n          sx={{\n            \"& .MuiLineElement-root\": { strokeWidth: 2.5 },\n            \"& .MuiAreaElement-root\": { fillOpacity: 0.12 },\n            \"& .MuiChartsAxis-line\": { stroke: t.inkSoft, strokeOpacity: 0.25 },\n            \"& .MuiChartsGrid-line\": { stroke: t.grid, strokeDasharray: \"4 3\" },\n          }}\n        >\n          <EventFlags />\n        </LineChart>\n      </Box>\n\n      <Box sx={{ height: LEGEND_H, display: \"flex\", alignItems: \"center\", justifyContent: \"center\", gap: \"14px\" }}>\n        {legendTypes.map((type) => (\n          <Box\n            key={type}\n            sx={{\n              display: \"flex\",\n              alignItems: \"center\",\n              gap: \"8px\",\n              padding: \"6px 14px\",\n              borderRadius: \"999px\",\n              border: `1px solid ${t.grid}`,\n              bgcolor: t.elevatedBg,\n            }}\n          >\n            <svg width={14} height={14} viewBox=\"0 0 14 14\">\n              <EventIcon type={type} color={EVENT_COLOR[type]} size={14} />\n            </svg>\n            <Typography sx={{ color: t.inkSoft, fontSize: 13, fontWeight: 500 }}>{EVENT_TYPE_LABEL[type]}</Typography>\n          </Box>\n        ))}\n      </Box>\n    </Box>\n  );\n}\n"}