DocumentationTransforms

Transforms

The transform tier (openalgo-charts/transform) turns time-based OHLC into movement-driven series. The chart can apply a transform itself, live, to the bars you feed it (see In-chart transforms), or you can run a transform and plot the result. Heikin Ashi, Renko, Range, and Line Break render as a candlestick series; Point & Figure and Kagi have dedicated renderers.

import { runTransform, RenkoTransform } from 'openalgo-charts/transform';
const bricks = runTransform(new RenkoTransform({ boxSize: 5 }), bars);
chart.addSeries('candlestick').setData(bricks); // Renko renders as candles
⚠️

Every series on a chart shares one time axis. A transformed series (Renko / Range / Line Break / Kagi / P&F) emits fewer elements than the raw bars, so if another series, typically a volume pane, is fed the raw bars, all the raw timestamps come back onto the shared axis and the bricks render scattered with gaps between them. Re-bucket companion series onto the transformed times instead (e.g. sum the raw volume of the source bars behind each brick, keyed by brick.time), as the live example does.

In-chart transforms

Give a series a transform and the chart applies it to the bars you feed, forming the elements again on every tick. The series keeps speaking your bars: setData, update and prependData take them and getData returns them, so a live feed, replay and history paging work unchanged, while the chart draws the bricks. This chart streams: a tick on the forming bar moves its bricks, a tick back takes them off, and a closed bar keeps them.

live
Rendering live chart…
View example code
const chart = lib.createChart(el);
const series = chart.addSeries('candlestick', { transform: { type: 'renko', options: { boxSize: 1 } } });
series.setData(lib.generateBars(1700000000, 400, 300));
chart.addIndicator('sma', { length: 9 });
chart.fitContent();

let bar = series.getData().slice(-1)[0], ticks = 0, seed = 11;
const rnd = () => { seed = (seed * 1103515245 + 12345) & 0x7fffffff; return seed / 0x7fffffff; };
const id = setInterval(() => {
const close = bar.close + (rnd() - 0.5) * 1.6;
bar = ticks++ < 5
  ? { ...bar, close, high: Math.max(bar.high, close), low: Math.min(bar.low, close) }
  : (ticks = 0, { time: bar.time + 300, open: bar.close, high: bar.close, low: bar.close, close: bar.close });
series.update(bar); // your bar; the chart forms the bricks
}, 600);
chart.on('destroy', () => clearInterval(id));
return chart;
import 'openalgo-charts/transform'; // registers the six transforms
 
const series = chart.addSeries('candlestick', { transform: { type: 'renko' } });
series.setData(bars);
 
// A new type selects its renderer too: 'point-figure' here.
chart.setSeriesTransform(series, { type: 'point-figure', options: { mode: 'atr' } });
chart.seriesTransform(series);          // that spec, as set
chart.setSeriesTransform(series, null); // your bars as given again
TypeOptions (0 or omitted: sized from the loaded history)
heikin-ashinone; one candle per bar, at the bar’s time
renkomode ('fixed', 'percent', 'atr', 'atrPercent'), boxSize, percent (1), atrPeriod (14), atrMultiplier (1), method ('close', 'hl')
range-barsrange (twice the history box)
line-breaklines, default 3
point-figuremode ('fixed', 'percent', 'atr', 'atrPercent'), boxSize, percent (1), atrPeriod (14), atrMultiplier (1), reversal (3), method ('hl', 'close')
kagireversal (twice the history box)

The history box is a fortieth of the loaded price range, at least a tenth of a percent of the last close, rounded to two significant figures. It is resolved when you call setData (a new load or symbol), never on a tick, and kept when prependData pages older history in. An option you leave out stays out of the spec and of saved state. A new transform type also selects its renderer; setSeriesType afterwards picks another and keeps the transform. The chart settings dialog’s Price tab offers the transform’s options, keyed transform.<option>.

Nothing is transformed unless you ask. addSeries('point-figure') and setSeriesType only pick the renderer, so a host that prepares its own elements, as 2.5.x required, is never transformed twice. The widget’s chart type menu uses in-chart transforms when it loads its own bars from a feed.

Studies on a transformed chart

A study computes on the elements the chart draws by default, which is what it computed on when a host fed it the elements. Set it to the underlying bars and it computes on your bars instead, reading each value at the bar the element was completed on: the value a trader could have read as that brick formed. Its points stay on the elements’ times.

const sma = chart.addIndicator('sma', { length: 20 }, { barSource: 'underlying' });
sma.barSource();              // 'underlying'
sma.setBarSource('chart');    // back to the elements

Heikin Ashi keeps one candle per bar at the bar’s own time, so there the underlying values need no reading across. On a chart with no transform the two choices are the same bars. The choice is saved with the study (barSource: 'underlying', written only when it is not the default).

A study on the underlying bars computes on your bars, but its hooks run on the elements: a plot’s colorBy, an alert’s when and message, and the other per-bar hooks receive ctx.index, ctx.bars and ctx.values indexed by element. A custom descriptor that keeps per-bar state from calc and reads it back by index therefore reads the wrong bar on every transform that is not one element per bar, which is all of them but Heikin Ashi. Read per-bar values from ctx.values, which the chart samples onto the elements.

Its alerts count your bars, not the elements. A bar that completes two bricks is judged once, at the first brick, and an alert with a frequency treats it as one bar: oncePerBar delivers once for it and onBarClose waits for that bar to close. On the chart’s bars each brick is judged.

A Kagi chart dates the vertex still forming at your newest bar, and a range bar that a newer bar extends is dated at that bar too. A study reads that as the same element revised, not as a change to history: its alerts judge each new vertex or range bar once, barState.isNew is true only on a bar that added an element, and a study with a live tail keeps taking it. A tick that takes back an element it formed, or a correction of an older bar that changes an element before the forming one, still reseeds the alerts silently.

Alerts on a transformed chart

Trader alerts judge the elements the chart draws. When one bar completes several bricks, each brick is judged in turn and an alert reports the brick that crossed its level. The newest brick is still forming, so a bar-close alert confirms it once the next brick follows.

Heikin Ashi

live
Rendering live chart…
View example code
const chart = lib.createChart(el);
const bars = lib.generateBars(1700000000, 200, 3600);
const ha = lib.runTransform(new lib.HeikinAshiTransform(), bars);
chart.addSeries('candlestick').setData(ha);
chart.timeScale.fitContent(ha.length);
return chart;

Renko

Bricks form only when price moves a full box size, filtering out noise.

live
Rendering live chart…
View example code
const chart = lib.createChart(el);
const bars = lib.generateBars(1700000000, 500, 3600);
const bricks = lib.runTransform(new lib.RenkoTransform({ boxSize: 2 }), bars);
chart.addSeries('candlestick').setData(bricks);
chart.timeScale.fitContent(bricks.length);
return chart;

Bricks are sized the same four ways as point and figure boxes: a fixed boxSize, a percent of price, the ATR times atrMultiplier, or (mode: 'atrPercent') a percent of the ATR. Percent and ATR sizes re-resolve as each brick forms. method: 'close' (the default) moves bricks on the close alone; method: 'hl' walks the bar’s low and high in the order the bar most likely traded them, low first on an up bar and high first on a down bar, so a long wick forms the bricks the close never reaches.

runTransform(new RenkoTransform({ mode: 'percent', percent: 0.25 }), bars);
runTransform(new RenkoTransform({ mode: 'atrPercent', percent: 50, atrPeriod: 14, method: 'hl' }), bars);

Range bars

Each bar spans a fixed price range, regardless of time.

live
Rendering live chart…
View example code
const chart = lib.createChart(el);
const bars = lib.generateBars(1700000000, 500, 3600);
const rb = lib.runTransform(new lib.RangeBarsTransform({ range: 2 }), bars);
chart.addSeries('candlestick').setData(rb);
chart.timeScale.fitContent(rb.length);
return chart;

Point & Figure

Columns of Xs (up) and Os (down); a new column starts only after a reversal of N boxes. Plot the result as a point-figure series.

live
Rendering live chart…
View example code
const chart = lib.createChart(el);
const bars = lib.generateBars(1700000000, 600, 3600);
const cols = lib.runTransform(new lib.PointFigureTransform({ boxSize: 1, reversal: 3 }), bars);
chart.addSeries('point-figure').setData(cols);
chart.timeScale.fitContent(cols.length);
return chart;
import { runTransform, PointFigureTransform } from 'openalgo-charts/transform';
const cols = runTransform(new PointFigureTransform({ boxSize: 1, reversal: 3 }), bars);
chart.addSeries('point-figure').setData(cols);

Each emitted column carries the boxSize it was built with, so the renderer sizes its glyph stack from the data. You no longer pass style: { boxSize } - and can no longer desync it from the transform. (style.boxSize still works as a fallback for hand-built column data.)

Options

OptionTypeDefaultDescription
boxSizenumber-Box height for mode: 'fixed'. Required in that mode.
reversalnumber3Boxes of counter-move needed to start a new column.
method'hl' | 'close''hl''hl' extends columns from each bar’s high/low - the standard construction. 'close' uses only the close.
mode'fixed' | 'percent' | 'atr' | 'atrPercent''fixed'How the box size is resolved.
percentnumber-Box as a percentage of price for mode: 'percent', or of the ATR for 'atrPercent' (0.5 = 0.5%).
atrPeriodnumber14ATR lookback for mode: 'atr' and 'atrPercent' (Wilder).
atrMultipliernumber1ATR multiplier for mode: 'atr'.

percent and atr re-resolve the box size each time a column opens, so the grid adapts to price level and volatility:

// 0.5%-of-price boxes - keeps the grid meaningful across a wide price range
runTransform(new PointFigureTransform({ mode: 'percent', percent: 0.5, reversal: 3 }), bars);
 
// volatility-scaled boxes
runTransform(new PointFigureTransform({ mode: 'atr', atrPeriod: 14, reversal: 3 }), bars);

Column shape

PointFigureColumn extends Bar with two fields:

FieldDescription
boxSizePrice height of one box - one X or O glyph - in this column.
boxesNumber of glyphs stacked in the column.

low is the bottom edge of the lowest box and high is the exclusive top edge of the highest box, so [low, high) is exactly the span the glyph stack occupies and boxes === (high - low) / boxSize.

method: 'hl' is the default because it is what P&F means on every desk: a bar that swings through several boxes intrabar builds those boxes even if it closes flat. method: 'close' reproduces the older close-only variant, which ignores intrabar range entirely.

Kagi

A continuous line that reverses (and switches thick/thin) only after a move of at least the reversal amount. Plot it as a kagi series.

live
Rendering live chart…
View example code
const chart = lib.createChart(el);
const bars = lib.generateBars(1700000000, 600, 3600);
const kagi = lib.runTransform(new lib.KagiTransform({ reversal: 3 }), bars);
chart.addSeries('kagi').setData(kagi);
chart.timeScale.fitContent(kagi.length);
return chart;
import { runTransform, KagiTransform } from 'openalgo-charts/transform';
const kagi = runTransform(new KagiTransform({ reversal: 4 }), bars);
chart.addSeries('kagi').setData(kagi);

All transforms are incremental, so they update efficiently on live data. The simplest way to use that is an in-chart transform: feed each bar and tick with series.update and the chart keeps building.


API reference

registerTransformChartTypes

function registerTransformChartTypes(): void

Registers the custom renderers for 'point-figure' and 'kagi' into the chart-type registry. This happens automatically as a side effect when you import the tier:

import 'openalgo-charts/transform'; // side effect registers the renderers

Bundlers that aggressively tree-shake a bare side-effect import may skip the call. In that case, call registerTransformChartTypes() explicitly before adding either series type. The function is idempotent and safe to call more than once.

import { registerTransformChartTypes } from 'openalgo-charts/transform';
 
registerTransformChartTypes(); // no-op if already registered
chart.addSeries('point-figure').setData(cols);

(Heikin Ashi, Renko, Range, and Line Break plot as the built-in 'candlestick' type and need no new renderer registration.)

ISeriesTransform

interface ISeriesTransform {
  reset(): void;
  push(bar: Bar): Bar[];
  flush?(): Bar[];
  clone?(): ISeriesTransform;
}

The contract every transform class implements. reset returns the transform to its initial state at the start of a fresh history batch. push feeds one source bar and returns zero or more newly completed derived elements as Bar[]. Each source bar is pushed once, in time order: every push moves the state, so pushing a bar that is still forming again on each tick counts it again. The optional flush emits any in-progress element at the end of the data stream (for example, the current partial range bar). runTransform calls all three in sequence. The optional clone returns an independent copy of the state, which is how an in-chart transform pushes a forming bar on every tick without committing it; every built-in transform has one.

import type { ISeriesTransform } from 'openalgo-charts/transform';
import type { Bar } from 'openalgo-charts';
 
class MyTransform implements ISeriesTransform {
  reset(): void { /* ... */ }
  push(bar: Bar): Bar[] { return []; }
  flush(): Bar[] { return []; }
}

ensureIncreasingTimes

function ensureIncreasingTimes(bars: readonly Bar[]): Bar[]

Makes times strictly increasing across a Bar[] by bumping any collision by +1 second. Multiple derived elements can form within the same source bar and therefore share a timestamp; this function gives each a distinct logical index in the DataLayer. Labels stay within a few seconds of the real formation time.

runTransform calls ensureIncreasingTimes automatically on its output. You only need to call it directly when assembling batches without runTransform:

import { ensureIncreasingTimes } from 'openalgo-charts/transform';
 
const raw: Bar[] = myTransform.flush();
const adjusted = ensureIncreasingTimes(raw);
series.setData(adjusted);