Data loading
This page covers every way to get price data into a chart: bulk load, live updates, tick aggregation, infinite history paging, large-dataset downsampling, and gap recovery after a feed drop.
Initial load
Create a series, then call setData with a sorted array of bars:
import { createChart } from 'openalgo-charts';
const chart = createChart(el);
const series = chart.addSeries('candlestick');
series.setData(bars);
chart.timeScale.fitContent(bars.length);setData replaces the entire series in one pass. The first call also triggers
an automatic fit so the chart shows all bars without you needing to call
fitContent yourself, but it is good practice to call it explicitly whenever
you replace data.
time is UTC seconds (a plain integer), not a Date and not milliseconds.
Pass bars sorted ascending by time; the DataLayer re-sorts on its own, but
supplying pre-sorted input is faster.
Bars use the same shape across all series types:
interface Bar {
time: number; // UTC seconds
open: number;
high: number;
low: number;
close: number;
volume?: number;
}Line and area series read only close; you can supply the same bar objects to
every series. Each bar gets a gapless logical index on the shared time axis,
so weekends, holidays, and overnight breaks collapse automatically. See
Core Concepts for how the index space works.
Incremental live updates
series.update(bar) is the hot path for live data. Pass the current
in-progress bar on each tick; the chart decides what to do based on time:
Incoming time vs last bar | Result |
|---|---|
| Newer | Appends a new bar; auto-scrolls if the viewport is already at the right edge |
| Same | Replaces the last bar in place (intra-bar tick update) |
| Older | Inserts into history or replaces an existing bar at that time |
// Intra-bar update (same time -> replaces)
series.update({ time: currentBarTime, open, high, low, close, volume });
// New bar (newer time -> appends)
series.update({ time: nextBarTime, open: close, high: close, low: close, close, volume: 0 });Auto-scroll on append only fires when the user’s viewport is already at the
right edge (rightOffset >= 0). If the user has scrolled left into history,
new bars accumulate off-screen without disturbing the view.
Contrast this with setData, which does a full replace and resets the time
axis. Use update for ticks; use setData when you need to swap in a
completely different dataset (timeframe change, symbol change).
To compute the next update value from the current bar (for example, to
accumulate volume), read the live bar with getData:
// getData() returns bars sorted oldest -> newest
const bars = series.getData();
const last = bars[bars.length - 1];
series.update({ ...last, close: newPrice, high: Math.max(last.high, newPrice) });Live streaming demo
This chart streams: a timer ticks the forming candle a few times, then rolls a
new one, all through series.update. Clean the timer up on teardown so it stops
when the chart is destroyed.
const chart = lib.createChart(el);
const series = chart.addSeries('candlestick');
series.setData(lib.generateBars(1700000000, 90, 3600));
chart.timeScale.fitContent(90);
let last = series.getData().slice(-1)[0];
let t = last.time, ticks = 0, seed = 7;
const rnd = () => { seed = (seed * 1103515245 + 12345) & 0x7fffffff; return seed / 0x7fffffff; };
const id = setInterval(() => {
const move = (rnd() - 0.5) * 1.8;
if (ticks >= 6) { // roll a fresh candle
t += 3600;
last = { time: t, open: last.close, high: last.close, low: last.close, close: last.close + move };
ticks = 0;
} else { // update the forming candle in place
const c = last.close + move;
last = { time: last.time, open: last.open, high: Math.max(last.high, c), low: Math.min(last.low, c), close: c };
ticks++;
}
series.update(last);
}, 700);
// Stop the stream when the chart is torn down.
const destroy = chart.destroy.bind(chart);
chart.destroy = () => { clearInterval(id); destroy(); };
return chart;Building bars from a tick stream
When your WebSocket feed delivers raw trade ticks (price + quantity) rather
than completed candles, use CandleBuilder to bucket them into OHLC bars.
import { CandleBuilder } from 'openalgo-charts';
const builder = new CandleBuilder({
intervalSec: 60, // 1-minute bars
volumeMode: 'ltq-sum', // accumulate last-traded qty per tick
lateTickPolicy: 'foldIntoBar',
sessionAnchorSec: 0, // align buckets to epoch; set to session open for IST alignment
});
// Seed from the last historical bar so the first live tick continues it
const bars = await feed.getHistory(...);
series.setData(bars);
builder.seed(bars[bars.length - 1]);
// Feed each tick as it arrives
ws.onTick((tick) => {
// tick: { time: UTCSeconds, price: number, ltq?: number, cumDayVolume?: number }
const update = builder.onTick(tick);
if (update !== null) {
series.update(update.bar); // update.isNew === true on a bar boundary
}
});onTick returns null only when the lateTickPolicy is
'dropOlderThanPrevBar' and the tick predates the current bar’s open. The
CandleUpdate.isNew flag is true on the first tick of a new bucket (useful
for logging or triggering signal evaluation).
The volumeMode option controls volume accounting:
'ltq-sum'— add each tick’sltqfield (last-traded quantity).'day-delta'— diff a cumulative daily volume counter (cumDayVolume); handles intraday resets automatically.
Lazy / infinite history
When a user pans left past the oldest loaded bar, the chart fires a history loader so you can fetch and prepend older data without rebuilding the chart.
Register the loader once after creating the chart:
chart.setHistoryLoader(async () => {
const older = await myApi.getBarsBefore(oldestLoadedTime, 500);
if (older.length > 0) {
series.prependData(older);
oldestLoadedTime = older[0].time;
}
// Always call this, even when there is nothing left to load.
// Without it the loader will never fire again.
chart.historyLoadComplete();
});prependData merges the incoming bars into the existing series by time: new
timestamps are inserted and existing ones are replaced. The viewport is
preserved — the same bars stay on screen because the time-scale baseIndex
shifts to compensate for the prepended count.
You must call chart.historyLoadComplete() after every loader invocation,
whether or not new bars were returned. Omitting it latches the “loading” flag
permanently and paging stalls.
You can also listen to the 'lazy-load' event on the unified bus (for example,
to log or debounce independently of the loader function):
chart.on('lazy-load', (e) => {
console.log('paging backward from', e.from, 'to', e.to);
});See Events for the full event reference.
Live demo: infinite history
Seed the chart with 120 bars, then pan left — the loader prepends 60 more bars each time you reach the oldest edge.
const chart = lib.createChart(el);
const intervalSec = 3600;
const seedStart = 1700000000;
const bars = lib.generateBars(seedStart, 120, intervalSec);
const series = chart.addSeries('candlestick');
series.setData(bars);
chart.timeScale.fitContent(bars.length);
let pageStart = seedStart;
chart.setHistoryLoader(function () {
const olderStart = pageStart - 60 * intervalSec;
const olderBars = lib.generateBars(olderStart, 60, intervalSec);
series.prependData(olderBars);
pageStart = olderStart;
chart.historyLoadComplete();
});
return chart;Large datasets: conflation
When zoomed far out, many bars collapse to sub-pixel widths. Enable conflation to let the renderer merge groups of bars into a single OHLC-preserving drawn bar, which keeps rendering fast and keeps the candle shape meaningful:
const chart = createChart(el, {
conflate: true,
conflationFactor: 1, // default; raise to 2-4 for more aggressive merging
});Conflation is OHLC-preserving — open comes from the first bar in the
group, close from the last, high and low from the extremes, and volume is
summed. It kicks in automatically when barSpacing * devicePixelRatio falls
below the pixel threshold and is transparent to your data layer; the underlying
bars are unchanged.
The same merge helpers are also available for manual use:
import { conflateBars, mergeBars } from 'openalgo-charts';
const merged = conflateBars(bars, 5); // merge every 5 bars into one
const single = mergeBars(bars.slice(0, 5)); // merge a specific groupSee Core Concepts for the gapless index model that makes conflation align correctly.
Reconnect and gap recovery
When a WebSocket feed drops and reconnects, re-fetch the missed window and pass
it to setData or prependData. The DataLayer merges by time and
deduplicates: bars whose timestamps already exist in the series are replaced
(not duplicated), and genuinely new bars are inserted in sorted order. You do
not need to compute the exact gap — overlapping re-fetched windows are safe.
ws.on('reconnect', async () => {
// Fetch everything since the last known bar, with some overlap for safety
const missed = await myApi.getBars({ from: lastKnownTime - 60, to: Date.now() / 1000 });
// DataLayer upserts by time -- no duplicates, no manual de-overlap needed
series.prependData(missed);
});If the gap is large enough that you want to reload the full visible range
instead, use setData — it is a full replace and runs the same O(n log n)
sort that the initial load does.
Empty state
setData([]) is valid and renders an empty chart. You can add data later with
another setData call or a sequence of update calls.
const series = chart.addSeries('candlestick');
series.setData([]); // chart is blank; axes are visible
// Later, once data arrives:
series.setData(historicalBars);
chart.timeScale.fitContent(historicalBars.length);This is useful for showing a chart shell while an async fetch is in flight.