DocumentationScales & Panes

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 bar

Preserve 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 window

Both 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 → price

Multi-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;
live
Rendering live 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.

MethodDescription
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.

ControlEffect
EyeShow / hide the source’s plots (it stays on the chart).
GearEmits indicatorSettings — see below.
Up / DownMove this pane one slot.
MaximizeExpand this pane; press again to restore.
TrashRemove 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;
live
Rendering live 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;
live
Rendering live 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 }
FieldTypeDefaultDescription
marginTopnumber0.1Fraction of the pane height kept empty above the highest visible value. 0.1 keeps a 10 % gap at the top.
marginBottomnumber0.1Fraction of the pane height kept empty below the lowest visible value.
minMovenumber0Instrument 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.
invertedbooleanfalseFlips 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 }
FieldTypeDefaultDescription
barSpacingnumber8Pixels per bar at initialisation. Clamped to [minBarSpacing, maxBarSpacing] at all times.
minBarSpacingnumber1Hard floor on bar spacing, reached at maximum zoom-in.
maxBarSpacingnumber80Hard ceiling on bar spacing, reached at maximum zoom-out.
rightOffsetnumber4Empty 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 margin

Custom 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 default

The 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