{"spec_id":"candlestick-basic","library":"muix","language":"javascript","code":"// anyplot.ai\n// candlestick-basic: Basic Candlestick Chart\n// Library: muix 7.29.1 | JavaScript 22.23.2\n// Quality: 87/100 | Created: 2026-08-24\n//# anyplot-orientation: landscape\n// anyplot.ai\n// candlestick-basic: Basic Candlestick Chart\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-08-24\n\nimport { ChartContainer } from \"@mui/x-charts/ChartContainer\";\nimport { ChartsXAxis } from \"@mui/x-charts/ChartsXAxis\";\nimport { ChartsYAxis } from \"@mui/x-charts/ChartsYAxis\";\nimport { ChartsGrid } from \"@mui/x-charts/ChartsGrid\";\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 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 PERIODS = 48; // hourly candles across two trading days\n\nconst open: number[] = [];\nconst high: number[] = [];\nconst low: number[] = [];\nconst close: number[] = [];\nlet price = 42000; // BTC/USD opening price\nfor (let i = 0; i < PERIODS; i++) {\n  const o = price;\n  const drift = (rand() - 0.5) * 260;\n  const c = Math.max(1000, o + drift);\n  const h = Math.max(o, c) + rand() * 140;\n  const l = Math.max(Math.min(o, c) - rand() * 140, 1);\n  open.push(o);\n  high.push(h);\n  low.push(l);\n  close.push(c);\n  price = c;\n}\n\n// Fixed calendar anchor (deterministic — not the render-time clock). Midnight\n// hours get a date label, other hours get a time label, matching the spec's\n// \"date formatting based on data frequency\" guidance for an hourly series.\nconst START = new Date(2024, 0, 8, 0, 0);\nconst dateLabels: string[] = [];\nfor (let i = 0; i < PERIODS; i++) {\n  const d = new Date(START);\n  d.setHours(d.getHours() + i);\n  dateLabels.push(\n    d.getHours() === 0\n      ? d.toLocaleDateString(\"en-US\", { month: \"short\", day: \"numeric\" })\n      : d.toLocaleTimeString(\"en-US\", { hour: \"2-digit\", minute: \"2-digit\", hour12: false }),\n  );\n}\n\nconst yMin = Math.min(...low);\nconst yMax = Math.max(...high);\nconst yPad = (yMax - yMin) * 0.12;\n\n// Session extremes — the focal point the review asked for: call out the\n// intraday high/low instead of presenting a uniform, unemphasized series.\nlet highIdx = 0;\nlet lowIdx = 0;\nfor (let i = 1; i < PERIODS; i++) {\n  if (high[i] > high[highIdx]) highIdx = i;\n  if (low[i] < low[lowIdx]) lowIdx = i;\n}\nconst fmtUsd = (v: number) => `$${Math.round(v).toLocaleString(\"en-US\")}`;\nconst highCallout = `Session High · ${fmtUsd(high[highIdx])}`;\nconst lowCallout = `Session Low · ${fmtUsd(low[lowIdx])}`;\n\n// --- Candlesticks: MUI X community has no native candlestick series — draw\n// against the shared band scale via useXScale/useYScale, the documented\n// ChartContainer composition pattern for chart types outside the community\n// surface. Bullish (close >= open) bodies are brand green, bearish bodies\n// matte red — the finance up/down semantic exception from the style guide.\n// Wick strokes stay well under the body width so the range line reads as\n// subordinate to the open/close body, per the spec's legibility note. A thin\n// ink-color stroke on every body is the style guide's \"optional outline\n// pattern\" — redundant contrast for CVD readers beyond hue alone.\nfunction Candlesticks() {\n  const xScale = useXScale(\"x\") as any;\n  const yScale = useYScale(\"y\") as any;\n  if (!xScale || !yScale) return null;\n  const bw = xScale.bandwidth();\n  const bodyWidth = bw * 0.62;\n  const [xRangeStart] = xScale.range();\n\n  const highCx = xScale(dateLabels[highIdx]) + bw / 2;\n  const highCy = yScale(high[highIdx]);\n  const lowCx = xScale(dateLabels[lowIdx]) + bw / 2;\n  const lowCy = yScale(low[lowIdx]);\n\n  return (\n    <g>\n      {dateLabels.map((label, i) => {\n        const cx = xScale(label) + bw / 2;\n        const bullish = close[i] >= open[i];\n        const color = bullish ? t.palette[0] : t.palette[4];\n        const yOpen = yScale(open[i]);\n        const yClose = yScale(close[i]);\n        const bodyTop = Math.min(yOpen, yClose);\n        const bodyHeight = Math.max(Math.abs(yClose - yOpen), 1.5);\n        return (\n          <g key={i}>\n            <line\n              x1={cx}\n              x2={cx}\n              y1={yScale(high[i])}\n              y2={yScale(low[i])}\n              stroke={color}\n              strokeWidth={1.5}\n              strokeLinecap=\"round\"\n            />\n            <rect\n              x={cx - bodyWidth / 2}\n              y={bodyTop}\n              width={bodyWidth}\n              height={bodyHeight}\n              fill={color}\n              stroke={t.ink}\n              strokeOpacity={0.35}\n              strokeWidth={1}\n            />\n          </g>\n        );\n      })}\n\n      {/* Focal-point callouts: dashed reference lines + labels pin the\n          session high and low, giving the series a deliberate story beyond\n          a uniform OHLC run. */}\n      <line\n        x1={xRangeStart}\n        x2={highCx}\n        y1={highCy}\n        y2={highCy}\n        stroke={t.inkSoft}\n        strokeOpacity={0.4}\n        strokeWidth={1}\n        strokeDasharray=\"2 3\"\n      />\n      <circle cx={highCx} cy={highCy} r={4} fill={t.pageBg} stroke={t.ink} strokeWidth={1.5} />\n      <text x={highCx} y={highCy - 12} textAnchor=\"middle\" fontSize={12} fontWeight={600} fill={t.inkSoft}>\n        {highCallout}\n      </text>\n\n      <line\n        x1={xRangeStart}\n        x2={lowCx}\n        y1={lowCy}\n        y2={lowCy}\n        stroke={t.inkSoft}\n        strokeOpacity={0.4}\n        strokeWidth={1}\n        strokeDasharray=\"2 3\"\n      />\n      <circle cx={lowCx} cy={lowCy} r={4} fill={t.pageBg} stroke={t.ink} strokeWidth={1.5} />\n      <text x={lowCx} y={lowCy + 20} textAnchor=\"middle\" fontSize={12} fontWeight={600} fill={t.inkSoft}>\n        {lowCallout}\n      </text>\n    </g>\n  );\n}\n\nexport default function Chart() {\n  const W = window.ANYPLOT_SIZE.width;\n  const H = window.ANYPLOT_SIZE.height;\n  const TITLE_H = 96;\n  const LEGEND_H = 52;\n  const chartH = H - TITLE_H - LEGEND_H;\n\n  const title = \"Bitcoin Hourly Price · candlestick-basic · javascript · muix · anyplot.ai\";\n  const titleSize = title.length > 67 ? Math.round((22 * 67) / title.length) : 22;\n\n  const rangeEnd = new Date(START);\n  rangeEnd.setHours(rangeEnd.getHours() + PERIODS - 1);\n  const subtitle = `${START.toLocaleDateString(\"en-US\", { month: \"short\", day: \"numeric\" })} – ${rangeEnd.toLocaleDateString(\n    \"en-US\",\n    { month: \"short\", day: \"numeric\", year: \"numeric\" },\n  )} · Hourly candles`;\n\n  const legendItems = [\n    { label: \"Bullish (close ≥ open)\", color: t.palette[0] },\n    { label: \"Bearish (close < open)\", color: t.palette[4] },\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\n        sx={{\n          height: TITLE_H,\n          display: \"flex\",\n          flexDirection: \"column\",\n          alignItems: \"center\",\n          justifyContent: \"center\",\n          gap: \"6px\",\n        }}\n      >\n        <Typography sx={{ color: t.ink, fontSize: titleSize, fontWeight: 600 }}>{title}</Typography>\n        <Typography sx={{ color: t.inkSoft, fontSize: 13, fontWeight: 400, letterSpacing: \"0.03em\" }}>\n          {subtitle}\n        </Typography>\n      </Box>\n\n      <ChartContainer\n        width={W}\n        height={chartH}\n        skipAnimation\n        series={[]}\n        xAxis={[\n          {\n            id: \"x\",\n            scaleType: \"band\",\n            data: dateLabels,\n            label: \"Time (Hourly)\",\n            tickLabelInterval: (_value: string, index: number) => index % 4 === 0,\n            tickLabelStyle: { fontSize: 14, fill: t.inkSoft },\n            labelStyle: { fontSize: 15, fill: t.ink },\n          },\n        ]}\n        yAxis={[\n          {\n            id: \"y\",\n            min: yMin - yPad,\n            max: yMax + yPad,\n            label: \"Price (USD)\",\n            valueFormatter: (v: number) => `$${Math.round(v).toLocaleString(\"en-US\")}`,\n            tickLabelStyle: { fontSize: 13, fill: t.inkSoft },\n            labelStyle: { fontSize: 15, fill: t.ink },\n          },\n        ]}\n        margin={{ top: 24, bottom: 64, left: 112, right: 32 }}\n        sx={{\n          \"& .MuiChartsAxis-line\": { stroke: t.inkSoft, strokeOpacity: 0.2 },\n          \"& .MuiChartsGrid-line\": { stroke: t.grid },\n        }}\n      >\n        <ChartsGrid horizontal />\n        <Candlesticks />\n        <ChartsXAxis axisId=\"x\" />\n        <ChartsYAxis axisId=\"y\" slotProps={{ axisLabel: { x: -80 } }} />\n      </ChartContainer>\n\n      <Box sx={{ height: LEGEND_H, display: \"flex\", alignItems: \"center\", justifyContent: \"center\", gap: \"16px\" }}>\n        {legendItems.map((item) => (\n          <Box\n            key={item.label}\n            sx={{\n              display: \"flex\",\n              alignItems: \"center\",\n              gap: \"8px\",\n              padding: \"6px 16px\",\n              borderRadius: \"999px\",\n              border: `1px solid ${t.grid}`,\n              bgcolor: t.elevatedBg,\n            }}\n          >\n            <Box sx={{ width: 10, height: 10, borderRadius: \"50%\", bgcolor: item.color }} />\n            <Typography sx={{ color: t.inkSoft, fontSize: 13, fontWeight: 500 }}>{item.label}</Typography>\n          </Box>\n        ))}\n      </Box>\n    </Box>\n  );\n}\n"}