{"spec_id":"candlestick-volume","library":"muix","language":"javascript","code":"// anyplot.ai\n// candlestick-volume: Stock Candlestick Chart with Volume\n// Library: muix 7.29.1 | JavaScript 22.23.2\n// Quality: 94/100 | Created: 2026-09-02\n\nimport { useState } from \"react\";\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 = 11;\nfunction rand() {\n  seed = (seed * 1103515245 + 12345) & 0x7fffffff;\n  return seed / 0x7fffffff;\n}\n\nconst PERIODS = 60; // daily candles, ~3 trading months\nconst open: number[] = [];\nconst high: number[] = [];\nconst low: number[] = [];\nconst close: number[] = [];\nconst volume: number[] = [];\nlet price = 148; // opening price, USD\n\nfor (let i = 0; i < PERIODS; i++) {\n  const o = price;\n  const drift = (rand() - 0.5) * 6.2;\n  const c = Math.max(20, o + drift);\n  const h = Math.max(o, c) + rand() * 2.4;\n  const l = Math.max(Math.min(o, c) - rand() * 2.4, 1);\n  const move = Math.abs(c - o) + (h - l);\n  const vol = Math.round(1.4e6 + move * 3.6e5 + rand() * 6e5);\n  open.push(o);\n  high.push(h);\n  low.push(l);\n  close.push(c);\n  volume.push(vol);\n  price = c;\n}\n\n// Fixed calendar anchor (deterministic — not the render-time clock).\nconst START = new Date(2024, 1, 1);\nconst dateLabels: string[] = [];\nconst fullDateLabels: string[] = [];\nfor (let i = 0; i < PERIODS; i++) {\n  const d = new Date(START);\n  d.setDate(d.getDate() + i);\n  dateLabels.push(\n    d.toLocaleDateString(\"en-US\", { month: \"short\", day: \"numeric\" }),\n  );\n  fullDateLabels.push(\n    d.toLocaleDateString(\"en-US\", {\n      month: \"short\",\n      day: \"numeric\",\n      year: \"numeric\",\n    }),\n  );\n}\n\nconst priceMin = Math.min(...low);\nconst priceMax = Math.max(...high);\nconst pricePad = (priceMax - priceMin) * 0.08;\nconst volumeMax = Math.max(...volume);\n\nconst fmtUsd = (v: number) => `$${v.toFixed(2)}`;\nconst fmtVolume = (v: number) =>\n  v >= 1e6 ? `${(v / 1e6).toFixed(1)}M` : `${Math.round(v / 1e3)}K`;\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.\nfunction Candlesticks() {\n  const xScale = useXScale(\"x\") as any;\n  const yScale = useYScale(\"yPrice\") as any;\n  if (!xScale || !yScale) return null;\n  const bw = xScale.bandwidth();\n  const bodyWidth = bw * 0.6;\n\n  return (\n    <g>\n      {/* Bearish bodies get a heavier outline + diagonal hatch on top of the\n      fill — a redundant (non-hue) encoding of the up/down distinction for\n      color-vision-deficient readers, alongside the brand green / matte red. */}\n      <defs>\n        <pattern\n          id=\"candle-bear-hatch\"\n          width={5}\n          height={5}\n          patternUnits=\"userSpaceOnUse\"\n          patternTransform=\"rotate(45)\"\n        >\n          <line\n            x1={0}\n            y1={0}\n            x2={0}\n            y2={5}\n            stroke={t.ink}\n            strokeOpacity={0.4}\n            strokeWidth={1.6}\n          />\n        </pattern>\n      </defs>\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.4}\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={bullish ? 0.3 : 0.6}\n              strokeWidth={bullish ? 1 : 2}\n            />\n            {!bullish && (\n              <rect\n                x={cx - bodyWidth / 2}\n                y={bodyTop}\n                width={bodyWidth}\n                height={bodyHeight}\n                fill=\"url(#candle-bear-hatch)\"\n                pointerEvents=\"none\"\n              />\n            )}\n          </g>\n        );\n      })}\n    </g>\n  );\n}\n\n// --- Volume bars: same up/down color scheme as the candlesticks above, on a\n// second ChartContainer sharing the identical band domain, width, and left /\n// right margins so its columns land under the matching candle.\nfunction VolumeBars() {\n  const xScale = useXScale(\"x2\") as any;\n  const yScale = useYScale(\"yVolume\") as any;\n  if (!xScale || !yScale) return null;\n  const bw = xScale.bandwidth();\n  const barWidth = bw * 0.6;\n  const yZero = yScale(0);\n\n  return (\n    <g>\n      {/* Same bearish hatch overlay as the candle bodies, so the secondary\n      up/down encoding stays consistent between the price and volume panes. */}\n      <defs>\n        <pattern\n          id=\"volume-bear-hatch\"\n          width={5}\n          height={5}\n          patternUnits=\"userSpaceOnUse\"\n          patternTransform=\"rotate(45)\"\n        >\n          <line\n            x1={0}\n            y1={0}\n            x2={0}\n            y2={5}\n            stroke={t.ink}\n            strokeOpacity={0.4}\n            strokeWidth={1.6}\n          />\n        </pattern>\n      </defs>\n      {dateLabels.map((label, i) => {\n        const bullish = close[i] >= open[i];\n        const color = bullish ? t.palette[0] : t.palette[4];\n        const cx = xScale(label) + bw / 2;\n        const yTop = yScale(volume[i]);\n        const barHeight = Math.max(yZero - yTop, 1);\n        return (\n          <g key={i}>\n            <rect\n              x={cx - barWidth / 2}\n              y={yTop}\n              width={barWidth}\n              height={barHeight}\n              fill={color}\n              fillOpacity={0.75}\n              stroke={bullish ? \"none\" : t.ink}\n              strokeOpacity={bullish ? 0 : 0.5}\n              strokeWidth={bullish ? 0 : 1.4}\n            />\n            {!bullish && (\n              <rect\n                x={cx - barWidth / 2}\n                y={yTop}\n                width={barWidth}\n                height={barHeight}\n                fill=\"url(#volume-bear-hatch)\"\n                pointerEvents=\"none\"\n              />\n            )}\n          </g>\n        );\n      })}\n    </g>\n  );\n}\n\nexport default function Chart() {\n  // Real cross-pane crosshair: mouse position over the shared band grid\n  // drives both the vertical rule and the OHLCV readout. Absent on the\n  // headless screenshot (no cursor at capture time) — that is expected, not\n  // faked; it is live in the emitted interactive HTML.\n  const [hoverIndex, setHoverIndex] = useState<number | null>(null);\n\n  const W = window.ANYPLOT_SIZE.width;\n  const H = window.ANYPLOT_SIZE.height;\n\n  const title = \"candlestick-volume · javascript · muix · anyplot.ai\";\n  const titleSize =\n    title.length > 67 ? Math.round((24 * 67) / title.length) : 24;\n\n  const legendItems = [\n    { label: \"Bullish (close ≥ open)\", color: t.palette[0], bearish: false },\n    { label: \"Bearish (close < open)\", color: t.palette[4], bearish: true },\n  ];\n\n  const TITLE_H = 64;\n  const LEGEND_H = 40;\n  const GAP = 10;\n  const chartsH = H - TITLE_H - LEGEND_H;\n  const PRICE_H = Math.round(chartsH * 0.72); // spec: price pane ~70-75% of vertical space\n  const VOLUME_H = chartsH - GAP - PRICE_H; // remaining ~25-30%\n\n  const MARGIN_LEFT = 100;\n  const MARGIN_RIGHT = 28;\n  const innerWidth = W - MARGIN_LEFT - MARGIN_RIGHT;\n  const bandStep = innerWidth / PERIODS;\n\n  const handleMouseMove = (event: React.MouseEvent<HTMLDivElement>) => {\n    const rect = event.currentTarget.getBoundingClientRect();\n    const localX = event.clientX - rect.left;\n    const idx = Math.floor((localX - MARGIN_LEFT) / bandStep);\n    setHoverIndex(idx >= 0 && idx < PERIODS ? idx : null);\n  };\n\n  const crosshairX =\n    hoverIndex === null\n      ? null\n      : MARGIN_LEFT + hoverIndex * bandStep + bandStep / 2;\n  const tooltipLeft =\n    crosshairX === null ? 0 : Math.min(Math.max(crosshairX - 100, 4), W - 216);\n\n  // Data storytelling: the Feb 19-21 selloff-into-reversal is the largest\n  // directional swing in the series and lines up with a volume spike — the\n  // spec's own framing (\"identify volume-confirmed trends or reversals\").\n  // Highlight that window across both panes instead of leaving all 60 days\n  // undifferentiated.\n  const HILITE_START = 18; // Feb 19\n  const HILITE_END = 20; // Feb 21 (inclusive)\n  const hiliteX = MARGIN_LEFT + HILITE_START * bandStep;\n  const hiliteWidth = (HILITE_END - HILITE_START + 1) * bandStep;\n  const calloutLeft = Math.min(\n    Math.max(hiliteX + hiliteWidth / 2 - 105, MARGIN_LEFT),\n    W - MARGIN_RIGHT - 210,\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          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      <Box\n        sx={{\n          height: LEGEND_H,\n          display: \"flex\",\n          alignItems: \"center\",\n          justifyContent: \"center\",\n          gap: \"16px\",\n        }}\n      >\n        {legendItems.map((item) => (\n          <Box\n            key={item.label}\n            sx={{\n              display: \"flex\",\n              alignItems: \"center\",\n              gap: \"8px\",\n              bgcolor: t.elevatedBg,\n              border: `1px solid ${t.grid}`,\n              borderRadius: \"999px\",\n              padding: \"4px 14px 4px 8px\",\n            }}\n          >\n            <Box\n              sx={{\n                width: 12,\n                height: 12,\n                borderRadius: \"3px\",\n                bgcolor: item.color,\n                border: item.bearish\n                  ? `2px solid ${t.ink}`\n                  : `2px solid transparent`,\n              }}\n            />\n            <Typography\n              sx={{\n                color: t.ink,\n                fontSize: 13,\n                fontWeight: 600,\n                letterSpacing: \"0.1px\",\n              }}\n            >\n              {item.label}\n            </Typography>\n          </Box>\n        ))}\n      </Box>\n\n      <Box\n        sx={{ position: \"relative\" }}\n        onMouseMove={handleMouseMove}\n        onMouseLeave={() => setHoverIndex(null)}\n      >\n        {/* Data-storytelling highlight: brackets the volume-confirmed\n        selloff/reversal window so it reads before the reader scans every\n        candle — painted first so bars/candles render on top of it. */}\n        <Box\n          sx={{\n            position: \"absolute\",\n            top: 0,\n            left: hiliteX,\n            width: hiliteWidth,\n            height: PRICE_H + GAP + VOLUME_H,\n            bgcolor: t.amber,\n            opacity: 0.09,\n            pointerEvents: \"none\",\n          }}\n        />\n        <Box\n          sx={{\n            position: \"absolute\",\n            top: 0,\n            left: hiliteX,\n            width: \"1px\",\n            height: PRICE_H + GAP + VOLUME_H,\n            bgcolor: t.amber,\n            opacity: 0.4,\n            pointerEvents: \"none\",\n          }}\n        />\n        <Box\n          sx={{\n            position: \"absolute\",\n            top: 0,\n            left: hiliteX + hiliteWidth,\n            width: \"1px\",\n            height: PRICE_H + GAP + VOLUME_H,\n            bgcolor: t.amber,\n            opacity: 0.4,\n            pointerEvents: \"none\",\n          }}\n        />\n        <Box\n          sx={{\n            position: \"absolute\",\n            top: 22,\n            left: calloutLeft,\n            maxWidth: 210,\n            bgcolor: t.elevatedBg,\n            border: `1px solid ${t.amber}`,\n            borderRadius: \"6px\",\n            padding: \"4px 10px\",\n            pointerEvents: \"none\",\n          }}\n        >\n          <Typography\n            sx={{ fontSize: 11, fontWeight: 700, color: t.ink, lineHeight: 1.3 }}\n          >\n            Selloff → reversal\n          </Typography>\n          <Typography sx={{ fontSize: 10, color: t.inkSoft, lineHeight: 1.3 }}>\n            Feb 19-21, volume-confirmed\n          </Typography>\n        </Box>\n\n        {/* Subtle tint on the volume pane's plot area delineates it from the\n        price pane above, beyond the shared divider rule. */}\n        <Box\n          sx={{\n            position: \"absolute\",\n            top: PRICE_H + GAP,\n            left: MARGIN_LEFT,\n            width: W - MARGIN_LEFT - MARGIN_RIGHT,\n            height: VOLUME_H,\n            bgcolor: t.elevatedBg,\n            opacity: 0.4,\n            pointerEvents: \"none\",\n          }}\n        />\n\n        <ChartContainer\n          width={W}\n          height={PRICE_H}\n          skipAnimation\n          series={[]}\n          xAxis={[{ id: \"x\", scaleType: \"band\", data: dateLabels }]}\n          yAxis={[\n            {\n              id: \"yPrice\",\n              min: priceMin - pricePad,\n              max: priceMax + pricePad,\n              label: \"Price (USD)\",\n              valueFormatter: fmtUsd,\n              tickLabelStyle: { fontSize: 13, fill: t.inkSoft },\n              labelStyle: { fontSize: 15, fill: t.ink },\n            },\n          ]}\n          margin={{\n            top: 20,\n            bottom: 8,\n            left: MARGIN_LEFT,\n            right: MARGIN_RIGHT,\n          }}\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          <ChartsYAxis axisId=\"yPrice\" slotProps={{ axisLabel: { x: -80 } }} />\n        </ChartContainer>\n\n        <Box\n          sx={{\n            height: GAP,\n            display: \"flex\",\n            alignItems: \"center\",\n            marginLeft: `${MARGIN_LEFT}px`,\n            marginRight: `${MARGIN_RIGHT}px`,\n          }}\n        >\n          <Box sx={{ flex: 1, height: \"1px\", bgcolor: t.grid }} />\n        </Box>\n\n        <ChartContainer\n          width={W}\n          height={VOLUME_H}\n          skipAnimation\n          series={[]}\n          xAxis={[\n            {\n              id: \"x2\",\n              scaleType: \"band\",\n              data: dateLabels,\n              label: \"Date\",\n              tickLabelInterval: (_value: string, index: number) =>\n                index % 6 === 0,\n              tickLabelStyle: { fontSize: 13, fill: t.inkSoft },\n              labelStyle: { fontSize: 15, fill: t.ink },\n            },\n          ]}\n          yAxis={[\n            {\n              id: \"yVolume\",\n              min: 0,\n              max: volumeMax * 1.15,\n              label: \"Volume\",\n              valueFormatter: fmtVolume,\n              tickLabelStyle: { fontSize: 12, fill: t.inkSoft },\n              labelStyle: { fontSize: 14, fill: t.ink },\n            },\n          ]}\n          margin={{\n            top: 8,\n            bottom: 52,\n            left: MARGIN_LEFT,\n            right: MARGIN_RIGHT,\n          }}\n          sx={{\n            \"& .MuiChartsAxis-line\": { stroke: t.inkSoft, strokeOpacity: 0.2 },\n            \"& .MuiChartsGrid-line\": { stroke: t.grid },\n          }}\n        >\n          <ChartsGrid horizontal />\n          <VolumeBars />\n          <ChartsXAxis axisId=\"x2\" />\n          <ChartsYAxis axisId=\"yVolume\" slotProps={{ axisLabel: { x: -70 } }} />\n        </ChartContainer>\n\n        {crosshairX !== null && (\n          <Box\n            sx={{\n              position: \"absolute\",\n              top: 0,\n              left: crosshairX,\n              width: \"1px\",\n              height: PRICE_H + GAP + VOLUME_H,\n              bgcolor: t.ink,\n              opacity: 0.45,\n              pointerEvents: \"none\",\n            }}\n          />\n        )}\n\n        {hoverIndex !== null && (\n          <Box\n            sx={{\n              position: \"absolute\",\n              top: 8,\n              left: tooltipLeft,\n              bgcolor: t.elevatedBg,\n              border: `1px solid ${t.grid}`,\n              borderRadius: \"6px\",\n              padding: \"8px 14px\",\n              pointerEvents: \"none\",\n            }}\n          >\n            <Typography sx={{ fontSize: 12, fontWeight: 600, color: t.ink }}>\n              {fullDateLabels[hoverIndex]}\n            </Typography>\n            <Typography sx={{ fontSize: 11, color: t.inkSoft }}>\n              O {fmtUsd(open[hoverIndex])} &nbsp;H {fmtUsd(high[hoverIndex])}{\" \"}\n              &nbsp;L {fmtUsd(low[hoverIndex])} &nbsp;C{\" \"}\n              {fmtUsd(close[hoverIndex])}\n            </Typography>\n            <Typography sx={{ fontSize: 11, color: t.inkSoft }}>\n              Vol {fmtVolume(volume[hoverIndex])}\n            </Typography>\n          </Box>\n        )}\n      </Box>\n    </Box>\n  );\n}\n"}