Point and Figure Chart — MUI X Charts

A Point and Figure (P&F) chart is a price-action focused visualization that uses columns of X's (rising prices) and O's (falling prices) to display significant price movements while filtering out time and minor fluctuations. Unlike traditional time-based charts, P&F charts only plot a new symbol when price moves by a defined box size, and only start a new column when price reverses by a specified number of boxes (typically 3). This makes it ideal for identifying clear trends, support/resistance levels, and generating trading signals.

Point and Figure Chart rendered with MUI X Charts

Renders

JavaScript source (MUI X Charts)

// anyplot.ai
// point-and-figure-basic: Point and Figure Chart
// Library: muix 7.29.1 | JavaScript 22.23.2
// Quality: 89/100 | Created: 2026-09-02
import { ChartContainer } from "@mui/x-charts/ChartContainer";
import { ChartsXAxis } from "@mui/x-charts/ChartsXAxis";
import { ChartsYAxis } from "@mui/x-charts/ChartsYAxis";
import { useXScale, useYScale } from "@mui/x-charts/hooks";
import { Box, Typography } from "@mui/material";

const t = window.ANYPLOT_TOKENS;
const TITLE = "point-and-figure-basic · javascript · muix · anyplot.ai";

// --- Deterministic daily-close series (small LCG PRNG — no seeded Math.random
// in the browser) standing in for ~1.5 years of a mid-cap stock's closes. ----
function makeLcg(seed) {
  let state = seed >>> 0;
  return () => {
    state = (Math.imul(state, 1664525) + 1013904223) >>> 0;
    return state / 4294967296;
  };
}

const NUM_SESSIONS = 360;
const rand = makeLcg(42);
const closes = [];
let price = 148;
for (let session = 0; session < NUM_SESSIONS; session += 1) {
  price = Math.max(60, price + 0.01 + (rand() - 0.5) * 4.3);
  closes.push(Math.round(price * 100) / 100);
}

// --- Point & Figure column construction (classic close-only method) --------
// A box is added whenever price moves a full BOX_SIZE from the last plotted
// box. A new column only opens once price reverses by REVERSAL_BOXES boxes —
// this is what strips time and minor noise out of the chart, leaving only
// columns of X (rising) and O (falling) boxes.
const BOX_SIZE = 1.5; // $ per box
const REVERSAL_BOXES = 3; // boxes required to reverse column direction

function buildColumns(prices, boxSize, reversalBoxes) {
  let boxIndex = Math.round(prices[0] / boxSize);
  let direction = null;
  let boxesInColumn = [boxIndex];
  const columns = [];

  for (let i = 1; i < prices.length; i += 1) {
    const nextBox = Math.round(prices[i] / boxSize);
    if (direction === null) {
      if (nextBox > boxIndex) {
        direction = "X";
        for (let b = boxIndex + 1; b <= nextBox; b += 1) boxesInColumn.push(b);
        boxIndex = nextBox;
      } else if (nextBox < boxIndex) {
        direction = "O";
        for (let b = boxIndex - 1; b >= nextBox; b -= 1) boxesInColumn.push(b);
        boxIndex = nextBox;
      }
      continue;
    }
    if (direction === "X") {
      if (nextBox > boxIndex) {
        for (let b = boxIndex + 1; b <= nextBox; b += 1) boxesInColumn.push(b);
        boxIndex = nextBox;
      } else if (nextBox <= boxIndex - reversalBoxes) {
        columns.push({ direction, boxes: boxesInColumn });
        direction = "O";
        boxesInColumn = [];
        for (let b = boxIndex - 1; b >= nextBox; b -= 1) boxesInColumn.push(b);
        boxIndex = nextBox;
      }
    } else {
      if (nextBox < boxIndex) {
        for (let b = boxIndex - 1; b >= nextBox; b -= 1) boxesInColumn.push(b);
        boxIndex = nextBox;
      } else if (nextBox >= boxIndex + reversalBoxes) {
        columns.push({ direction, boxes: boxesInColumn });
        direction = "X";
        boxesInColumn = [];
        for (let b = boxIndex + 1; b <= nextBox; b += 1) boxesInColumn.push(b);
        boxIndex = nextBox;
      }
    }
  }
  columns.push({ direction: direction ?? "X", boxes: boxesInColumn });
  return columns;
}

const columns = buildColumns(closes, BOX_SIZE, REVERSAL_BOXES);
const allBoxes = columns.flatMap((column) => column.boxes);
const minBox = Math.min(...allBoxes);
const maxBox = Math.max(...allBoxes);

// --- 45-degree support/resistance trend lines (classic P&F construction) ---
// Support: anchored on the low of a bullish reversal column (an X column that
// immediately follows an O column), extended up-right at +1 box per column
// until a later O column's low breaks below the line.
// Resistance: anchored on the high of a bearish reversal column (an O column
// that immediately follows an X column), extended down-right at -1 box per
// column until a later X column's high breaks above the line.
const MIN_TREND_SPAN = 2; // columns — drop trivially short lines to limit clutter
function buildTrendLines(cols) {
  const supports = [];
  const resistances = [];
  for (let i = 1; i < cols.length; i += 1) {
    const column = cols[i];
    const prevColumn = cols[i - 1];
    if (column.direction === "X" && prevColumn.direction === "O") {
      const startBox = Math.min(...column.boxes);
      let endIndex = i;
      for (let j = i + 1; j < cols.length; j += 1) {
        const projected = startBox + (j - i);
        if (cols[j].direction === "O" && Math.min(...cols[j].boxes) < projected) break;
        endIndex = j;
      }
      if (endIndex - i >= MIN_TREND_SPAN) {
        supports.push({ startIndex: i, startBox, endIndex, endBox: startBox + (endIndex - i) });
      }
    }
    if (column.direction === "O" && prevColumn.direction === "X") {
      const startBox = Math.max(...column.boxes);
      let endIndex = i;
      for (let j = i + 1; j < cols.length; j += 1) {
        const projected = startBox - (j - i);
        if (cols[j].direction === "X" && Math.max(...cols[j].boxes) > projected) break;
        endIndex = j;
      }
      if (endIndex - i >= MIN_TREND_SPAN) {
        resistances.push({ startIndex: i, startBox, endIndex, endBox: startBox - (endIndex - i) });
      }
    }
  }
  return { supports, resistances };
}

const { supports: supportLines, resistances: resistanceLines } = buildTrendLines(columns);

// Finance semantic exception: rising (X) columns read as green/bullish,
// falling (O) columns as red/bearish — not the plain ordinal 1st/2nd slots.
const RISING = t.palette[0]; // brand green — bullish X columns
const FALLING = t.palette[4]; // matte red (semantic loss anchor) — bearish O columns

// --- Chart (default-exported component — the harness mounts it) -----------
export default function Chart() {
  const size = window.ANYPLOT_SIZE;
  const padding = { top: 28, right: 44, bottom: 20, left: 16 };
  const headerHeight = 56;
  const chartWidth = size.width - padding.left - padding.right;
  const chartHeight = size.height - padding.top - padding.bottom - headerHeight;

  return (
    <Box
      sx={{
        width: size.width,
        height: size.height,
        boxSizing: "border-box",
        padding: `${padding.top}px ${padding.right}px ${padding.bottom}px ${padding.left}px`,
        display: "flex",
        flexDirection: "column",
      }}
    >
      <Box sx={{ display: "flex", alignItems: "baseline", justifyContent: "space-between", mb: "18px" }}>
        <Typography sx={{ fontSize: 22, fontWeight: 600, color: "text.primary", lineHeight: 1 }}>
          {TITLE}
        </Typography>
        <Box sx={{ display: "flex", alignItems: "center", gap: "20px" }}>
          <Box sx={{ display: "flex", alignItems: "center", gap: "6px" }}>
            <Typography sx={{ fontSize: 16, fontWeight: 700, color: RISING, lineHeight: 1 }}>X</Typography>
            <Typography sx={{ fontSize: 14, color: "text.secondary", lineHeight: 1 }}>Rising column</Typography>
          </Box>
          <Box sx={{ display: "flex", alignItems: "center", gap: "6px" }}>
            <Typography sx={{ fontSize: 16, fontWeight: 700, color: FALLING, lineHeight: 1 }}>O</Typography>
            <Typography sx={{ fontSize: 14, color: "text.secondary", lineHeight: 1 }}>Falling column</Typography>
          </Box>
          <Box sx={{ display: "flex", alignItems: "center", gap: "6px" }}>
            <svg width="20" height="14" aria-hidden="true">
              <line x1="2" y1="12" x2="18" y2="2" stroke={RISING} strokeWidth={2} strokeDasharray="4 3" strokeOpacity={0.7} />
            </svg>
            <Typography sx={{ fontSize: 14, color: "text.secondary", lineHeight: 1 }}>Support</Typography>
          </Box>
          <Box sx={{ display: "flex", alignItems: "center", gap: "6px" }}>
            <svg width="20" height="14" aria-hidden="true">
              <line x1="2" y1="2" x2="18" y2="12" stroke={FALLING} strokeWidth={2} strokeDasharray="4 3" strokeOpacity={0.7} />
            </svg>
            <Typography sx={{ fontSize: 14, color: "text.secondary", lineHeight: 1 }}>Resistance</Typography>
          </Box>
        </Box>
      </Box>
      <ChartContainer
        width={chartWidth}
        height={chartHeight}
        series={[]}
        skipAnimation
        margin={{ top: 8, right: 12, bottom: 40, left: 60 }}
        xAxis={[
          {
            id: "columns",
            scaleType: "band",
            data: columns.map((_, index) => index + 1),
            categoryGapRatio: 0.12,
            label: "Column (price reversal, not time)",
            labelStyle: { fontSize: 15 },
            tickLabelStyle: { fontSize: 13 },
            tickLabelInterval: (_value, index) => index % 5 === 0,
          },
        ]}
        yAxis={[
          {
            id: "price",
            scaleType: "linear",
            min: minBox - 2,
            max: maxBox + 2,
            label: "Price ($)",
            labelStyle: { fontSize: 15 },
            valueFormatter: (boxValue) => `$${(boxValue * BOX_SIZE).toFixed(1)}`,
            tickLabelStyle: { fontSize: 13 },
            tickNumber: 10,
          },
        ]}
      >
        <PriceGrid minBox={minBox} maxBox={maxBox} />
        <TrendLines supports={supportLines} resistances={resistanceLines} />
        <PfMarks columns={columns} />
        <ChartsXAxis axisId="columns" />
        <ChartsYAxis axisId="price" />
      </ChartContainer>
    </Box>
  );
}

// Horizontal reference lines at round box-size price intervals, per the spec
// note to scale the Y-axis with grid lines at box-size intervals.
function PriceGrid({ minBox, maxBox }) {
  const xScale = useXScale();
  const yScale = useYScale();
  const [left, right] = xScale.range();
  const step = Math.max(1, Math.round((maxBox - minBox) / 9));
  const rows = [];
  for (let box = Math.ceil(minBox / step) * step; box <= maxBox; box += step) rows.push(box);

  return (
    <g>
      {rows.map((box) => (
        <line key={box} x1={left} x2={right} y1={yScale(box)} y2={yScale(box)} stroke={t.grid} strokeWidth={1} />
      ))}
    </g>
  );
}

// Diagonal 45-degree support/resistance reference lines (spec-required), drawn
// as a subtle dashed overlay so they read as trend guides rather than data.
function TrendLines({ supports, resistances }) {
  const xScale = useXScale();
  const yScale = useYScale();
  const half = xScale.bandwidth() / 2;
  const toPoint = (index, box) => ({ x: xScale(index + 1) + half, y: yScale(box) });

  return (
    <g>
      {supports.map((line, index) => {
        const start = toPoint(line.startIndex, line.startBox);
        const end = toPoint(line.endIndex, line.endBox);
        return (
          <line
            key={`support-${index}`}
            x1={start.x}
            y1={start.y}
            x2={end.x}
            y2={end.y}
            stroke={RISING}
            strokeWidth={1.5}
            strokeDasharray="7 4"
            strokeOpacity={0.55}
          />
        );
      })}
      {resistances.map((line, index) => {
        const start = toPoint(line.startIndex, line.startBox);
        const end = toPoint(line.endIndex, line.endBox);
        return (
          <line
            key={`resistance-${index}`}
            x1={start.x}
            y1={start.y}
            x2={end.x}
            y2={end.y}
            stroke={FALLING}
            strokeWidth={1.5}
            strokeDasharray="7 4"
            strokeOpacity={0.55}
          />
        );
      })}
    </g>
  );
}

// The X/O glyphs themselves — one per box, stacked inside each column. MUI X
// has no native P&F series type, so the marks are placed directly with the
// chart's own band/linear scales (the same scales ChartsXAxis/ChartsYAxis use).
function PfMarks({ columns: pfColumns }) {
  const xScale = useXScale();
  const yScale = useYScale();
  const half = xScale.bandwidth() / 2;
  const rowHeight = Math.abs(yScale(1) - yScale(0));
  const fontSize = Math.max(10, Math.min(xScale.bandwidth() * 0.6, rowHeight * 0.8, 22));

  return (
    <g style={{ fontWeight: 700 }}>
      {pfColumns.map((column, columnIndex) => {
        const cx = xScale(columnIndex + 1) + half;
        const color = column.direction === "X" ? RISING : FALLING;
        return column.boxes.map((box) => (
          <text
            key={`${columnIndex}-${box}`}
            x={cx}
            y={yScale(box)}
            fontSize={fontSize}
            fill={color}
            textAnchor="middle"
            dominantBaseline="central"
          >
            {column.direction}
          </text>
        ));
      })}
    </g>
  );
}

Retrieve this implementation

Runnable source as JSON, for any HTTP client: https://api.anyplot.ai/specs/point-and-figure-basic/muix/code. Any spec id and library id listed in llms-full.txt fit the same URL shape; every URL below is complete and callable.

{
  "spec_id": "point-and-figure-basic",
  "language": "javascript",
  "library": "muix",
  "page": "https://anyplot.ai/point-and-figure-basic/javascript/muix",
  "hub": "https://anyplot.ai/point-and-figure-basic",
  "code_json": "https://api.anyplot.ai/specs/point-and-figure-basic/muix/code",
  "spec_json": "https://api.anyplot.ai/specs/point-and-figure-basic",
  "render_light_png": "https://storage.googleapis.com/anyplot-images/plots/point-and-figure-basic/javascript/muix/plot-light.png",
  "render_dark_png": "https://storage.googleapis.com/anyplot-images/plots/point-and-figure-basic/javascript/muix/plot-dark.png",
  "interactive_light_html": "https://storage.googleapis.com/anyplot-images/plots/point-and-figure-basic/javascript/muix/plot-light.html",
  "interactive_dark_html": "https://storage.googleapis.com/anyplot-images/plots/point-and-figure-basic/javascript/muix/plot-dark.html",
  "quality_score": 89.0,
  "license": "MIT",
  "guide": "https://anyplot.ai/llms.txt"
}

Part of Point and Figure Chart on anyplot.ai.

Other implementations