Point and Figure Chart — Highcharts

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 Highcharts

Renders

JavaScript source (Highcharts)

// anyplot.ai
// point-and-figure-basic: Point and Figure Chart
// Library: highcharts 12.6.0 | JavaScript 22.23.2
// Quality: 95/100 | Created: 2026-09-02

const t = window.ANYPLOT_TOKENS;

// --- Data: synthetic daily closes over ~1 trading year (deterministic LCG) -
function lcg(seed) {
  let state = seed;
  return function rand() {
    state = (state * 1664525 + 1013904223) % 4294967296;
    return state / 4294967296;
  };
}
const rand = lcg(42);

// Short alternating-direction legs (strict sign flip each leg) so the box
// conversion below produces a genuine zigzag of many columns instead of one
// long trend per macro regime.
const NUM_LEGS = 20;
let dir = 1;
const segments = [];
for (let s = 0; s < NUM_LEGS; s++) {
  const days = 8 + Math.floor(rand() * 8); // 8-15 trading days per leg
  const magnitude = 0.7 + rand() * 0.9; // 0.7-1.6 drift per day
  segments.push({ days, drift: dir * magnitude, vol: 1.6 + rand() * 0.8 });
  dir *= -1;
}

let price = 148;
const closes = [];
segments.forEach(({ days, drift, vol }) => {
  for (let i = 0; i < days; i++) {
    price += drift + (rand() - 0.5) * vol;
    closes.push(Math.max(price, 5));
  }
});

// --- Point & figure box conversion (close-only, classic 1-box / 3-box rule) -
const BOX_SIZE = 2;
const REVERSAL_BOXES = 3;
const boxOf = (p) => Math.round(p / BOX_SIZE);

function buildColumns(prices) {
  const boxes = prices.map(boxOf);
  const columns = [];
  let direction = 0; // 0 undetermined, 1 = X column (rising), -1 = O column (falling)
  let currentBox = boxes[0];
  let column = { direction: 0, boxes: [currentBox] };

  for (let i = 1; i < boxes.length; i++) {
    const box = boxes[i];
    if (direction === 0) {
      if (box > currentBox) {
        for (let b = currentBox + 1; b <= box; b++) column.boxes.push(b);
        column.direction = 1;
        direction = 1;
        currentBox = box;
      } else if (box < currentBox) {
        for (let b = currentBox - 1; b >= box; b--) column.boxes.push(b);
        column.direction = -1;
        direction = -1;
        currentBox = box;
      }
    } else if (direction === 1) {
      if (box > currentBox) {
        for (let b = currentBox + 1; b <= box; b++) column.boxes.push(b);
        currentBox = box;
      } else if (box <= currentBox - REVERSAL_BOXES) {
        columns.push(column);
        column = { direction: -1, boxes: [] };
        for (let b = currentBox - 1; b >= box; b--) column.boxes.push(b);
        direction = -1;
        currentBox = box;
      }
    } else {
      if (box < currentBox) {
        for (let b = currentBox - 1; b >= box; b--) column.boxes.push(b);
        currentBox = box;
      } else if (box >= currentBox + REVERSAL_BOXES) {
        columns.push(column);
        column = { direction: 1, boxes: [] };
        for (let b = currentBox + 1; b <= box; b++) column.boxes.push(b);
        direction = 1;
        currentBox = box;
      }
    }
  }
  columns.push(column);
  return columns.filter((c) => c.boxes.length > 0);
}

const columns = buildColumns(closes);

// --- Series points: one scatter series per symbol; dataLabels ARE the glyph -
const xPoints = [];
const oPoints = [];
columns.forEach((col, colIndex) => {
  col.boxes.forEach((box) => {
    const point = [colIndex, box * BOX_SIZE];
    if (col.direction === 1) xPoints.push(point);
    else oPoints.push(point);
  });
});

// --- 45-degree support / resistance projections (1 box per column) ---------
let support = { col: 0, box: Infinity };
let resistance = { col: 0, box: -Infinity };
columns.forEach((col, colIndex) => {
  const low = Math.min(...col.boxes);
  const high = Math.max(...col.boxes);
  if (low < support.box) support = { col: colIndex, box: low };
  if (high > resistance.box) resistance = { col: colIndex, box: high };
});
const lastCol = columns.length - 1;
const span = Math.min(12, lastCol - Math.min(support.col, resistance.col));
const supportLine = Array.from({ length: span + 1 }, (_, i) => [support.col + i, (support.box + i) * BOX_SIZE]).filter(
  ([x]) => x <= lastCol,
);
const resistanceLine = Array.from({ length: span + 1 }, (_, i) => [
  resistance.col + i,
  (resistance.box - i) * BOX_SIZE,
]).filter(([x]) => x <= lastCol);

// --- Chart -------------------------------------------------------------------
// Semantic exception (default-style-guide.md): bullish/bearish reads as
// green/red to any trader, so X columns keep the brand green (palette
// position 1) and O columns use the matte-red semantic anchor rather than
// the next ordinal palette slot.
const RISING = t.palette[0];
const FALLING = "#AE3030";
const tickStep = Math.max(1, Math.ceil((lastCol + 1) / 15));

// --- Zebra row shading every other box: keeps dense columns (many stacked
// X/O glyphs at the box interval) reading as discrete symbols rather than a
// solid block, without shrinking the data-label font below VQ-01 comfort ---
const allBoxes = columns.flatMap((c) => c.boxes);
const minBoxAll = Math.min(...allBoxes);
const maxBoxAll = Math.max(...allBoxes);
const rowBands = [];
for (let b = minBoxAll; b <= maxBoxAll; b += 2) {
  rowBands.push({
    from: (b - 0.5) * BOX_SIZE,
    to: (b + 0.5) * BOX_SIZE,
    color: Highcharts.color(t.grid).setOpacity(0.15).get(),
  });
}

// --- Current reversal-zone highlight: shades the most recent column so the
// reader can spot the live reversal at a glance (Highcharts xAxis.plotBands,
// a core capability distinct from a plain scatter+dataLabels rendering) ----
const lastColumn = columns[lastCol];
const reversalZoneColor = Highcharts.color(lastColumn.direction === 1 ? RISING : FALLING)
  .setOpacity(0.1)
  .get();

// --- Price target via the classic vertical count method (spec Applications):
// target = extreme of the active column + column height x box size x
// reversal count, projected in the column's own direction --------------------
const lastBoxCount = lastColumn.boxes.length;
const priceTarget =
  lastColumn.direction === 1
    ? Math.min(...lastColumn.boxes) * BOX_SIZE + lastBoxCount * BOX_SIZE * REVERSAL_BOXES
    : Math.max(...lastColumn.boxes) * BOX_SIZE - lastBoxCount * BOX_SIZE * REVERSAL_BOXES;

Highcharts.chart("container", {
  chart: {
    type: "scatter",
    backgroundColor: "transparent",
    animation: false,
    style: { fontFamily: "inherit" },
  },
  credits: { enabled: false },
  title: {
    text: "point-and-figure-basic · javascript · highcharts · anyplot.ai",
    style: { color: t.ink, fontSize: "22px", fontWeight: "600" },
  },
  subtitle: {
    text: `Box size $${BOX_SIZE} · ${REVERSAL_BOXES}-box reversal`,
    style: { color: t.inkSoft, fontSize: "14px" },
  },
  xAxis: {
    title: {
      text: "Column (price reversals — not time)",
      style: { color: t.inkSoft, fontSize: "16px" },
    },
    min: -0.5,
    max: lastCol + 0.5,
    tickInterval: tickStep,
    gridLineWidth: 0,
    lineColor: t.inkSoft,
    tickColor: t.inkSoft,
    labels: { style: { color: t.inkSoft, fontSize: "14px" } },
    plotBands: [
      {
        from: lastCol - 0.5,
        to: lastCol + 0.5,
        color: reversalZoneColor,
        zIndex: 0,
      },
    ],
  },
  yAxis: {
    title: { text: "Price ($)", style: { color: t.inkSoft, fontSize: "16px" } },
    tickInterval: BOX_SIZE,
    gridLineColor: t.grid,
    lineColor: t.inkSoft,
    labels: { style: { color: t.inkSoft, fontSize: "14px" } },
    plotBands: rowBands,
    plotLines: [
      {
        value: priceTarget,
        color: t.amber,
        dashStyle: "ShortDot",
        width: 2,
        zIndex: 5,
        label: {
          text: `Price target (vertical count): $${priceTarget.toFixed(0)}`,
          align: "right",
          x: -6,
          style: { color: t.amber, fontSize: "12px", fontWeight: "600" },
        },
      },
    ],
  },
  legend: {
    itemStyle: { color: t.inkSoft, fontSize: "14px" },
    itemHoverStyle: { color: t.ink },
  },
  tooltip: {
    headerFormat: "",
    pointFormat: "Column {point.x} · ${point.y}",
  },
  plotOptions: {
    series: { animation: false },
    scatter: {
      marker: { enabled: false },
      dataLabels: {
        enabled: true,
        allowOverlap: true,
        verticalAlign: "middle",
        align: "center",
        y: 1,
        padding: 0,
        style: { fontWeight: "700", fontSize: "14px", textOutline: "none" },
      },
    },
  },
  series: [
    {
      name: "Rising (X)",
      type: "scatter",
      data: xPoints,
      color: RISING,
      dataLabels: { format: "X", style: { color: RISING } },
    },
    {
      name: "Falling (O)",
      type: "scatter",
      data: oPoints,
      color: FALLING,
      dataLabels: { format: "O", style: { color: FALLING } },
    },
    {
      name: "Support (45°)",
      type: "line",
      data: supportLine,
      color: t.inkSoft,
      dashStyle: "Dash",
      lineWidth: 2,
      marker: { enabled: false },
      enableMouseTracking: false,
      showInLegend: false,
    },
    {
      name: "Resistance (45°)",
      type: "line",
      data: resistanceLine,
      color: t.inkSoft,
      dashStyle: "Dash",
      lineWidth: 2,
      marker: { enabled: false },
      enableMouseTracking: false,
      showInLegend: false,
    },
  ],
});

Retrieve this implementation

Runnable source as JSON, for any HTTP client: https://api.anyplot.ai/specs/point-and-figure-basic/highcharts/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": "highcharts",
  "page": "https://anyplot.ai/point-and-figure-basic/javascript/highcharts",
  "hub": "https://anyplot.ai/point-and-figure-basic",
  "code_json": "https://api.anyplot.ai/specs/point-and-figure-basic/highcharts/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/highcharts/plot-light.png",
  "render_dark_png": "https://storage.googleapis.com/anyplot-images/plots/point-and-figure-basic/javascript/highcharts/plot-dark.png",
  "interactive_light_html": "https://storage.googleapis.com/anyplot-images/plots/point-and-figure-basic/javascript/highcharts/plot-light.html",
  "interactive_dark_html": "https://storage.googleapis.com/anyplot-images/plots/point-and-figure-basic/javascript/highcharts/plot-dark.html",
  "quality_score": 95.0,
  "license": "MIT",
  "guide": "https://anyplot.ai/llms.txt"
}

Part of Point and Figure Chart on anyplot.ai.

Other implementations