Scales & panes
Time scale
The time scale is shared by all panes. Useful methods:
chart.timeScale.fitContent(barCount); // fit all bars to the width
chart.fitContent(); // no-arg convenience (bar count from data)
chart.timeScale.setBarSpacing(8); // pixels per bar (zoom)
chart.timeScale.scrollToRealtime?.(); // jump to the latest barPreserve zoom across a data reload
Snapshot the visible logical range before replacing the data and restore it after, so a full-history reconcile does not throw away the user’s zoom/scroll:
const range = chart.getVisibleLogicalRange(); // { from, to }
mainSeries.setData(fullHistory); // would otherwise jump to the default view
chart.setVisibleLogicalRange(range); // restore the exact windowBoth methods also exist directly on chart.timeScale (getVisibleLogicalRange /
setVisibleLogicalRange); mutating the scale repaints automatically.
Price scale & axis-drag rescale
Each pane has an independent price scale (linear by default, autoscaling to the visible range). Direct manipulation:
- Wheel - zoom both axes around the cursor.
- Drag the price (Y) axis up/down - expand / compress the price scale (switches that pane to manual scale).
- Drag the time (X) axis left/right - compress / expand bar spacing.
- Double-click - reset: fit content and re-enable autoscale (
chart.resetScale()).
Programmatic conversion between price and pixels (for DOM overlays):
const y = chart.priceToCoordinate(101.5); // price → container px
const p = chart.coordinateToPrice(y); // px → priceMulti-pane layouts
Reference a new paneIndex and the pane is created on demand. Panes share the time axis, so
they stay aligned bar-for-bar. A classic price + volume + RSI stack:
const chart = lib.createChart(el);
const bars = lib.generateBars(1700000000, 180, 3600);
// pane 0: price
chart.addSeries('candlestick').setData(bars);
// pane 1: volume
chart.addSeries('histogram', { paneIndex: 1 })
.setData(bars.map(b => ({ time: b.time, value: b.volume, close: b.volume })));
// pane 2: RSI(14) with guides
chart.addSeries('line', { paneIndex: 2, style: { color: '#e0b020' } }).setData(lib.rsiSeries(bars, 14));
chart.addPriceLine({ price: 70, color: '#ef5350', lineWidth: 1, dashed: true, id: 'r70' }, 2);
chart.addPriceLine({ price: 30, color: '#26a69a', lineWidth: 1, dashed: true, id: 'r30' }, 2);
chart.timeScale.fitContent(bars.length);
return chart;Resizing, reordering & removing panes
Pane heights are relative weights, not pixels — only the ratio matters. Users drag the
boundary between two panes (the cursor becomes row-resize within 4px of it) and the height
moves from one to the other, conserving their combined weight so the rest of the stack is
untouched and neither side can collapse.
| Method | Description |
|---|---|
chart.setPaneWeight(index, weight) | Set a pane’s relative height. |
chart.paneWeight(index) | Read it back. |
chart.movePane(index, -1 | 1) | Move a pane one slot up or down. |
chart.maximizePane(index) | Expand one pane to fill the chart; call again to restore. |
chart.maximizedPane() | The maximized pane index, or null. |
chart.removePane(index) | Remove a pane with its series and indicators. |
Pane 0 is pinned. It owns the primary price series and the shared price context, so
removePane(0) and any movePane that would displace it return false rather than
silently doing something surprising. Removing a pane re-indexes the panes below it, and the
indicators living there move with them.
Events: paneResized, paneMoved, paneMaximized, paneRemoved.
Pane legends
Every indicator gets a legend row at the top-left of its pane — a colour swatch, the name, its parameters, and one reading per plot in that plot’s own colour, tracking the crosshair. Hovering the row reveals inline controls; the chart handles the presses itself, so you get show/hide, move, maximize, and delete without wiring anything.
| Control | Effect |
|---|---|
| Eye | Show / hide the source’s plots (it stays on the chart). |
| Gear | Emits indicatorSettings — see below. |
| Up / Down | Move this pane one slot. |
| Maximize | Expand this pane; press again to restore. |
| Trash | Remove the source, and its pane if that empties it. |
The first legend on a non-price pane also carries the pane-level controls (move, maximize), so extra rows on the same pane stay uncluttered.
The engine ships no DOM, so the gear has no built-in dialog — it emits and you render one.
Everything needed to generate a settings form already lives on the descriptor’s inputs:
chart.on('indicatorSettings', ({ instanceId }) => {
const inst = chart.indicators().find((i) => i.id === instanceId);
const descriptor = getIndicator(inst.indicatorId);
for (const input of descriptor.inputs) {
// input.type is 'number' | 'boolean' | 'color' | 'text' | 'select' | 'source'
// build a field, then: inst.setSettings({ [input.key]: value })
}
});Your own legend rows
PaneLegend is a public primitive, so a host can add its own rows — a symbol / OHLC header,
a volume readout. Rows added to the same pane stack automatically in insertion order, and
removing one closes the gap.
import { PaneLegend } from 'openalgo-charts';
const symbol = new PaneLegend({ id: 'symbol', title: 'AAPL', params: '1D · NSE', actions: [] });
chart.addPrimitive(symbol, 0);
symbol.setValues([
{ label: 'O', text: '322.04', color: '#26a69a' },
{ label: 'H', text: '334.37', color: '#26a69a' },
{ text: '+11.36 (+3.53%)', color: '#26a69a' },
]);actions: [] gives a read-only row. Pass any of 'hide' | 'settings' | 'up' | 'down' | 'maximize' | 'close' to show controls; they hit-test as ${id}::<action> and route through
chart.subscribeClick.
Legends draw on the canvas, not in the DOM — like BuySellButtons and the DOM ladder.
They composite into takeScreenshot(), cost no DOM per pane, and their icons are vector
strokes rather than text glyphs (⛶ and 🗑 render as emoji on some platforms).
Left, right & overlay price scales
Assign a series to a price axis with priceScaleId:
'right'(default) and'left'each draw their own axis and autoscale independently — spot price on one side, a premium or spread on the other.''(empty string) is a hidden overlay scale with no axis, autoscaled on its own — the standard way to pin a volume histogram inside the price pane.
Reach a series’ scale with series.priceScale() and tune it via .setOptions(...).
Dual left / right axes
const chart = lib.createChart(el);
const bars = lib.generateBars(1700000000, 160, 3600);
// spot price on the RIGHT axis
chart.addSeries('line', { priceScaleId: 'right', style: { color: '#2962ff' } })
.setData(bars.map(b => ({ time: b.time, value: b.close, close: b.close })));
// a premium series (different magnitude) on the LEFT axis, drawn dashed
chart.addSeries('line', {
priceScaleId: 'left',
style: { color: '#e0473e', lineStyle: 'dashed' },
}).setData(bars.map(b => ({ time: b.time, value: b.close * 0.02 + 40, close: b.close * 0.02 + 40 })));
chart.fitContent();
return chart;Volume overlay on a hidden scale
const chart = lib.createChart(el);
const bars = lib.generateBars(1700000000, 160, 3600);
chart.addSeries('candlestick').setData(bars);
// volume on a hidden '' overlay scale, pinned to the bottom ~20%
const vol = chart.addSeries('histogram', {
priceScaleId: '',
priceFormat: { type: 'volume' }, // compact 1.2K / 3.4M / 5.6B on its own axis
});
vol.setData(bars.map(b => ({ time: b.time, value: b.volume, close: b.volume })));
vol.priceScale().setOptions({ marginTop: 0.8, marginBottom: 0 });
chart.fitContent();
return chart;Per-series price format
priceFormat sets a series’ axis and crosshair-tag formatting without affecting other
series (each has its own scale):
// currency
chart.addSeries('area', { priceFormat: { type: 'custom', formatter: (v) => 'Rs ' + v.toFixed(2) } });
// compact volume: 1.2K / 3.4M / 5.6B
chart.addSeries('histogram', { priceScaleId: '', priceFormat: { type: 'volume' } });
// fixed precision derived from the tick size
chart.addSeries('line', { priceFormat: { type: 'price', minMove: 0.05 } });PriceScaleOptions
Each pane has an independent PriceScale. The options that control it are typed as
PriceScaleOptions and exported from the package. All fields are optional at
construction; missing keys fall back to DEFAULT_PRICE_SCALE_OPTIONS:
import { DEFAULT_PRICE_SCALE_OPTIONS } from 'openalgo-charts';
// { marginTop: 0.1, marginBottom: 0.1, minMove: 0, mode: 'linear', inverted: false }| Field | Type | Default | Description |
|---|---|---|---|
marginTop | number | 0.1 | Fraction of the pane height kept empty above the highest visible value. 0.1 keeps a 10 % gap at the top. |
marginBottom | number | 0.1 | Fraction of the pane height kept empty below the lowest visible value. |
minMove | number | 0 | Instrument tick size, for example 0.05 for a 5-paise step. 0 means precision is inferred from the visible range automatically. |
mode | 'linear' | 'logarithmic' | 'linear' | Price-to-pixel coordinate transform. logarithmic uses a log10 map so equal percentage moves occupy equal screen space. |
inverted | boolean | false | Flips the Y axis so prices increase downward, useful for spread or inverted-chart views. |
Only linear and logarithmic modes are implemented. percentage and indexed-to-100
rebasing modes are planned but not yet available; see the README known-limitations section.
TimeScaleOptions
TimeScaleOptions configures the shared time axis. The library maps a logical bar index
to pixel x-coordinates (not timestamps), so non-trading gaps such as weekends and
holidays collapse automatically. Initial options fall back to DEFAULT_TIME_SCALE_OPTIONS:
import { DEFAULT_TIME_SCALE_OPTIONS } from 'openalgo-charts';
// { barSpacing: 8, minBarSpacing: 1, maxBarSpacing: 80, rightOffset: 4 }| Field | Type | Default | Description |
|---|---|---|---|
barSpacing | number | 8 | Pixels per bar at initialisation. Clamped to [minBarSpacing, maxBarSpacing] at all times. |
minBarSpacing | number | 1 | Hard floor on bar spacing, reached at maximum zoom-in. |
maxBarSpacing | number | 80 | Hard ceiling on bar spacing, reached at maximum zoom-out. |
rightOffset | number | 4 | Empty bar slots kept to the right of the latest bar, leaving a small look-ahead margin. |
TimeScaleOptions is an init-time snapshot. To change values at runtime use the
setter methods on the live TimeScale instance:
chart.timeScale.setBarSpacing(12); // change zoom level
chart.timeScale.setRightOffset(8); // widen the right marginCustom price formatting
A priceFormatter function overrides the default tick-size-aware toFixed that the
library uses for axis tick labels, the last-price tag, and price-line labels. Supply it
at construction via ChartOptions:
const chart = createChart(el, {
priceFormatter: (p) -> '$' + p.toFixed(2),
});To swap the formatter at runtime, call chart.setPriceFormatter. Pass null to
restore the default:
chart.setPriceFormatter((p) -> p.toFixed(4)); // switch to 4 decimal places
chart.setPriceFormatter(null); // revert to defaultThe formatter is applied to every existing and future pane automatically.
See Types for the ChartOptions type definition and
Scales and panes for the per-pane access pattern below.
Exported scale classes
PriceScale and TimeScale are exported classes with their own methods, including
coordinate transforms, autoscale control, snap-to-tick, and formatter override. The
supporting option types PriceScaleOptions, PriceScaleMode, TimeScaleOptions,
and LogicalRange are also exported. See Types for the full list.
Access the live instances at runtime:
// Time scale is shared across all panes
const ts = chart.timeScale; // TimeScale instance
ts.setBarSpacing(12);
ts.setRightOffset(8);
// Price scale is per-pane; pane 0 is the main price pane
const ps = chart.panes()[0].priceScale; // PriceScale instance
ps.setPriceFormatter((p) -> p.toFixed(3));
ps.setAutoScale(true); // re-enable autoscale after a manual axis drag