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 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 resulttype: '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.
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)
},
})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 correctly