Point and Figure Chart — D3.js

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 D3.js

Renders

JavaScript source (D3.js)

// anyplot.ai
// point-and-figure-basic: Point and Figure Chart
// Library: d3 7.9.0 | JavaScript 22.23.2
// Quality: 92/100 | Created: 2026-09-02

const t = window.ANYPLOT_TOKENS;
const { width, height } = window.ANYPLOT_SIZE;

// --- Data: synthetic daily closes, converted into Point & Figure columns ---
function lcg(seed) {
  let s = seed >>> 0;
  return () => {
    s = (s * 1664525 + 1013904223) >>> 0;
    return s / 4294967296;
  };
}

const rand = lcg(42);
const boxSize = 2; // $ per box
const reversal = 3; // boxes required to start a new column

let price = 120;
let momentum = 0;
const closes = [];
for (let i = 0; i < 300; i++) {
  momentum = (momentum + (rand() - 0.5) * 0.4) * 0.85;
  price = Math.max(20, price + momentum + (rand() - 0.5) * 5);
  closes.push(price);
}

const boxIndex = (p) => Math.round(p / boxSize);
const boxed = closes.map(boxIndex);

// Whole-box reversal method: extend the current column while price keeps
// moving in its direction, start a new column only once price reverses by
// `reversal` boxes.
const columns = [];
let current = boxed[0];
let column = null;
for (let i = 1; i < boxed.length; i++) {
  const idx = boxed[i];
  if (column === null) {
    if (idx === current) continue;
    column = idx > current ? { type: "X", low: current, high: idx } : { type: "O", low: idx, high: current };
    current = idx;
    continue;
  }
  if (column.type === "X") {
    if (idx > column.high) {
      column.high = idx;
      current = idx;
    } else if (idx <= column.high - reversal) {
      columns.push(column);
      column = { type: "O", low: idx, high: column.high - 1 };
      current = idx;
    }
  } else {
    if (idx < column.low) {
      column.low = idx;
      current = idx;
    } else if (idx >= column.low + reversal) {
      columns.push(column);
      column = { type: "X", low: column.low + 1, high: idx };
      current = idx;
    }
  }
}
if (column) columns.push(column);

// Classic 45-degree trend lines: a bullish support line rising from the
// chart's lowest box, and a bearish resistance line falling from an early
// swing high (last 30% of columns excluded so the line has room to run).
const minLow = d3.min(columns, (c) => c.low);
const supportStart = columns.findIndex((c) => c.low === minLow);

const earlyCutoff = Math.floor(columns.length * 0.7);
const earlyHigh = d3.max(columns.slice(0, earlyCutoff), (c) => c.high);
const resistanceStart = columns.findIndex((c) => c.high === earlyHigh);

function trendLine(startIdx, startBox, slope) {
  const points = [];
  for (let i = startIdx; i < columns.length; i++) {
    points.push([i, startBox + slope * (i - startIdx)]);
  }
  return points;
}

// --- Layout ------------------------------------------------------------
const margin = { top: 150, right: 70, bottom: 90, left: 110 };
const iw = width - margin.left - margin.right;
const ih = height - margin.top - margin.bottom;

const minBox = d3.min(columns, (c) => c.low) - 1;
const maxBox = d3.max(columns, (c) => c.high) + 1;

const x = d3.scaleBand().domain(d3.range(columns.length)).range([0, iw]).paddingInner(0.08).paddingOuter(0.04);
const y = d3.scaleLinear().domain([minBox, maxBox]).range([ih, 0]);
const cellH = Math.abs(y(minBox) - y(minBox + 1));

const svg = d3.select("#container").append("svg").attr("width", width).attr("height", height);
const g = svg.append("g").attr("transform", `translate(${margin.left},${margin.top})`);

// --- Breakout highlight: a soft band behind the single largest column ----
// (the biggest box-count swing), giving the chart one clear focal point
// instead of every column reading with equal weight.
const columnSizes = columns.map((c) => c.high - c.low + 1);
const breakoutIdx = columnSizes.indexOf(d3.max(columnSizes));
const breakoutColumn = columns[breakoutIdx];

g.append("rect")
  .attr("x", x(breakoutIdx) - (x.step() - x.bandwidth()) / 2)
  .attr("y", 0)
  .attr("width", x.step())
  .attr("height", ih)
  .attr("fill", breakoutColumn.type === "X" ? t.palette[0] : t.palette[4])
  .attr("opacity", 0.08);

// --- Gridlines at box-size price intervals (every other box, kept light
// so the box-structure glyphs stay the primary read) -----------------------
const boxTicks = d3.range(minBox, maxBox + 1);
const gridTicks = boxTicks.filter((_, i) => i % 2 === 0);
g.append("g")
  .selectAll("line")
  .data(gridTicks)
  .join("line")
  .attr("x1", 0)
  .attr("x2", iw)
  .attr("y1", (b) => y(b))
  .attr("y2", (b) => y(b))
  .attr("stroke", t.grid)
  .attr("stroke-width", 1);

// --- Y axis: price scale -------------------------------------------------
const yAxis = g.append("g").call(
  d3
    .axisLeft(y)
    .tickValues(boxTicks.filter((_, i) => i % 2 === 0))
    .tickFormat((b) => `$${b * boxSize}`)
);
yAxis.selectAll("text").attr("fill", t.inkSoft).style("font-size", "14px");
yAxis.selectAll("line").attr("stroke", t.inkSoft);
yAxis.select(".domain").attr("stroke", t.inkSoft);

// --- X axis: column index (reversals), not time ---------------------------
const xAxis = g
  .append("g")
  .attr("transform", `translate(0,${ih})`)
  .call(
    d3
      .axisBottom(x)
      .tickValues(d3.range(columns.length).filter((i) => i % 2 === 0))
      .tickFormat((i) => i + 1)
  );
xAxis.selectAll("text").attr("fill", t.inkSoft).style("font-size", "14px");
xAxis.selectAll("line").attr("stroke", t.inkSoft);
xAxis.select(".domain").attr("stroke", t.inkSoft);

// --- Support / resistance trend lines ------------------------------------
const lineGen = d3
  .line()
  .x((d) => x(d[0]) + x.bandwidth() / 2)
  .y((d) => y(d[1]));

g.append("path")
  .datum(trendLine(supportStart, minLow, 1))
  .attr("fill", "none")
  .attr("stroke", t.inkSoft)
  .attr("stroke-width", 1.5)
  .attr("stroke-dasharray", "9,7")
  .attr("opacity", 0.5)
  .attr("d", lineGen);

g.append("path")
  .datum(trendLine(resistanceStart, earlyHigh, -1))
  .attr("fill", "none")
  .attr("stroke", t.inkSoft)
  .attr("stroke-width", 1.5)
  .attr("stroke-dasharray", "9,7")
  .attr("opacity", 0.5)
  .attr("d", lineGen);

// --- Columns of X's (rising) and O's (falling) ---------------------------
const bullish = t.palette[0]; // #009E73 — brand green, always first series
const bearish = t.palette[4]; // matte red — semantic anchor for loss / decline

const cells = columns.flatMap((c, i) => d3.range(c.low, c.high + 1).map((level) => ({ col: i, level, type: c.type })));
const symbolSize = Math.min(x.bandwidth(), cellH) * 0.66;

g.selectAll("text.box")
  .data(cells)
  .join("text")
  .attr("class", "box")
  .attr("x", (d) => x(d.col) + x.bandwidth() / 2)
  .attr("y", (d) => y(d.level))
  .attr("text-anchor", "middle")
  .attr("dominant-baseline", "central")
  .style("font-size", `${symbolSize}px`)
  .style("font-weight", 700)
  .style("font-family", "monospace")
  .attr("fill", (d) => (d.type === "X" ? bullish : bearish))
  .text((d) => d.type);

// --- Axis labels -----------------------------------------------------------
g.append("text")
  .attr("x", iw / 2)
  .attr("y", ih + 62)
  .attr("text-anchor", "middle")
  .attr("fill", t.ink)
  .style("font-size", "18px")
  .text("Column (price reversal), not time");

g.append("text")
  .attr("transform", "rotate(-90)")
  .attr("x", -ih / 2)
  .attr("y", -80)
  .attr("text-anchor", "middle")
  .attr("fill", t.ink)
  .style("font-size", "18px")
  .text("Price ($)");

// --- Legend: X / O meaning --------------------------------------------------
const legend = svg.append("g").attr("transform", `translate(${margin.left},92)`);
const legendItems = [
  { symbol: "X", label: "Rising column", color: bullish },
  { symbol: "O", label: "Falling column", color: bearish },
];
let legendX = 0;
for (const item of legendItems) {
  const entry = legend.append("g").attr("transform", `translate(${legendX},0)`);
  entry
    .append("text")
    .attr("fill", item.color)
    .style("font-size", "20px")
    .style("font-weight", 700)
    .style("font-family", "monospace")
    .text(item.symbol);
  entry
    .append("text")
    .attr("x", 26)
    .attr("y", 0)
    .attr("dominant-baseline", "middle")
    .attr("fill", t.inkSoft)
    .style("font-size", "15px")
    .text(item.label);
  legendX += 26 + item.label.length * 8.5 + 40;
}

// --- Subtitle: box size + reversal setting ---------------------------------
svg
  .append("text")
  .attr("x", width / 2)
  .attr("y", 82)
  .attr("text-anchor", "middle")
  .attr("fill", t.inkSoft)
  .style("font-size", "16px")
  .text(`Box size $${boxSize} · ${reversal}-box reversal`);

// --- Title -------------------------------------------------------------
svg
  .append("text")
  .attr("x", width / 2)
  .attr("y", 44)
  .attr("text-anchor", "middle")
  .attr("fill", t.ink)
  .style("font-size", "22px")
  .style("font-weight", 600)
  .text("point-and-figure-basic · javascript · d3 · anyplot.ai");

Retrieve this implementation

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

Part of Point and Figure Chart on anyplot.ai.

Other implementations