AG Charts is optimised for high-frequency data updates, maintaining smooth performance with rapid or real-time data updates on large datasets, while maintaining full functionality.
import {
AgCartesianChartOptions,
AgCartesianSeriesOptions,
AgCharts,
AreaSeriesModule,
BarSeriesModule,
CandlestickSeriesModule,
CrosshairModule,
LineSeriesModule,
ModuleRegistry,
NumberAxisModule,
OhlcSeriesModule,
RangeAreaSeriesModule,
RangeBarSeriesModule,
TimeAxisModule,
ZoomModule,
} from "ag-charts-enterprise";
import { Datum, SeriesType, createSeedData, generateNextDatum } from "./data";
let initialPoints = 1000;
let batchSize = 10;
let currentSeriesType: SeriesType = "line";
let currentUpdateMode: "rolling" | "append" = "rolling";
const seedResult = createSeedData(initialPoints, currentSeriesType);
let data: Datum[] = seedResult.data;
let nextIndex = data.length;
let lastBasePrice: number | undefined = seedResult.lastBasePrice;
function createSeriesConfig(
seriesType: SeriesType,
): AgCartesianSeriesOptions[] {
switch (seriesType) {
case "ohlc":
case "candlestick":
return [
{
type: seriesType,
xKey: "timestamp",
openKey: "open",
highKey: "high",
lowKey: "low",
closeKey: "close",
},
];
case "stacked-bar":
return [
{ type: "bar", xKey: "timestamp", yKey: "value", stacked: true },
{ type: "bar", xKey: "timestamp", yKey: "value2", stacked: true },
];
case "stacked-area":
return [
{
type: "area",
xKey: "timestamp",
yKey: "value",
stacked: true,
marker: { enabled: false },
},
{
type: "area",
xKey: "timestamp",
yKey: "value2",
stacked: true,
marker: { enabled: false },
},
];
case "range-area":
return [
{
type: "range-area",
xKey: "timestamp",
yLowKey: "low",
yHighKey: "high",
},
];
case "range-bar":
return [
{
type: "range-bar",
xKey: "timestamp",
yLowKey: "low",
yHighKey: "high",
},
];
case "area":
return [
{
type: "area",
xKey: "timestamp",
yKey: "value",
marker: { enabled: false },
strokeWidth: 1,
},
];
case "bar":
return [
{
type: "bar",
xKey: "timestamp",
yKey: "value",
},
];
case "line":
default:
return [
{
type: "line",
xKey: "timestamp",
yKey: "value",
marker: { enabled: false },
strokeWidth: 1,
},
];
}
}
let isRunning = false;
let animationFrameId: number | undefined;
function updateBatchSize(value: string) {
batchSize = parseInt(value, 10);
}
function updateDataSize(value: string) {
const wasRunning = isRunning;
if (wasRunning) stopUpdates();
initialPoints = parseInt(value, 10);
const seedResult = createSeedData(initialPoints, currentSeriesType);
data = seedResult.data;
nextIndex = data.length;
lastBasePrice = seedResult.lastBasePrice;
options.data = data;
chart.update(options);
if (wasRunning) startUpdates();
}
ModuleRegistry.registerModules([
AreaSeriesModule,
BarSeriesModule,
CandlestickSeriesModule,
CrosshairModule,
LineSeriesModule,
NumberAxisModule,
OhlcSeriesModule,
RangeAreaSeriesModule,
RangeBarSeriesModule,
TimeAxisModule,
ZoomModule,
]);
const options: AgCartesianChartOptions = {
data,
title: { text: "High-Frequency Data Updates" },
subtitle: { text: "Rolling Window: removing old points, adding new points" },
animation: { enabled: false },
zoom: { enabled: true, onDataChange: { strategy: "preserveRatios" } },
axes: {
x: {
type: "time",
position: "bottom",
nice: false,
label: {
format: "%H:%M:%S",
},
},
},
series: createSeriesConfig(currentSeriesType),
legend: { enabled: false },
};
options.container = document.getElementById("myChart");
const chart = AgCharts.create(options);
function runUpdate() {
if (!isRunning) return;
switch (currentUpdateMode) {
case "rolling":
performRollingUpdate();
break;
case "append":
performAppendUpdate();
break;
}
animationFrameId = requestAnimationFrame(runUpdate);
}
function performRollingUpdate() {
const newPoints: Datum[] = [];
for (let i = 0; i < batchSize; i++) {
const result = generateNextDatum(
nextIndex++,
currentSeriesType,
lastBasePrice,
);
newPoints.push(result.datum);
lastBasePrice = result.lastBasePrice;
}
const pointsToRemove = data.slice(0, batchSize);
chart.applyTransaction({
remove: pointsToRemove,
add: newPoints,
});
data.splice(0, batchSize);
data.push(...newPoints);
}
function performAppendUpdate() {
const newPoints: Datum[] = [];
for (let i = 0; i < batchSize; i++) {
const result = generateNextDatum(
nextIndex++,
currentSeriesType,
lastBasePrice,
);
newPoints.push(result.datum);
lastBasePrice = result.lastBasePrice;
}
chart.applyTransaction({
add: newPoints,
});
data.push(...newPoints);
}
function startUpdates() {
if (isRunning) return;
isRunning = true;
updateButton();
animationFrameId = requestAnimationFrame(runUpdate);
}
function stopUpdates() {
if (!isRunning) return;
isRunning = false;
updateButton();
if (animationFrameId !== undefined) {
cancelAnimationFrame(animationFrameId);
animationFrameId = undefined;
}
}
function updateButton() {
const button = document.getElementById("toggleBtn");
if (button) {
button.textContent = isRunning ? "Stop Updates" : "Start Updates";
}
}
function toggleUpdates() {
if (isRunning) {
stopUpdates();
} else {
startUpdates();
}
}
function updateSeriesType(value: string) {
const wasRunning = isRunning;
if (wasRunning) stopUpdates();
currentSeriesType = value as SeriesType;
const seedResult = createSeedData(initialPoints, currentSeriesType);
data = seedResult.data;
nextIndex = data.length;
lastBasePrice = seedResult.lastBasePrice;
options.data = data;
options.series = createSeriesConfig(currentSeriesType);
if (wasRunning) startUpdates();
chart.update(options);
}
function updateMode(value: string) {
const wasRunning = isRunning;
if (wasRunning) stopUpdates();
currentUpdateMode = value as "rolling" | "append";
const subtitleText =
currentUpdateMode === "rolling"
? "Rolling Window: removing old points, adding new points"
: "Append Only: continuously adding new points";
options.subtitle = { text: subtitleText };
const seedResult = createSeedData(initialPoints, currentSeriesType);
data = seedResult.data;
nextIndex = data.length;
lastBasePrice = seedResult.lastBasePrice;
options.data = data;
if (wasRunning) startUpdates();
chart.update(options);
}
if (typeof window !== "undefined") {
// Attach external event handlers to window so they can be called from index.html
(<any>window).toggleUpdates = toggleUpdates;
(<any>window).updateSeriesType = updateSeriesType;
(<any>window).updateMode = updateMode;
}
const DATA_INTERVAL_MS = 250;
const START_TIMESTAMP = Date.UTC(2024, 0, 1, 0, 0, 0);
export type SeriesType =
| "line"
| "area"
| "bar"
| "stacked-bar"
| "stacked-area"
| "range-area"
| "range-bar"
| "candlestick"
| "ohlc";
export type ValueDatum = {
timestamp: number;
value: number;
value2?: number;
};
export type OhlcDatum = {
timestamp: number;
open: number;
high: number;
low: number;
close: number;
};
export type RangeDatum = {
timestamp: number;
low: number;
high: number;
};
export type Datum = ValueDatum | OhlcDatum | RangeDatum;
function generateValueDatum(
index: number,
includeValue2: boolean = false,
): ValueDatum {
const timestamp = START_TIMESTAMP + index * DATA_INTERVAL_MS;
const trend = Math.sin(index / 240) * 40 + Math.cos(index / 80) * 25;
const volatility = Math.sin(index / 15) * 5;
const baseline = 1000 + index * 0.02;
const datum: ValueDatum = {
timestamp,
value: Number((baseline + trend + volatility).toFixed(2)),
};
if (includeValue2) {
const trend2 = Math.cos(index / 200) * 30 + Math.sin(index / 60) * 20;
datum.value2 = Number((baseline + trend2 + volatility * 0.8).toFixed(2));
}
return datum;
}
function generateOhlcDatum(
index: number,
previousClose?: number,
): { datum: OhlcDatum; basePrice: number } {
const timestamp = START_TIMESTAMP + index * DATA_INTERVAL_MS;
const trend = Math.sin(index / 240) * 40 + Math.cos(index / 80) * 25;
const volatility = Math.sin(index / 15) * 5;
const baseline = 1000 + index * 0.02;
const midPrice = baseline + trend + volatility;
const open = previousClose ?? midPrice;
const closeOffset = Math.sin(index / 7) * 2 + Math.cos(index / 11) * 1.5;
const close = Number((midPrice + closeOffset).toFixed(2));
const range = 2 + Math.abs(Math.sin(index / 13)) * 3;
const high = Number((Math.max(open, close) + range).toFixed(2));
const low = Number((Math.min(open, close) - range).toFixed(2));
return {
datum: {
timestamp,
open: Number(open.toFixed(2)),
high,
low,
close,
},
basePrice: close,
};
}
function generateRangeDatum(index: number): RangeDatum {
const timestamp = START_TIMESTAMP + index * DATA_INTERVAL_MS;
const trend = Math.sin(index / 240) * 40 + Math.cos(index / 80) * 25;
const volatility = Math.sin(index / 15) * 5;
const baseline = 1000 + index * 0.02;
const midValue = baseline + trend + volatility;
const range = 10 + Math.abs(Math.sin(index / 30)) * 20;
return {
timestamp,
low: Number((midValue - range / 2).toFixed(2)),
high: Number((midValue + range / 2).toFixed(2)),
};
}
export function createSeedData(
count: number,
seriesType: SeriesType,
): { data: Datum[]; lastBasePrice?: number } {
const result: Datum[] = [];
let basePrice: number | undefined = undefined;
const isStacked =
seriesType === "stacked-bar" || seriesType === "stacked-area";
const isRange = seriesType === "range-area" || seriesType === "range-bar";
for (let i = 0; i < count; i++) {
if (seriesType === "ohlc" || seriesType === "candlestick") {
const { datum, basePrice: newBasePrice } = generateOhlcDatum(
i,
basePrice,
);
result.push(datum);
basePrice = newBasePrice;
} else if (isRange) {
result.push(generateRangeDatum(i));
} else {
result.push(generateValueDatum(i, isStacked));
}
}
return { data: result, lastBasePrice: basePrice };
}
export function generateNextDatum(
index: number,
seriesType: SeriesType,
previousClose?: number,
): { datum: Datum; lastBasePrice?: number } {
const isStacked =
seriesType === "stacked-bar" || seriesType === "stacked-area";
const isRange = seriesType === "range-area" || seriesType === "range-bar";
if (seriesType === "ohlc" || seriesType === "candlestick") {
const { datum, basePrice } = generateOhlcDatum(index, previousClose);
return { datum, lastBasePrice: basePrice };
} else if (isRange) {
return { datum: generateRangeDatum(index), lastBasePrice: undefined };
} else {
return {
datum: generateValueDatum(index, isStacked),
lastBasePrice: undefined,
};
}
}
In the above example:
- Use the controls to select different series types.
- Choose between update modes: Rolling Window (remove old, add new) or Append Only (continuously add).
- Updates run at
requestAnimationFramespeed for maximum throughput. - The zoom functionality is available during the updates.
Performance may vary based on your specific use case, environment and hardware.
How it works Copy Link
The applyTransaction() API provides efficient incremental updates to chart data. Instead of replacing the entire dataset on each update, transactions specify only the changes: items to add, remove, or update. This dramatically reduces processing overhead for real-time scenarios.
// Rolling window: remove old points, add new ones
chart.applyTransaction({
remove: oldPoints,
add: newPoints,
});For detailed information on transaction operations and best practices, see Transactions.
Financial Charts Copy Link
High-frequency updates also work with Financial Charts.
The following example simulates real-time candlestick trading data:
- Starts with 365 days of historical data.
- Every 2 seconds, a new candle is created representing a new trading day.
- Between new candles, price ticks update the current candle's high, low, and close values at
requestAnimationFramespeed.
import {
AgCharts,
AgFinancialChartOptions,
FinancialChartModule,
ModuleRegistry,
} from "ag-charts-enterprise";
import { Candle, MS_PER_DAY, PriceSimulator, getHistoricalData } from "./data";
import { random } from "./seededRandom";
const CANDLE_INTERVAL_MS = 2000;
const TICKS_PER_CANDLE = 100;
const INITIAL_POINTS = 365;
const VISIBLE_POINTS = 25;
const data: Candle[] = getHistoricalData(INITIAL_POINTS);
let currentCandle: Candle | undefined;
let simulator: PriceSimulator | undefined;
let candleIntervalId: ReturnType<typeof setInterval> | undefined;
let animationFrameId: number | undefined;
let isRunning = false;
ModuleRegistry.registerModules([FinancialChartModule]);
const options: AgFinancialChartOptions = {
title: { text: "High-Frequency Update" },
data,
volume: true,
navigator: false,
rangeButtons: false,
statusBar: true,
toolbar: false,
zoom: true,
initialState: {
zoom: {
ratioX: { start: 1 - VISIBLE_POINTS / data.length, end: 1 },
},
},
};
options.container = document.getElementById("myChart");
const chart = AgCharts.createFinancialChart(options);
function startNewCandle() {
const lastCandle = data[data.length - 1];
const newTimestamp = lastCandle.date + MS_PER_DAY;
const openPrice = lastCandle.close;
simulator = new PriceSimulator(openPrice, TICKS_PER_CANDLE);
currentCandle = {
date: newTimestamp,
open: openPrice,
high: openPrice,
low: openPrice,
close: openPrice,
volume: Math.round(1000000 + random() * 500000),
};
data.push(currentCandle);
chart.applyTransaction({ add: [currentCandle] });
}
function processTick() {
if (!currentCandle || !simulator) return;
const newPrice = simulator.tick();
currentCandle.close = newPrice;
currentCandle.high = Math.max(currentCandle.high, newPrice);
currentCandle.low = Math.min(currentCandle.low, newPrice);
chart.applyTransaction({ update: [currentCandle] });
}
function scheduleNextTick() {
if (isRunning) {
animationFrameId = requestAnimationFrame(() => {
processTick();
scheduleNextTick();
});
}
}
function stopAllUpdates() {
if (candleIntervalId) {
clearInterval(candleIntervalId);
candleIntervalId = undefined;
}
if (animationFrameId) {
cancelAnimationFrame(animationFrameId);
animationFrameId = undefined;
}
}
function updateButton() {
const button = document.getElementById("toggleBtn");
if (button) {
button.textContent = isRunning ? "Stop" : "Start";
}
}
function toggleUpdates() {
if (isRunning) {
isRunning = false;
stopAllUpdates();
} else {
isRunning = true;
startNewCandle();
scheduleNextTick();
candleIntervalId = setInterval(() => startNewCandle(), CANDLE_INTERVAL_MS);
}
updateButton();
}
if (typeof window !== "undefined") {
// Attach external event handlers to window so they can be called from index.html
(<any>window).toggleUpdates = toggleUpdates;
}
import { random } from "./seededRandom";
const STARTING_PRICE = 185;
const DAILY_VOLATILITY = 0.015;
const DAILY_DRIFT = 0.0001;
export const MS_PER_DAY = 24 * 60 * 60 * 1000;
function randomNormal() {
const u1 = random();
const u2 = random();
return Math.sqrt(-2 * Math.log(u1)) * Math.cos(2 * Math.PI * u2);
}
export interface Candle {
date: number;
open: number;
high: number;
low: number;
close: number;
volume: number;
}
function generateCandle(prevClose: number, date: number): Candle {
const open = prevClose * (1 + (random() - 0.5) * 0.002);
let price = open;
let high = open;
let low = open;
const tickCount = 100;
const tickVol = DAILY_VOLATILITY / Math.sqrt(tickCount);
const tickDrift = DAILY_DRIFT / tickCount;
for (let i = 0; i < tickCount; i++) {
const change = price * (tickDrift + tickVol * randomNormal());
price = Math.max(price + change, 1);
high = Math.max(high, price);
low = Math.min(low, price);
}
return {
date,
open: Number(open.toFixed(2)),
high: Number(high.toFixed(2)),
low: Number(low.toFixed(2)),
close: Number(price.toFixed(2)),
volume: Math.round(1000000 + random() * 500000),
};
}
export function getHistoricalData(days: number): Candle[] {
const data: Candle[] = [];
const startTimestamp = Date.UTC(2024, 0, 1);
let prevClose = STARTING_PRICE;
for (let i = 0; i < days; i++) {
const date = startTimestamp + i * MS_PER_DAY;
const candle = generateCandle(prevClose, date);
data.push(candle);
prevClose = candle.close;
}
return data;
}
export class PriceSimulator {
private price: number;
private readonly tickVolatility: number;
private readonly tickDrift: number;
constructor(startPrice: number, ticksPerCandle: number) {
this.price = startPrice;
this.tickVolatility = DAILY_VOLATILITY / Math.sqrt(ticksPerCandle);
this.tickDrift = DAILY_DRIFT / ticksPerCandle;
}
tick(): number {
const change =
this.price * (this.tickDrift + this.tickVolatility * randomNormal());
this.price = Math.max(this.price + change, 1);
return Number(this.price.toFixed(2));
}
}
export function createSeededRandom(seed = 42): () => number {
let state = seed;
return () => {
state = (state * 16807) % 2147483647;
return (state - 1) / 2147483646;
};
}
export const random = createSeededRandom();