Point and Figure Chart — Apache ECharts

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 Apache ECharts

Renders

JavaScript source (Apache ECharts)

// anyplot.ai
// point-and-figure-basic: Point and Figure Chart
// Library: echarts 6.1.0 | JavaScript 22.23.2
// Quality: 91/100 | Created: 2026-09-02

const t = window.ANYPLOT_TOKENS;

// --- Data: simulated daily closes for a mid-cap tech stock -----------------
// Deterministic LCG (no network, no Math.random) driving a regime-switching
// random walk so the price alternates between multi-week up/down swings.
function makeLcg(seed) {
  let state = seed >>> 0;
  return () => {
    state = (state * 1664525 + 1013904223) >>> 0;
    return state / 4294967296;
  };
}
const rnd = makeLcg(391);

const CLOSE_COUNT = 280;
const closes = [];
let price = 100;
let regime = 1;
let regimeDaysLeft = 0;
for (let i = 0; i < CLOSE_COUNT; i++) {
  regimeDaysLeft -= 1;
  if (regimeDaysLeft <= 0) {
    regime = (rnd() - 0.5) * 2;
    regimeDaysLeft = 8 + Math.floor(rnd() * 12);
  }
  price += regime * 0.35 + (rnd() - 0.5) * 3.2;
  price = Math.max(20, price);
  closes.push(Math.round(price * 100) / 100);
}

// --- Point-and-figure column construction -----------------------------------
// Box size: $2 per box. Reversal: 3 boxes required to start a new column.
const BOX_SIZE = 2;
const REVERSAL = 3;
const priceToBox = (p) => Math.round(p / BOX_SIZE);

const columns = [];
let direction = null; // "up" | "down"
let boxes = [priceToBox(closes[0])];
for (let i = 1; i < closes.length; i++) {
  const box = priceToBox(closes[i]);
  const lastBox = boxes[boxes.length - 1];
  if (direction === null) {
    if (box > lastBox) {
      for (let b = lastBox + 1; b <= box; b++) boxes.push(b);
      direction = "up";
    } else if (box < lastBox) {
      for (let b = lastBox - 1; b >= box; b--) boxes.push(b);
      direction = "down";
    }
  } else if (direction === "up") {
    if (box > lastBox) {
      for (let b = lastBox + 1; b <= box; b++) boxes.push(b);
    } else if (box <= lastBox - REVERSAL) {
      columns.push({ direction, boxes });
      const start = lastBox - 1;
      boxes = [];
      for (let b = start; b >= box; b--) boxes.push(b);
      direction = "down";
    }
  } else {
    if (box < lastBox) {
      for (let b = lastBox - 1; b >= box; b--) boxes.push(b);
    } else if (box >= lastBox + REVERSAL) {
      columns.push({ direction, boxes });
      const start = lastBox + 1;
      boxes = [];
      for (let b = start; b <= box; b++) boxes.push(b);
      direction = "up";
    }
  }
}
columns.push({ direction, boxes });

// --- Chart-ready points -------------------------------------------------
const risingPoints = []; // X — rising columns
const fallingPoints = []; // O — falling columns
let globalMinBox = Infinity;
let globalMaxBox = -Infinity;
let bottomColumn = 0;
let topColumn = 0;
// Category-axis x-values must match the category strings exactly — a plain
// number is treated as a 0-based category *index*, which silently drops the
// last column and shifts every other column by one.
columns.forEach((col, colIndex) => {
  const colNumber = colIndex + 1;
  const target = col.direction === "up" ? risingPoints : fallingPoints;
  col.boxes.forEach((b) => {
    target.push([String(colNumber), b * BOX_SIZE]);
    if (b < globalMinBox) {
      globalMinBox = b;
      bottomColumn = colNumber;
    }
    if (b > globalMaxBox) {
      globalMaxBox = b;
      topColumn = colNumber;
    }
  });
});

// Bullish support line: a classic P&F trend line anchored on the deepest
// reversal low, rising at the canonical 45-degree rate of one box per column.
const supportStart = [String(bottomColumn), globalMinBox * BOX_SIZE];
const supportEnd = [
  String(columns.length),
  (globalMinBox + (columns.length - bottomColumn)) * BOX_SIZE,
];

// Bearish resistance line: mirrors the support line's construction, anchored
// on the highest reversal high and descending at the same 45-degree rate.
const resistanceStart = [String(topColumn), globalMaxBox * BOX_SIZE];
const resistanceEnd = [
  String(columns.length),
  (globalMaxBox - (columns.length - topColumn)) * BOX_SIZE,
];

// --- Option -------------------------------------------------------------
const chart = echarts.init(document.getElementById("container"));
chart.setOption({
  animation: false,
  backgroundColor: "transparent",
  title: {
    text: "point-and-figure-basic · javascript · echarts · anyplot.ai",
    left: "center",
    textStyle: { color: t.ink, fontSize: 22, fontWeight: 500 },
  },
  legend: {
    top: 56,
    // Series markers are transparent (only the X/O label glyph is visible),
    // so the legend needs its own swatch colors rather than inheriting them.
    data: [
      { name: "Rising (X)", icon: "rect", itemStyle: { color: t.palette[0] } },
      { name: "Falling (O)", icon: "rect", itemStyle: { color: t.palette[4] } },
      { name: "Bullish support (45°)", icon: "rect", itemStyle: { color: t.inkSoft } },
      { name: "Bearish resistance (45°)", icon: "rect", itemStyle: { color: t.inkSoft } },
    ],
    textStyle: { color: t.inkSoft, fontSize: 15 },
  },
  grid: { left: 110, right: 60, top: 130, bottom: 90 },
  xAxis: {
    type: "category",
    name: "Column (price reversal)",
    nameLocation: "middle",
    nameGap: 40,
    nameTextStyle: { color: t.inkSoft, fontSize: 15 },
    data: columns.map((_, i) => String(i + 1)),
    axisLabel: { color: t.inkSoft, fontSize: 14 },
    axisLine: { lineStyle: { color: t.inkSoft } },
    axisTick: { show: false },
    splitLine: { show: false },
  },
  yAxis: {
    type: "value",
    name: "Price ($)",
    nameTextStyle: { color: t.inkSoft, fontSize: 15 },
    min: (globalMinBox - 2) * BOX_SIZE,
    max: (globalMaxBox + 1) * BOX_SIZE,
    interval: BOX_SIZE,
    axisLabel: { color: t.inkSoft, fontSize: 14, formatter: "${value}" },
    axisLine: { show: false },
    axisTick: { show: false },
    splitLine: { lineStyle: { color: t.grid } },
  },
  series: [
    {
      name: "Rising (X)",
      type: "scatter",
      data: risingPoints,
      symbol: "circle",
      symbolSize: 30,
      itemStyle: { color: "transparent" },
      label: {
        show: true,
        position: "inside",
        formatter: "X",
        fontSize: 26,
        fontWeight: 700,
        color: t.palette[0],
      },
      tooltip: { valueFormatter: (v) => `$${v}` },
      z: 3,
    },
    {
      name: "Falling (O)",
      type: "scatter",
      data: fallingPoints,
      symbol: "circle",
      symbolSize: 30,
      itemStyle: { color: "transparent" },
      label: {
        show: true,
        position: "inside",
        formatter: "O",
        fontSize: 26,
        fontWeight: 700,
        color: t.palette[4],
      },
      tooltip: { valueFormatter: (v) => `$${v}` },
      z: 3,
    },
    {
      name: "Bullish support (45°)",
      type: "line",
      data: [supportStart, supportEnd],
      showSymbol: false,
      lineStyle: { color: t.inkSoft, width: 2, type: "dashed" },
      z: 2,
      tooltip: { show: false },
    },
    {
      name: "Bearish resistance (45°)",
      type: "line",
      data: [resistanceStart, resistanceEnd],
      showSymbol: false,
      lineStyle: { color: t.inkSoft, width: 2, type: "dotted" },
      z: 2,
      tooltip: { show: false },
    },
  ],
});

Retrieve this implementation

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

Part of Point and Figure Chart on anyplot.ai.

Other implementations