timestamp must be a millisecond timestamp• timestamp, open, close, high, low, volume, and turnover must all be numeric typesThis document explains how to integrate historical and real-time data into the chart.
The core of data integration is:
setSymbol(...) to set the symbolsetPeriod(...) to set the periodsetDataLoader(...) to set the data loaderAmong the three functions inside setDataLoader(...):
getBars: returns historical data, used for both initialization and paginationsubscribeBar: starts pushing the latest data after historical data has finished loadingunsubscribeBar: stops the real-time subscription when switching symbol, switching period, or destroying the chartIn real projects, you will usually encounter the following data source combinations:
getBars requests the REST APIsubscribeBar subscribes to WebSocketsubscribeBar uses setInterval internally to fetch the latest record regularlygetBars slices data from a local arraysubscribeBar pushes the next bar as time advancesNo matter where your data comes from, it eventually comes down to the same thing:
KLineData[]KLineDataThe historical data received by the chart must follow a fixed format. Both historical and real-time data in setDataLoader eventually need to be converted into this structure:
{
// Timestamp in milliseconds, required field
timestamp: number
// Open price, required field
open: number
// Close price, required field
close: number
// High price, required field
high: number
// Low price, required field
low: number
// Volume, optional field
volume: number
// Turnover, optional field. Required if you need to display 'EMV' and 'AVP'
turnover: number
}timestamp must be a millisecond timestamp• timestamp, open, close, high, low, volume, and turnover must all be numeric typesYour backend fields usually will not exactly match KLineData, so in most cases you should normalize them first.
Assume the backend returns:
{
t: 1711425600,
o: '68000.1',
h: '68920.5',
l: '67500.2',
c: '68610.8',
v: '1234.56'
}It can be mapped like this:
function normalizeToKLineData(data: any) {
return {
timestamp: data.t * 1000,
open: Number(data.o),
high: Number(data.h),
low: Number(data.l),
close: Number(data.c),
volume: Number(data.v),
}
}If your API returns an array, it is also recommended to apply map(normalizeToKLineData) before calling callback(...).
Among the three functions in setDataLoader({ getBars, subscribeBar, unsubscribeBar }), getBars must be implemented. If you do not need real-time updates, you may leave out subscribeBar and unsubscribeBar.
getBars is triggered only after the chart has confirmed that symbol and period are set, and the visible area requires data.The data flow inside the chart is a fixed pipeline. Once you understand it, you will no longer be confused about "when data comes in and when it updates":
setSymbol(symbol) + setPeriod(period) + setDataLoader(loader)
│ The chart starts the first data load only after all three are ready
▼
getBars({ type: 'init', timestamp: null, ... })
│ Fetch historical data and return it to the chart through callback(data, more)
▼
The chart renders the historical data and automatically calls subscribeBar({ symbol, period, callback })
│ Afterwards, every time the real-time source receives a record
▼
subscribeBar's callback(data) ──► the chart merges it into the last bar by timestampA few notes:
setDataLoader(...) without setSymbol(...) / setPeriod(...), getBars will never be triggered and the chart will stay blank.subscribeBar yourself; the chart calls it automatically once the init callback has finished.setSymbol / setPeriod / resetData again re-runs this pipeline: unsubscribeBar fires first, then a new getBars({ type: 'init' }) starts.setSymbol only ticker is required; pricePrecision / volumePrecision may be omitted, in which case the default values 2 and 0 are used.The getBars function in setDataLoader is responsible for fetching and returning historical data when needed.
The signature of getBars comes from the chart's internal data loading contract:
getBars: ({
type,
timestamp,
symbol,
period,
callback
}: DataLoaderGetBarsParams) => void | Promise<void>You can understand it as:
callback(...)Key meanings:
typeinit: triggered after initialization or after switching symbol/period. At this time timestamp = null.forward: used to load older data on the left boundary, usually triggered when dragging to the left boundary.backward: used to load newer data on the right boundary, usually triggered when dragging to the right boundary.timestampforward: usually the timestamp of the current leftmost barbackward: usually the timestamp of the current rightmost barinit: nullcallback(data, more)data: KLineData[]more: tells the chart whether there is more data on the left or right boundary boolean to mean both sides are the same{ forward?: boolean, backward?: boolean } to control each side separatelyThe most common implementations are:
init: load a recent chunk of historical dataforward: load older data using the left boundary timestampbackward: load newer data using the right boundary timestampIf your API only supports backward pagination in one direction, you can start by correctly handling only init and forward.
more Should Be Returned The purpose of more is not to tell the chart "how much data was returned this time", but to tell it "whether there is more data in this direction".
For example:
callback(bars, { forward: true })callback(bars, { forward: false })callback(bars, false)A practical rule is:
hasMore or nextCursor, prefer the backend resultAlso note that the two directions of more are independent. Returning false for one direction only means that direction has no more data; it does not affect boundary loading in the other direction. Even if one direction is exhausted, pagination in the other direction will still be triggered normally.
type: 'init': clears existing data and replaces it with the new array.type: 'forward': prepends the new data to the front of the array to fill older bars on the left.type: 'backward': appends the new data to the end of the array to fill newer bars on the right.more only affects whether future left/right pagination can continue to be triggered.getBarsThe chart calls subscribeBar only after the init callback of getBars has completed, which means after historical data is ready.
If your real-time source provides individual trades instead of bars for the current period, use Data Aggregator to aggregate trades into K-line data across multiple periods before pushing them to the chart through
subscribeBar.
The signature of subscribeBar:
subscribeBar: ({
symbol,
period,
callback
}: DataLoaderSubscribeBarParams) => voidWhere:
callback(data: KLineData): when your real-time source receives one data record, normalize it into KLineData and return it to the chart.data.timestamp is a millisecond timestamp.• What you push is the record corresponding to the current period, not arbitrary trade details.• When time enters the next period, the newly pushed data should use a new timestamp.When the chart receives one real-time K-line record, it merges it with the current last record based on data.timestamp:
data.timestamp is greater: append it as a new last recorddata.timestamp is the same: overwrite the last record with the new valuedata.timestamp is smaller: treat it as old data and ignore it without insertingWhen you call setSymbol, setPeriod, resetData, or dispose to reset or destroy the chart, the chart internally triggers unsubscribeBar.
Best practice:
dataLoader sidesubscribeBar creates the subscription and stores the cleanup functionunsubscribeBar retrieves the corresponding cleanup function and stops the pushThe following example shows the typical idea of "REST for history + WebSocket for real-time":
chart.setDataLoader({
async getBars({ type, timestamp, symbol, period, callback }) {
const response = await api.getKlineList({
symbol: symbol.ticker,
period: `${period.span}${period.type}`,
endTime: timestamp ?? Date.now(),
limit: 500,
direction: type,
})
const bars = response.list
.map(normalizeToKLineData)
.sort((a, b) => a.timestamp - b.timestamp)
callback(bars, {
forward: response.hasMoreBefore,
backward: response.hasMoreAfter,
})
},
subscribeBar({ symbol, period, callback }) {
const key = makeKey(symbol, period)
const ws = createWsConnection(symbol.ticker, period)
ws.onmessage = (message) => {
const bar = normalizeToKLineData(JSON.parse(message.data))
callback(bar)
}
stopMap.set(key, () => ws.close())
},
unsubscribeBar({ symbol, period }) {
const key = makeKey(symbol, period)
stopMap.get(key)?.()
stopMap.delete(key)
},
})The v9 methods applyNewData(...) and updateData(...) have been removed in v10. In v10, data integration and management go through setDataLoader only. If you are migrating from an older version, see v9 to v10 and replace these two methods with setDataLoader.
more Causes a Loading Loop If the callback of getBars is invoked synchronously and always returns more: true, then when all the data fits in the visible area at once, the chart will keep triggering loads at the boundary, forming a "loading loop" — the chart keeps requesting data and may even scroll back and forth. Recommendations:
callback (start the request first, call back when the response arrives), so the next load is not triggered within the same event loopmore honestly: use false when a direction has no more datagetBars is triggered only when dataLoader, symbol, and period are all ready. If you only call setDataLoader(...) and forget setSymbol(...) and setPeriod(...), loading never starts and the chart gives no hint about it.
getBars definitely calls callback(data) and returns KLineData[]timestamp is in millisecondssetSymbol and setPeriod have been setmore.forward/backward is returned correctly in callback(bars, more)subscribeBar really calls callback(KLineData)timestamp you push is smaller than the latest record's timestampvolume and turnover are passed correctlysubscribeBarchart.subscribeAction('onVisibleRangeChange', ...), see subscribeAction