{"spec_id":"indicator-ichimoku","library":"muix","language":"javascript","code":"// anyplot.ai\n// indicator-ichimoku: Ichimoku Cloud Technical Indicator Chart\n// Library: muix 7.29.1 | JavaScript 22.23.2\n// Quality: 92/100 | Created: 2026-08-20\n//# anyplot-orientation: landscape\n// anyplot.ai\n// indicator-ichimoku: Ichimoku Cloud Technical Indicator 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-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 { 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 HISTORY = 130; // trading periods with OHLC data\nconst FORWARD = 26; // Senkou spans project this many periods ahead of price data\nconst TOTAL = HISTORY + FORWARD;\n\nconst open: number[] = [];\nconst high: number[] = [];\nconst low: number[] = [];\nconst close: number[] = [];\nlet price = 148;\nfor (let i = 0; i < HISTORY; i++) {\n  const o = price;\n  const drift = (rand() - 0.47) * 2.4;\n  const c = Math.max(40, o + drift);\n  const h = Math.max(o, c) + rand() * 1.6;\n  const l = Math.max(Math.min(o, c) - rand() * 1.6, 1);\n  open.push(o);\n  close.push(c);\n  high.push(h);\n  low.push(l);\n  price = c;\n}\n\n// Standard Ichimoku parameters: 9 / 26 / 52\nfunction rollingMid(period: number, i: number) {\n  if (i < period - 1) return null;\n  let hh = -Infinity;\n  let ll = Infinity;\n  for (let k = i - period + 1; k <= i; k++) {\n    hh = Math.max(hh, high[k]);\n    ll = Math.min(ll, low[k]);\n  }\n  return (hh + ll) / 2;\n}\n\nconst tenkanRaw = open.map((_, i) => rollingMid(9, i));\nconst kijunRaw = open.map((_, i) => rollingMid(26, i));\nconst spanARaw = open.map((_, i) =>\n  tenkanRaw[i] != null && kijunRaw[i] != null ? (tenkanRaw[i]! + kijunRaw[i]!) / 2 : null,\n);\nconst spanBRaw = open.map((_, i) => rollingMid(52, i));\n\n// Fixed calendar anchor (deterministic — not the render-time clock)\nconst START = new Date(2024, 0, 2);\nconst dateLabels: string[] = [];\nfor (let i = 0; i < TOTAL; i++) {\n  const d = new Date(START);\n  d.setDate(d.getDate() + i);\n  dateLabels.push(d.toLocaleDateString(\"en-US\", { month: \"short\", day: \"numeric\" }));\n}\n\n// Full-width arrays (length TOTAL) so candles, indicator lines and the\n// forward-shifted / backward-shifted spans all share the same x categories\nconst openArr = new Array(TOTAL).fill(null) as (number | null)[];\nconst highArr = new Array(TOTAL).fill(null) as (number | null)[];\nconst lowArr = new Array(TOTAL).fill(null) as (number | null)[];\nconst closeArr = new Array(TOTAL).fill(null) as (number | null)[];\nconst tenkanArr = new Array(TOTAL).fill(null) as (number | null)[];\nconst kijunArr = new Array(TOTAL).fill(null) as (number | null)[];\nconst spanAArr = new Array(TOTAL).fill(null) as (number | null)[];\nconst spanBArr = new Array(TOTAL).fill(null) as (number | null)[];\nconst chikouArr = new Array(TOTAL).fill(null) as (number | null)[];\n\nfor (let i = 0; i < HISTORY; i++) {\n  openArr[i] = open[i];\n  highArr[i] = high[i];\n  lowArr[i] = low[i];\n  closeArr[i] = close[i];\n  tenkanArr[i] = tenkanRaw[i];\n  kijunArr[i] = kijunRaw[i];\n  if (spanARaw[i] != null && i + FORWARD < TOTAL) spanAArr[i + FORWARD] = spanARaw[i];\n  if (spanBRaw[i] != null && i + FORWARD < TOTAL) spanBArr[i + FORWARD] = spanBRaw[i];\n  if (i >= FORWARD) chikouArr[i - FORWARD] = close[i];\n}\n\nconst definedValues = [\n  ...highArr,\n  ...lowArr,\n  ...spanAArr,\n  ...spanBArr,\n  ...tenkanArr,\n  ...kijunArr,\n  ...chikouArr,\n].filter((v): v is number => v != null);\nconst yMin = Math.min(...definedValues);\nconst yMax = Math.max(...definedValues);\nconst yPad = (yMax - yMin) * 0.08;\n\n// --- Candlesticks: MUI X has no native candlestick — draw against the\n// shared band scale via useXScale/useYScale, the documented ChartContainer\n// composition pattern for chart types outside the community surface -------\n// Bullish bodies are drawn HOLLOW (page-background fill, colored outline) and\n// bearish bodies SOLID-filled — a fill/outline pattern cue redundant to hue,\n// the classic candlestick convention, so trend direction survives CVD sims.\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.72;\n\n  return (\n    <g>\n      {openArr.slice(0, HISTORY).map((_, i) => {\n        const cx = xScale(dateLabels[i]) + bw / 2;\n        const bullish = closeArr[i]! >= openArr[i]!;\n        const color = bullish ? t.palette[0] : t.palette[4];\n        const yO = yScale(openArr[i]!);\n        const yC = yScale(closeArr[i]!);\n        const bodyTop = Math.min(yO, yC);\n        const bodyHeight = Math.max(Math.abs(yC - yO), 1.5);\n        return (\n          <g key={i}>\n            <line\n              x1={cx}\n              x2={cx}\n              y1={yScale(highArr[i]!)}\n              y2={yScale(lowArr[i]!)}\n              stroke={color}\n              strokeWidth={1.5}\n            />\n            <rect\n              x={cx - bodyWidth / 2}\n              y={bodyTop}\n              width={bodyWidth}\n              height={bodyHeight}\n              fill={bullish ? t.pageBg : color}\n              stroke={color}\n              strokeWidth={bullish ? 2 : 0}\n            />\n          </g>\n        );\n      })}\n    </g>\n  );\n}\n\n// Kumo cloud: the filled area between Senkou Span A and B, split into\n// bullish/bearish segments at every crossover (green when A > B, red when\n// B > A — the spec's \"at-a-glance\" trend signal). Bearish borders are dashed\n// (solid for bullish) as a pattern cue redundant to hue, and the bearish fill\n// gets a theme-adaptive opacity bump so it reads as clearly on the near-black\n// dark page as the bullish fill does — both cloud states equally legible.\nfunction KumoCloud() {\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 cx = (i: number) => xScale(dateLabels[i]) + bw / 2;\n  const isDark = window.ANYPLOT_THEME === \"dark\";\n\n  const segments: { idx: number[]; bullish: boolean }[] = [];\n  let current: number[] = [];\n  let bullish: boolean | null = null;\n  for (let j = 0; j < TOTAL; j++) {\n    const a = spanAArr[j];\n    const b = spanBArr[j];\n    if (a == null || b == null) {\n      if (current.length > 1) segments.push({ idx: current, bullish: bullish! });\n      current = [];\n      bullish = null;\n      continue;\n    }\n    const up = a >= b;\n    if (bullish === null) {\n      bullish = up;\n      current = [j];\n    } else if (up === bullish) {\n      current.push(j);\n    } else {\n      current.push(j); // shared boundary point keeps segments seamless at the crossover\n      segments.push({ idx: current, bullish });\n      current = [j];\n      bullish = up;\n    }\n  }\n  if (current.length > 1) segments.push({ idx: current, bullish: bullish! });\n\n  return (\n    <g>\n      {segments.map((seg, s) => {\n        const color = seg.bullish ? t.palette[0] : t.palette[4];\n        const fillOpacity = seg.bullish ? 0.22 : isDark ? 0.34 : 0.24;\n        const dash = seg.bullish ? undefined : \"6 4\";\n        const top = seg.idx.map((j) => `${cx(j)},${yScale(spanAArr[j]!)}`);\n        const bottom = seg.idx\n          .slice()\n          .reverse()\n          .map((j) => `${cx(j)},${yScale(spanBArr[j]!)}`);\n        return (\n          <g key={s}>\n            <polygon points={[...top, ...bottom].join(\" \")} fill={color} fillOpacity={fillOpacity} />\n            <polyline\n              points={top.join(\" \")}\n              fill=\"none\"\n              stroke={color}\n              strokeOpacity={0.55}\n              strokeWidth={1.25}\n              strokeDasharray={dash}\n            />\n            <polyline\n              points={seg.idx.map((j) => `${cx(j)},${yScale(spanBArr[j]!)}`).join(\" \")}\n              fill=\"none\"\n              stroke={color}\n              strokeOpacity={0.55}\n              strokeWidth={1.25}\n              strokeDasharray={dash}\n            />\n          </g>\n        );\n      })}\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 = 74;\n  const LEGEND_H = 48;\n  const chartH = H - TITLE_H - LEGEND_H;\n\n  const title = \"TechCo Daily Price · indicator-ichimoku · javascript · muix · anyplot.ai\";\n  const titleSize = title.length > 67 ? Math.round((22 * 67) / title.length) : 22;\n\n  const legendItems = [\n    {\n      label: \"Bullish Candle\",\n      swatch: <Box sx={{ width: 14, height: 14, bgcolor: t.pageBg, border: `2px solid ${t.palette[0]}`, borderRadius: \"2px\" }} />,\n    },\n    { label: \"Bearish Candle\", swatch: <Box sx={{ width: 14, height: 14, bgcolor: t.palette[4], borderRadius: \"2px\" }} /> },\n    { label: \"Tenkan-sen\", swatch: <Box sx={{ width: 22, height: 3, bgcolor: t.palette[1], borderRadius: \"2px\" }} /> },\n    { label: \"Kijun-sen\", swatch: <Box sx={{ width: 22, height: 3, bgcolor: t.palette[2], borderRadius: \"2px\" }} /> },\n    { label: \"Chikou Span\", swatch: <Box sx={{ width: 22, height: 0, borderTop: `2.5px dashed ${t.palette[3]}` }} /> },\n    {\n      label: \"Kumo (Bullish)\",\n      swatch: <Box sx={{ width: 18, height: 14, bgcolor: t.palette[0], opacity: 0.3, borderRadius: \"2px\", border: `1.5px solid ${t.palette[0]}` }} />,\n    },\n    {\n      label: \"Kumo (Bearish)\",\n      swatch: (\n        <Box\n          sx={{\n            width: 18,\n            height: 14,\n            bgcolor: t.palette[4],\n            opacity: 0.3,\n            borderRadius: \"2px\",\n            border: `1.5px dashed ${t.palette[4]}`,\n          }}\n        />\n      ),\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      <ChartContainer\n        width={W}\n        height={chartH}\n        skipAnimation\n        series={[\n          {\n            type: \"line\",\n            id: \"tenkan\",\n            xAxisId: \"x\",\n            yAxisId: \"y\",\n            data: tenkanArr,\n            label: \"Tenkan-sen\",\n            color: t.palette[1],\n            showMark: false,\n            curve: \"linear\",\n            connectNulls: false,\n          },\n          {\n            type: \"line\",\n            id: \"kijun\",\n            xAxisId: \"x\",\n            yAxisId: \"y\",\n            data: kijunArr,\n            label: \"Kijun-sen\",\n            color: t.palette[2],\n            showMark: false,\n            curve: \"linear\",\n            connectNulls: false,\n          },\n          {\n            type: \"line\",\n            id: \"chikou\",\n            xAxisId: \"x\",\n            yAxisId: \"y\",\n            data: chikouArr,\n            label: \"Chikou Span\",\n            color: t.palette[3],\n            showMark: false,\n            curve: \"linear\",\n            connectNulls: false,\n          },\n        ]}\n        xAxis={[\n          {\n            id: \"x\",\n            scaleType: \"band\",\n            data: dateLabels,\n            tickLabelInterval: (_value: string, index: number) => index % 13 === 0,\n            tickLabelStyle: { fontSize: 14, fill: t.inkSoft },\n          },\n        ]}\n        yAxis={[\n          {\n            id: \"y\",\n            min: yMin - yPad,\n            max: yMax + yPad,\n            valueFormatter: (v: number) => `$${v.toFixed(0)}`,\n            tickLabelStyle: { fontSize: 13, fill: t.inkSoft },\n          },\n        ]}\n        margin={{ top: 24, bottom: 64, left: 112, right: 32 }}\n        sx={{\n          \"& .MuiLineElement-series-chikou\": { strokeDasharray: \"7 5\", strokeWidth: 2.5 },\n          \"& .MuiLineElement-series-tenkan\": { strokeWidth: 2.25 },\n          \"& .MuiLineElement-series-kijun\": { strokeWidth: 2.25 },\n          \"& .MuiChartsAxis-line\": { stroke: t.inkSoft, strokeOpacity: 0.2 },\n          \"& .MuiChartsGrid-line\": { stroke: t.grid },\n        }}\n      >\n        <ChartsGrid horizontal />\n        <KumoCloud />\n        <Candlesticks />\n        <LinePlot skipAnimation />\n        <ChartsXAxis axisId=\"x\" label=\"Trading Date\" labelStyle={{ fontSize: 15, fill: t.ink }} />\n        {/* Explicit axisLabel x offset — the default offset formula assumes short\n            tick labels and clips against our \"$162\"-style dollar-formatted ticks. */}\n        <ChartsYAxis\n          axisId=\"y\"\n          label=\"Price (USD)\"\n          labelStyle={{ fontSize: 15, fill: t.ink }}\n          slotProps={{ axisLabel: { x: -72 } }}\n        />\n      </ChartContainer>\n\n      <Box sx={{ height: LEGEND_H, display: \"flex\", alignItems: \"center\", justifyContent: \"center\", gap: \"26px\", flexWrap: \"wrap\" }}>\n        {legendItems.map((item) => (\n          <Box key={item.label} sx={{ display: \"flex\", alignItems: \"center\", gap: \"8px\" }}>\n            {item.swatch}\n            <Typography sx={{ color: t.inkSoft, fontSize: 13 }}>{item.label}</Typography>\n          </Box>\n        ))}\n      </Box>\n    </Box>\n  );\n}\n"}