{"spec_id":"point-and-figure-basic","library":"muix","language":"javascript","code":"// anyplot.ai\n// point-and-figure-basic: Point and Figure Chart\n// Library: muix 7.29.1 | JavaScript 22.23.2\n// Quality: 89/100 | Created: 2026-09-02\nimport { ChartContainer } from \"@mui/x-charts/ChartContainer\";\nimport { ChartsXAxis } from \"@mui/x-charts/ChartsXAxis\";\nimport { ChartsYAxis } from \"@mui/x-charts/ChartsYAxis\";\nimport { useXScale, useYScale } from \"@mui/x-charts/hooks\";\nimport { Box, Typography } from \"@mui/material\";\n\nconst t = window.ANYPLOT_TOKENS;\nconst TITLE = \"point-and-figure-basic · javascript · muix · anyplot.ai\";\n\n// --- Deterministic daily-close series (small LCG PRNG — no seeded Math.random\n// in the browser) standing in for ~1.5 years of a mid-cap stock's closes. ----\nfunction makeLcg(seed) {\n  let state = seed >>> 0;\n  return () => {\n    state = (Math.imul(state, 1664525) + 1013904223) >>> 0;\n    return state / 4294967296;\n  };\n}\n\nconst NUM_SESSIONS = 360;\nconst rand = makeLcg(42);\nconst closes = [];\nlet price = 148;\nfor (let session = 0; session < NUM_SESSIONS; session += 1) {\n  price = Math.max(60, price + 0.01 + (rand() - 0.5) * 4.3);\n  closes.push(Math.round(price * 100) / 100);\n}\n\n// --- Point & Figure column construction (classic close-only method) --------\n// A box is added whenever price moves a full BOX_SIZE from the last plotted\n// box. A new column only opens once price reverses by REVERSAL_BOXES boxes —\n// this is what strips time and minor noise out of the chart, leaving only\n// columns of X (rising) and O (falling) boxes.\nconst BOX_SIZE = 1.5; // $ per box\nconst REVERSAL_BOXES = 3; // boxes required to reverse column direction\n\nfunction buildColumns(prices, boxSize, reversalBoxes) {\n  let boxIndex = Math.round(prices[0] / boxSize);\n  let direction = null;\n  let boxesInColumn = [boxIndex];\n  const columns = [];\n\n  for (let i = 1; i < prices.length; i += 1) {\n    const nextBox = Math.round(prices[i] / boxSize);\n    if (direction === null) {\n      if (nextBox > boxIndex) {\n        direction = \"X\";\n        for (let b = boxIndex + 1; b <= nextBox; b += 1) boxesInColumn.push(b);\n        boxIndex = nextBox;\n      } else if (nextBox < boxIndex) {\n        direction = \"O\";\n        for (let b = boxIndex - 1; b >= nextBox; b -= 1) boxesInColumn.push(b);\n        boxIndex = nextBox;\n      }\n      continue;\n    }\n    if (direction === \"X\") {\n      if (nextBox > boxIndex) {\n        for (let b = boxIndex + 1; b <= nextBox; b += 1) boxesInColumn.push(b);\n        boxIndex = nextBox;\n      } else if (nextBox <= boxIndex - reversalBoxes) {\n        columns.push({ direction, boxes: boxesInColumn });\n        direction = \"O\";\n        boxesInColumn = [];\n        for (let b = boxIndex - 1; b >= nextBox; b -= 1) boxesInColumn.push(b);\n        boxIndex = nextBox;\n      }\n    } else {\n      if (nextBox < boxIndex) {\n        for (let b = boxIndex - 1; b >= nextBox; b -= 1) boxesInColumn.push(b);\n        boxIndex = nextBox;\n      } else if (nextBox >= boxIndex + reversalBoxes) {\n        columns.push({ direction, boxes: boxesInColumn });\n        direction = \"X\";\n        boxesInColumn = [];\n        for (let b = boxIndex + 1; b <= nextBox; b += 1) boxesInColumn.push(b);\n        boxIndex = nextBox;\n      }\n    }\n  }\n  columns.push({ direction: direction ?? \"X\", boxes: boxesInColumn });\n  return columns;\n}\n\nconst columns = buildColumns(closes, BOX_SIZE, REVERSAL_BOXES);\nconst allBoxes = columns.flatMap((column) => column.boxes);\nconst minBox = Math.min(...allBoxes);\nconst maxBox = Math.max(...allBoxes);\n\n// --- 45-degree support/resistance trend lines (classic P&F construction) ---\n// Support: anchored on the low of a bullish reversal column (an X column that\n// immediately follows an O column), extended up-right at +1 box per column\n// until a later O column's low breaks below the line.\n// Resistance: anchored on the high of a bearish reversal column (an O column\n// that immediately follows an X column), extended down-right at -1 box per\n// column until a later X column's high breaks above the line.\nconst MIN_TREND_SPAN = 2; // columns — drop trivially short lines to limit clutter\nfunction buildTrendLines(cols) {\n  const supports = [];\n  const resistances = [];\n  for (let i = 1; i < cols.length; i += 1) {\n    const column = cols[i];\n    const prevColumn = cols[i - 1];\n    if (column.direction === \"X\" && prevColumn.direction === \"O\") {\n      const startBox = Math.min(...column.boxes);\n      let endIndex = i;\n      for (let j = i + 1; j < cols.length; j += 1) {\n        const projected = startBox + (j - i);\n        if (cols[j].direction === \"O\" && Math.min(...cols[j].boxes) < projected) break;\n        endIndex = j;\n      }\n      if (endIndex - i >= MIN_TREND_SPAN) {\n        supports.push({ startIndex: i, startBox, endIndex, endBox: startBox + (endIndex - i) });\n      }\n    }\n    if (column.direction === \"O\" && prevColumn.direction === \"X\") {\n      const startBox = Math.max(...column.boxes);\n      let endIndex = i;\n      for (let j = i + 1; j < cols.length; j += 1) {\n        const projected = startBox - (j - i);\n        if (cols[j].direction === \"X\" && Math.max(...cols[j].boxes) > projected) break;\n        endIndex = j;\n      }\n      if (endIndex - i >= MIN_TREND_SPAN) {\n        resistances.push({ startIndex: i, startBox, endIndex, endBox: startBox - (endIndex - i) });\n      }\n    }\n  }\n  return { supports, resistances };\n}\n\nconst { supports: supportLines, resistances: resistanceLines } = buildTrendLines(columns);\n\n// Finance semantic exception: rising (X) columns read as green/bullish,\n// falling (O) columns as red/bearish — not the plain ordinal 1st/2nd slots.\nconst RISING = t.palette[0]; // brand green — bullish X columns\nconst FALLING = t.palette[4]; // matte red (semantic loss anchor) — bearish O columns\n\n// --- Chart (default-exported component — the harness mounts it) -----------\nexport default function Chart() {\n  const size = window.ANYPLOT_SIZE;\n  const padding = { top: 28, right: 44, bottom: 20, left: 16 };\n  const headerHeight = 56;\n  const chartWidth = size.width - padding.left - padding.right;\n  const chartHeight = size.height - padding.top - padding.bottom - headerHeight;\n\n  return (\n    <Box\n      sx={{\n        width: size.width,\n        height: size.height,\n        boxSizing: \"border-box\",\n        padding: `${padding.top}px ${padding.right}px ${padding.bottom}px ${padding.left}px`,\n        display: \"flex\",\n        flexDirection: \"column\",\n      }}\n    >\n      <Box sx={{ display: \"flex\", alignItems: \"baseline\", justifyContent: \"space-between\", mb: \"18px\" }}>\n        <Typography sx={{ fontSize: 22, fontWeight: 600, color: \"text.primary\", lineHeight: 1 }}>\n          {TITLE}\n        </Typography>\n        <Box sx={{ display: \"flex\", alignItems: \"center\", gap: \"20px\" }}>\n          <Box sx={{ display: \"flex\", alignItems: \"center\", gap: \"6px\" }}>\n            <Typography sx={{ fontSize: 16, fontWeight: 700, color: RISING, lineHeight: 1 }}>X</Typography>\n            <Typography sx={{ fontSize: 14, color: \"text.secondary\", lineHeight: 1 }}>Rising column</Typography>\n          </Box>\n          <Box sx={{ display: \"flex\", alignItems: \"center\", gap: \"6px\" }}>\n            <Typography sx={{ fontSize: 16, fontWeight: 700, color: FALLING, lineHeight: 1 }}>O</Typography>\n            <Typography sx={{ fontSize: 14, color: \"text.secondary\", lineHeight: 1 }}>Falling column</Typography>\n          </Box>\n          <Box sx={{ display: \"flex\", alignItems: \"center\", gap: \"6px\" }}>\n            <svg width=\"20\" height=\"14\" aria-hidden=\"true\">\n              <line x1=\"2\" y1=\"12\" x2=\"18\" y2=\"2\" stroke={RISING} strokeWidth={2} strokeDasharray=\"4 3\" strokeOpacity={0.7} />\n            </svg>\n            <Typography sx={{ fontSize: 14, color: \"text.secondary\", lineHeight: 1 }}>Support</Typography>\n          </Box>\n          <Box sx={{ display: \"flex\", alignItems: \"center\", gap: \"6px\" }}>\n            <svg width=\"20\" height=\"14\" aria-hidden=\"true\">\n              <line x1=\"2\" y1=\"2\" x2=\"18\" y2=\"12\" stroke={FALLING} strokeWidth={2} strokeDasharray=\"4 3\" strokeOpacity={0.7} />\n            </svg>\n            <Typography sx={{ fontSize: 14, color: \"text.secondary\", lineHeight: 1 }}>Resistance</Typography>\n          </Box>\n        </Box>\n      </Box>\n      <ChartContainer\n        width={chartWidth}\n        height={chartHeight}\n        series={[]}\n        skipAnimation\n        margin={{ top: 8, right: 12, bottom: 40, left: 60 }}\n        xAxis={[\n          {\n            id: \"columns\",\n            scaleType: \"band\",\n            data: columns.map((_, index) => index + 1),\n            categoryGapRatio: 0.12,\n            label: \"Column (price reversal, not time)\",\n            labelStyle: { fontSize: 15 },\n            tickLabelStyle: { fontSize: 13 },\n            tickLabelInterval: (_value, index) => index % 5 === 0,\n          },\n        ]}\n        yAxis={[\n          {\n            id: \"price\",\n            scaleType: \"linear\",\n            min: minBox - 2,\n            max: maxBox + 2,\n            label: \"Price ($)\",\n            labelStyle: { fontSize: 15 },\n            valueFormatter: (boxValue) => `$${(boxValue * BOX_SIZE).toFixed(1)}`,\n            tickLabelStyle: { fontSize: 13 },\n            tickNumber: 10,\n          },\n        ]}\n      >\n        <PriceGrid minBox={minBox} maxBox={maxBox} />\n        <TrendLines supports={supportLines} resistances={resistanceLines} />\n        <PfMarks columns={columns} />\n        <ChartsXAxis axisId=\"columns\" />\n        <ChartsYAxis axisId=\"price\" />\n      </ChartContainer>\n    </Box>\n  );\n}\n\n// Horizontal reference lines at round box-size price intervals, per the spec\n// note to scale the Y-axis with grid lines at box-size intervals.\nfunction PriceGrid({ minBox, maxBox }) {\n  const xScale = useXScale();\n  const yScale = useYScale();\n  const [left, right] = xScale.range();\n  const step = Math.max(1, Math.round((maxBox - minBox) / 9));\n  const rows = [];\n  for (let box = Math.ceil(minBox / step) * step; box <= maxBox; box += step) rows.push(box);\n\n  return (\n    <g>\n      {rows.map((box) => (\n        <line key={box} x1={left} x2={right} y1={yScale(box)} y2={yScale(box)} stroke={t.grid} strokeWidth={1} />\n      ))}\n    </g>\n  );\n}\n\n// Diagonal 45-degree support/resistance reference lines (spec-required), drawn\n// as a subtle dashed overlay so they read as trend guides rather than data.\nfunction TrendLines({ supports, resistances }) {\n  const xScale = useXScale();\n  const yScale = useYScale();\n  const half = xScale.bandwidth() / 2;\n  const toPoint = (index, box) => ({ x: xScale(index + 1) + half, y: yScale(box) });\n\n  return (\n    <g>\n      {supports.map((line, index) => {\n        const start = toPoint(line.startIndex, line.startBox);\n        const end = toPoint(line.endIndex, line.endBox);\n        return (\n          <line\n            key={`support-${index}`}\n            x1={start.x}\n            y1={start.y}\n            x2={end.x}\n            y2={end.y}\n            stroke={RISING}\n            strokeWidth={1.5}\n            strokeDasharray=\"7 4\"\n            strokeOpacity={0.55}\n          />\n        );\n      })}\n      {resistances.map((line, index) => {\n        const start = toPoint(line.startIndex, line.startBox);\n        const end = toPoint(line.endIndex, line.endBox);\n        return (\n          <line\n            key={`resistance-${index}`}\n            x1={start.x}\n            y1={start.y}\n            x2={end.x}\n            y2={end.y}\n            stroke={FALLING}\n            strokeWidth={1.5}\n            strokeDasharray=\"7 4\"\n            strokeOpacity={0.55}\n          />\n        );\n      })}\n    </g>\n  );\n}\n\n// The X/O glyphs themselves — one per box, stacked inside each column. MUI X\n// has no native P&F series type, so the marks are placed directly with the\n// chart's own band/linear scales (the same scales ChartsXAxis/ChartsYAxis use).\nfunction PfMarks({ columns: pfColumns }) {\n  const xScale = useXScale();\n  const yScale = useYScale();\n  const half = xScale.bandwidth() / 2;\n  const rowHeight = Math.abs(yScale(1) - yScale(0));\n  const fontSize = Math.max(10, Math.min(xScale.bandwidth() * 0.6, rowHeight * 0.8, 22));\n\n  return (\n    <g style={{ fontWeight: 700 }}>\n      {pfColumns.map((column, columnIndex) => {\n        const cx = xScale(columnIndex + 1) + half;\n        const color = column.direction === \"X\" ? RISING : FALLING;\n        return column.boxes.map((box) => (\n          <text\n            key={`${columnIndex}-${box}`}\n            x={cx}\n            y={yScale(box)}\n            fontSize={fontSize}\n            fill={color}\n            textAnchor=\"middle\"\n            dominantBaseline=\"central\"\n          >\n            {column.direction}\n          </text>\n        ));\n      })}\n    </g>\n  );\n}\n"}