{"spec_id":"kagi-basic","library":"muix","language":"javascript","code":"// anyplot.ai\n// kagi-basic: Basic Kagi Chart\n// Library: muix 7.29.1 | JavaScript 22.23.2\n// Quality: 94/100 | Created: 2026-09-02\nimport { LineChart } from \"@mui/x-charts/LineChart\";\nimport { ChartsReferenceLine } from \"@mui/x-charts/ChartsReferenceLine\";\nimport Box from \"@mui/material/Box\";\nimport Typography from \"@mui/material/Typography\";\n\nconst t = window.ANYPLOT_TOKENS;\nconst TITLE = \"kagi-basic · javascript · muix · anyplot.ai\";\nconst TITLE_HEIGHT = 56;\n\n// --- Data (in-memory, deterministic LCG — no seeded RNG in the browser) -----\nfunction lcg(seed) {\n  let state = seed;\n  return () => {\n    state = (state * 1664525 + 1013904223) % 4294967296;\n    return state / 4294967296;\n  };\n}\n\nfunction randomNormal(rand, mean, stdDev) {\n  const u1 = Math.max(rand(), 1e-9);\n  const u2 = rand();\n  const z = Math.sqrt(-2 * Math.log(u1)) * Math.cos(2 * Math.PI * u2);\n  return mean + z * stdDev;\n}\n\n// Daily closing prices for a mid-cap stock over ~1 trading year, generated as\n// a log-return random walk (mild upward drift, realistic daily volatility).\nconst OBSERVATIONS = 240;\nconst rand = lcg(42);\nlet price = 180;\nconst closingPrices = [price];\nfor (let i = 1; i < OBSERVATIONS; i++) {\n  const dailyReturn = randomNormal(rand, 0.0006, 0.016);\n  price *= 1 + dailyReturn;\n  closingPrices.push(price);\n}\n\n// Kagi construction: track a running extreme in the current direction and\n// only flip once price reverses by the threshold — this is what filters\n// time-based noise and keeps only the meaningful swings as turning points.\nconst REVERSAL_PCT = 0.04;\n\nfunction buildKagiTurningPoints(prices, reversalPct) {\n  const turningPoints = [prices[0]];\n  let direction = null;\n  let extreme = prices[0];\n  for (let i = 1; i < prices.length; i++) {\n    const priceNow = prices[i];\n    if (direction === null) {\n      const change = (priceNow - extreme) / extreme;\n      if (change >= reversalPct) {\n        direction = \"up\";\n        extreme = priceNow;\n        turningPoints.push(extreme);\n      } else if (change <= -reversalPct) {\n        direction = \"down\";\n        extreme = priceNow;\n        turningPoints.push(extreme);\n      }\n    } else if (direction === \"up\") {\n      if (priceNow > extreme) {\n        extreme = priceNow;\n        turningPoints[turningPoints.length - 1] = extreme;\n      } else if ((extreme - priceNow) / extreme >= reversalPct) {\n        direction = \"down\";\n        extreme = priceNow;\n        turningPoints.push(extreme);\n      }\n    } else {\n      if (priceNow < extreme) {\n        extreme = priceNow;\n        turningPoints[turningPoints.length - 1] = extreme;\n      } else if ((priceNow - extreme) / extreme >= reversalPct) {\n        direction = \"up\";\n        extreme = priceNow;\n        turningPoints.push(extreme);\n      }\n    }\n  }\n  return turningPoints;\n}\n\nconst turningPoints = buildKagiTurningPoints(closingPrices, REVERSAL_PCT);\n\n// Kagi thickness rule: a segment turns yang (thick) once price breaks above\n// the PREVIOUS shoulder (an earlier peak) and yin (thin) once it breaks below\n// the previous waist (an earlier trough) — thickness only flips on those\n// breakouts, not on every reversal. That's what lets a sustained trend render\n// as several consecutive same-thickness columns even while the path itself\n// keeps zig-zagging shoulder/waist to shoulder/waist.\nfunction classifyKagiThickness(points) {\n  const thickness = [];\n  let priorPeak = null;\n  let priorTrough = null;\n  let current = points[1] > points[0] ? \"yang\" : \"yin\";\n  for (let k = 1; k < points.length; k++) {\n    const isPeak = points[k] > points[k - 1];\n    if (isPeak) {\n      if (priorPeak !== null && points[k] > priorPeak) current = \"yang\";\n      priorPeak = points[k];\n    } else {\n      if (priorTrough !== null && points[k] < priorTrough) current = \"yin\";\n      priorTrough = points[k];\n    }\n    thickness.push(current);\n  }\n  return thickness;\n}\n\nconst segmentThickness = classifyKagiThickness(turningPoints);\n\n// The single largest turning-point move, called out with a reference-line\n// annotation so the chart's standout swing doesn't go unlabeled.\nlet breakoutIndex = 1;\nlet breakoutMove = turningPoints[1] - turningPoints[0];\nfor (let k = 2; k < turningPoints.length; k++) {\n  const move = turningPoints[k] - turningPoints[k - 1];\n  if (Math.abs(move) > Math.abs(breakoutMove)) {\n    breakoutMove = move;\n    breakoutIndex = k;\n  }\n}\nconst breakoutLabel = `${breakoutMove >= 0 ? \"+\" : \"-\"}$${Math.round(Math.abs(breakoutMove))} move`;\nconst startPrice = Math.round(closingPrices[0]);\n\n// Rectilinear Kagi path: each turn contributes a horizontal shoulder/waist\n// (connecting the previous column to the new one) followed by the vertical\n// line itself — the right-angle geometry a Kagi chart is drawn with. The\n// x-axis is the line index, not time, per the spec.\nconst vertices = [{ x: 0, y: turningPoints[0] }];\nfor (let k = 1; k < turningPoints.length; k++) {\n  vertices.push({ x: k, y: turningPoints[k - 1] });\n  vertices.push({ x: k, y: turningPoints[k] });\n}\nconst lineIndex = vertices.map((v) => v.x);\nconst priceMin = Math.min(...turningPoints);\nconst priceMax = Math.max(...turningPoints);\nconst pricePadding = (priceMax - priceMin) * 0.08;\n\n// Split the rectilinear path into two series by Kagi thickness so each can\n// carry its own line width (thick yang / thin yin). Null gaps keep unrelated\n// segments apart, while shared boundary vertices let a segment's series\n// also carry the turning point it starts or ends on, so the two colors\n// visually meet exactly at each reversal.\nconst yangPrice = new Array(vertices.length).fill(null);\nconst yinPrice = new Array(vertices.length).fill(null);\nfor (let k = 1; k < turningPoints.length; k++) {\n  const startIdx = 2 * (k - 1);\n  const midIdx = 2 * k - 1;\n  const endIdx = 2 * k;\n  const target = segmentThickness[k - 1] === \"yang\" ? yangPrice : yinPrice;\n  target[startIdx] = vertices[startIdx].y;\n  target[midIdx] = vertices[midIdx].y;\n  target[endIdx] = vertices[endIdx].y;\n}\n\n// --- Chart (default-exported component — the harness mounts it) -------------\nexport default function Chart() {\n  const { width, height } = window.ANYPLOT_SIZE;\n  const chartHeight = height - TITLE_HEIGHT;\n\n  return (\n    <Box sx={{ width, height, bgcolor: t.pageBg }}>\n      <Box sx={{ height: TITLE_HEIGHT, display: \"flex\", alignItems: \"center\", px: \"40px\" }}>\n        <Typography sx={{ color: t.ink, fontSize: \"22px\", fontWeight: 600, lineHeight: 1 }}>\n          {TITLE}\n        </Typography>\n      </Box>\n      <LineChart\n        width={width}\n        height={chartHeight}\n        skipAnimation\n        colors={[t.palette[0], t.palette[4]]}\n        grid={{ horizontal: true }}\n        xAxis={[\n          {\n            data: lineIndex,\n            scaleType: \"linear\",\n            label: \"Kagi Line Index\",\n            valueFormatter: (v) => Math.round(v).toString(),\n            tickLabelStyle: { fontSize: 14 },\n            labelStyle: { fontSize: 16 },\n            tickSize: 0,\n          },\n        ]}\n        yAxis={[\n          {\n            label: \"Closing Price ($)\",\n            min: priceMin - pricePadding,\n            max: priceMax + pricePadding,\n            valueFormatter: (v) => `$${Math.round(v)}`,\n            // ChartsYAxis: labelRefPoint.x = -(tickFontSize + tickSize + 10).\n            // Set it wide enough to clear the \"$277\"-style tick text, while\n            // tickLabelStyle.fontSize keeps the rendered tick size correct.\n            tickFontSize: 40,\n            tickLabelStyle: { fontSize: 14 },\n            labelStyle: { fontSize: 16 },\n            tickSize: 0,\n          },\n        ]}\n        series={[\n          {\n            id: \"yang\",\n            data: yangPrice,\n            label: \"Yang (uptrend)\",\n            curve: \"linear\",\n            showMark: false,\n          },\n          {\n            id: \"yin\",\n            data: yinPrice,\n            label: \"Yin (downtrend)\",\n            curve: \"linear\",\n            showMark: false,\n          },\n        ]}\n        margin={{ top: 24, bottom: 90, left: 130, right: 40 }}\n        sx={{\n          \"& .MuiLineElement-series-yang\": { strokeWidth: 6 },\n          \"& .MuiLineElement-series-yin\": { strokeWidth: 2 },\n          \"& .MuiChartsAxis-line\": { stroke: t.grid },\n          \"& .MuiChartsAxis-tick\": { stroke: t.grid },\n          \"& .MuiChartsGrid-line\": { stroke: t.grid, strokeWidth: 0.75 },\n          \"& .MuiChartsLegend-label\": { fontSize: \"15px\" },\n        }}\n        slotProps={{\n          legend: {\n            position: { vertical: \"bottom\", horizontal: \"middle\" },\n            itemMarkWidth: 24,\n            itemMarkHeight: 6,\n            padding: { top: 16 },\n          },\n        }}\n      >\n        <ChartsReferenceLine\n          y={startPrice}\n          label={`Start $${startPrice}`}\n          labelAlign=\"start\"\n          spacing={{ x: 8, y: 6 }}\n          lineStyle={{ stroke: t.inkSoft, strokeDasharray: \"4 4\", strokeWidth: 1 }}\n          labelStyle={{ fontSize: 12, fill: t.inkSoft }}\n        />\n        <ChartsReferenceLine\n          x={breakoutIndex}\n          label={breakoutLabel}\n          labelAlign=\"start\"\n          spacing={{ x: -96, y: 8 }}\n          lineStyle={{ stroke: t.inkSoft, strokeDasharray: \"4 4\", strokeWidth: 1 }}\n          labelStyle={{ fontSize: 13, fontWeight: 600, fill: t.ink }}\n        />\n      </LineChart>\n    </Box>\n  );\n}\n"}