Skip to content

Price-axis transforms — Renko, Line Break, Kagi, Point & Figure ​

Four charts that throw time away and keep price, all derived from the candles on top. Each is registered with derive, so a tick goes in as a candle and the transform is re-derived; the Renko chart takes the same ticks the candles do (play/pause), and a moving average sits on its bricks as it would on candles. Their x is an ordinal, not a time — the axis format wins the closing time back from closedAt. One price step sizes the three that take one — brick, reversal, box — atrPriceStep, the tape's own ATR; Line Break counts lines instead.

What this page shows, and what it cannot: the candles on top take a tick every 400 ms; the Renko chart is registered with derive and takes the same ticks — the bricks are re-derived from the accepted candles, the ordinals of the closed prefix stay put — and a moving average is attached to its bricks the way it would be to candles. Line Break, Kagi and Point & Figure are derived the same way, from the tape as it stood when the page opened. Every derived chart's x is an ordinal; the axis format reads the block's closedAt back as its label. The three price steps — brick, reversal, box — are one number, atrPriceStep of the tape; Line Break takes a count of lines, not a price. What does not fit this lineage (a volume pane, infiniteHistory, drawings that anchor to time) is in the guide's Price-axis transforms section.

Source ​

apps/examples/src/cases/price-axis-transforms.ts — the real thing, type-checked in CI.

ts
import { PlotBuilder, browserDeps } from "@finchart/dom";
import type { OHLC, Plot } from "@finchart/core";
import { candleSeries, priceFormat, timeTicks } from "@finchart/core";
import { atrPriceStep, attachMovingAverage, kagi, kagiSeries, lineBreak, pointAndFigure, pointAndFigureSeries, renko } from "@finchart/indicators";
import { fixtureCandles } from "./fixture";
import { chartHost } from "./stage";

export const title = "Price-axis transforms — Renko, Line Break, Kagi, Point & Figure";
export const description =
  "Four charts that throw time away and keep price, all derived from the candles on top. Each is registered with derive, so a tick goes in as a candle and the transform is re-derived; " +
  "the Renko chart takes the same ticks the candles do (play/pause), and a moving average sits on its bricks as it would on candles. " +
  "Their x is an ordinal, not a time — the axis format wins the closing time back from closedAt. One price step sizes the three that take one — brick, reversal, box — atrPriceStep, the tape's own ATR; Line Break counts lines instead.";

export function mount(container: HTMLElement): () => void {
  const toolbar = document.createElement("div");
  toolbar.style.cssText = "margin-bottom: 8px";
  container.append(toolbar);

  const timeLabel = new Intl.DateTimeFormat("en-US", { timeZone: "UTC", hour: "2-digit", minute: "2-digit", hour12: false });
  const all = fixtureCandles(600);
  let revealed = 300;
  const step = atrPriceStep(all.slice(0, revealed), { period: 14 });

  const plots: Plot[] = [];
  const panel = (height: number, ordinal: boolean): Plot & { label: HTMLDivElement } => {
    const label = document.createElement("div");
    label.style.cssText = "font: 12px/1.4 system-ui; opacity: 0.7; margin: 6px 0 2px";
    container.append(label);
    const host = chartHost(container, height);
    const plot = PlotBuilder.create<OHLC>(browserDeps({ autoSize: true }))
      .setSize(container.clientWidth || 900, height)
      .setAxis({
        x: ordinal ? {} : { ticks: timeTicks({ timeZone: "UTC", locale: "en-US" }) },
        y: { position: "right", format: priceFormat({ compact: true, locale: "en-US" }) },
      })
      .build(host);
    plots.push(plot);
    return Object.assign(plot, { label });
  };

  // The x-axis format of an ordinal chart: the accepted array's `closedAt` is the closing time of the block at x.
  const timeAxis = (plot: Plot, read: () => readonly unknown[]): void => {
    plot.applyOptions({
      axis: {
        x: {
          format: (x: number) => {
            const blocks = read();
            const block = x >= 0 && x <= blocks.length - 1 ? blocks[Math.round(x)] : undefined;
            return typeof block === "object" && block !== null && "closedAt" in block && typeof block.closedAt === "number"
              ? timeLabel.format(block.closedAt)
              : "";
          },
        },
      },
    });
  };

  // 1. The source: candles on a time axis, ticking.
  const candles = panel(260, false);
  candles.label.textContent = "Candles — the source, on a time axis (ticking)";
  candles.applyOptions({ shiftVisibleRangeOnNewBar: true, rightOffset: 4 });
  const price = candles.mainPane.addSeries({ series: candleSeries(), data: all.slice(0, revealed), name: "Price" });

  // 2. Renko — derived from the same ticks, with a moving average on the bricks.
  const renkoPlot = panel(260, true);
  renkoPlot.label.textContent = `Renko(${step.toFixed(0)}) — derived from the ticks above; MA(5) on the bricks`;
  const bricks = renkoPlot.mainPane.addSeries({
    series: candleSeries(),
    data: all.slice(0, revealed),
    derive: (source: readonly OHLC[]) => renko(source, { brickSize: step }),
    name: `Renko(${step.toFixed(0)})`,
  });
  renkoPlot.mainPane.use(attachMovingAverage({ source: bricks, period: 5, name: "MA on bricks" }));
  timeAxis(renkoPlot, () => bricks.read());

  // 3. Line Break — three-line break, static.
  const lbPlot = panel(220, true);
  lbPlot.label.textContent = "Line Break(3)";
  const lines = lbPlot.mainPane.addSeries({ series: candleSeries(), data: all.slice(0, revealed), derive: (source: readonly OHLC[]) => lineBreak(source, { lines: 3 }), name: "Line Break(3)" });
  timeAxis(lbPlot, () => lines.read());

  // 4. Kagi — yang thick, yin thin, static.
  const kagiPlot = panel(220, true);
  kagiPlot.label.textContent = `Kagi(${step.toFixed(0)}) — thick yang, thin yin`;
  const points = kagiPlot.mainPane.addSeries({ series: kagiSeries(), data: all.slice(0, revealed), derive: (source: readonly OHLC[]) => kagi(source, { reversal: step }), name: `Kagi(${step.toFixed(0)})` });
  timeAxis(kagiPlot, () => points.read());

  // 5. Point & Figure — X's and O's on the step's grid, three-box reversal, static.
  const pnfPlot = panel(260, true);
  pnfPlot.label.textContent = `Point & Figure(${step.toFixed(0)} × 3)`;
  const columns = pnfPlot.mainPane.addSeries({
    series: pointAndFigureSeries({ boxSize: step }),
    data: all.slice(0, revealed),
    derive: (source: readonly OHLC[]) => pointAndFigure(source, { boxSize: step }),
    name: `P&F(${step.toFixed(0)} × 3)`,
  });
  timeAxis(pnfPlot, () => columns.read());

  // Ticks — the realtime case's grammar: the same x replaces (the bar in progress), a larger x appends.
  const TICKS_PER_BAR = 3;
  let tick = 0;
  let timer: number | undefined;
  const inProgress = (bar: OHLC, progress: number): OHLC => {
    const close = bar.open + (bar.close - bar.open) * progress;
    return {
      x: bar.x,
      open: bar.open,
      close,
      high: Math.max(bar.open, close, bar.open + (bar.high - bar.open) * progress),
      low: Math.min(bar.open, close, bar.open + (bar.low - bar.open) * progress),
      volume: Math.round((bar.volume ?? 0) * progress),
    };
  };
  const feed = (candle: OHLC): void => {
    price.updateLast(candle);
    bricks.updateLast(candle); // the derived registration re-derives the bricks from the accepted candles
  };
  const button = document.createElement("button");
  const pause = () => {
    clearInterval(timer);
    timer = undefined;
    button.textContent = "Play";
  };
  const play = () => {
    button.textContent = "Pause";
    timer = window.setInterval(() => {
      const next = all[revealed];
      if (!next) return pause();
      tick += 1;
      if (tick < TICKS_PER_BAR) return feed(inProgress(next, tick / TICKS_PER_BAR));
      feed(next);
      revealed += 1;
      tick = 0;
    }, 400);
  };
  button.addEventListener("click", () => (timer === undefined ? play() : pause()));
  toolbar.appendChild(button);
  play();

  return Object.assign(
    () => {
      pause();
      for (const plot of plots) plot.destroy();
      container.replaceChildren();
    },
    { requestRender: () => plots.forEach((plot) => plot.requestRender()) },
  );
}