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 { createApp, defineComponent, ref } from "vue";
import { AgCharts } from "ag-charts-vue3";
import type { AgChartOptions } from "ag-charts-types";
import {
AreaSeriesModule,
BarSeriesModule,
CandlestickSeriesModule,
CrosshairModule,
LineSeriesModule,
ModuleRegistry,
NumberAxisModule,
OhlcSeriesModule,
RangeAreaSeriesModule,
RangeBarSeriesModule,
TimeAxisModule,
ZoomModule,
} from "ag-charts-enterprise";
import { createSeedData, generateNextDatum } from "./data";
import clone from "clone";
let initialPoints = 1000;
let batchSize = 10;
let currentSeriesType = "line";
let currentUpdateMode = "rolling";
const seedResult = createSeedData(initialPoints, currentSeriesType);
let data = seedResult.data;
let nextIndex = data.length;
let lastBasePrice = seedResult.lastBasePrice;
function createSeriesConfig(seriesType) {
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;
function updateBatchSize(value) {
batchSize = parseInt(value, 10);
}
function updateDataSize(value) {
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;
agCharts.value.chart.update(options);
if (wasRunning) startUpdates();
}
ModuleRegistry.registerModules([
AreaSeriesModule,
BarSeriesModule,
CandlestickSeriesModule,
CrosshairModule,
LineSeriesModule,
NumberAxisModule,
OhlcSeriesModule,
RangeAreaSeriesModule,
RangeBarSeriesModule,
TimeAxisModule,
ZoomModule,
]);
const ChartExample = defineComponent({
template: `
<div class="example-controls">
<div class="controls-row">
<button id="toggleBtn" v-on:click="toggleUpdates()">Start Updates</button>
<select id="seriesTypeSelect" v-on:change="updateSeriesType($event.target.value)">
<option value="line" selected="">Line</option>
<option value="area">Area</option>
<option value="bar">Bar</option>
<option value="stacked-bar">Stacked Bar</option>
<option value="stacked-area">Stacked Area</option>
<option value="range-area">Range Area</option>
<option value="range-bar">Range Bar</option>
<option value="candlestick">Candlestick</option>
<option value="ohlc">OHLC</option>
</select>
<select id="updateModeSelect" v-on:change="updateMode($event.target.value)">
<option value="rolling" selected="">Rolling Window</option>
<option value="append">Append Only</option>
</select>
</div>
</div>
<ag-charts
ref="agCharts"
:options="options"
/>
`,
components: {
"ag-charts": AgCharts,
},
setup(props) {
const options = ref<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 },
});
const agCharts = ref(null);
const runUpdate = () => {
if (!isRunning) return;
switch (currentUpdateMode) {
case "rolling":
performRollingUpdate();
break;
case "append":
performAppendUpdate();
break;
}
animationFrameId = requestAnimationFrame(runUpdate);
};
const performRollingUpdate = () => {
const newPoints = [];
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);
agCharts.value.chart.applyTransaction({
remove: pointsToRemove,
add: newPoints,
});
data.splice(0, batchSize);
data.push(...newPoints);
};
const performAppendUpdate = () => {
const newPoints = [];
for (let i = 0; i < batchSize; i++) {
const result = generateNextDatum(
nextIndex++,
currentSeriesType,
lastBasePrice,
);
newPoints.push(result.datum);
lastBasePrice = result.lastBasePrice;
}
agCharts.value.chart.applyTransaction({
add: newPoints,
});
data.push(...newPoints);
};
const startUpdates = () => {
if (isRunning) return;
isRunning = true;
updateButton();
animationFrameId = requestAnimationFrame(runUpdate);
};
const stopUpdates = () => {
if (!isRunning) return;
isRunning = false;
updateButton();
if (animationFrameId !== undefined) {
cancelAnimationFrame(animationFrameId);
animationFrameId = undefined;
}
};
const updateButton = () => {
const button = document.getElementById("toggleBtn");
if (button) {
button.textContent = isRunning ? "Stop Updates" : "Start Updates";
}
};
const toggleUpdates = () => {
if (isRunning) {
stopUpdates();
} else {
startUpdates();
}
};
const updateSeriesType = (value) => {
const optionsCopy = clone(options.value);
const wasRunning = isRunning;
if (wasRunning) stopUpdates();
currentSeriesType = value;
const seedResult = createSeedData(initialPoints, currentSeriesType);
data = seedResult.data;
nextIndex = data.length;
lastBasePrice = seedResult.lastBasePrice;
optionsCopy.data = data;
optionsCopy.series = createSeriesConfig(currentSeriesType);
if (wasRunning) startUpdates();
options.value = optionsCopy;
};
const updateMode = (value) => {
const optionsCopy = clone(options.value);
const wasRunning = isRunning;
if (wasRunning) stopUpdates();
currentUpdateMode = value;
const subtitleText =
currentUpdateMode === "rolling"
? "Rolling Window: removing old points, adding new points"
: "Append Only: continuously adding new points";
optionsCopy.subtitle = { text: subtitleText };
const seedResult = createSeedData(initialPoints, currentSeriesType);
data = seedResult.data;
nextIndex = data.length;
lastBasePrice = seedResult.lastBasePrice;
optionsCopy.data = data;
if (wasRunning) startUpdates();
options.value = optionsCopy;
};
return {
options,
agCharts,
toggleUpdates,
updateSeriesType,
updateMode,
};
},
});
createApp(ChartExample).mount("#app");
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 { createApp, defineComponent, ref } from "vue";
import { AgFinancialCharts } from "ag-charts-vue3";
import type { AgChartOptions } from "ag-charts-types";
import { FinancialChartModule, ModuleRegistry } from "ag-charts-enterprise";
import { MS_PER_DAY, PriceSimulator, getHistoricalData } from "./data";
import { random } from "./seededRandom";
import clone from "clone";
const CANDLE_INTERVAL_MS = 2000;
const TICKS_PER_CANDLE = 100;
const INITIAL_POINTS = 365;
const VISIBLE_POINTS = 25;
const data = getHistoricalData(INITIAL_POINTS);
let currentCandle;
let simulator;
let candleIntervalId;
let animationFrameId;
let isRunning = false;
ModuleRegistry.registerModules([FinancialChartModule]);
const ChartExample = defineComponent({
template: `
<div class="example-controls">
<div class="controls-row">
<button id="toggleBtn" v-on:click="toggleUpdates()">Start</button>
</div>
</div>
<ag-financial-charts
ref="agCharts"
:options="options"
/>
`,
components: {
"ag-financial-charts": AgFinancialCharts,
},
setup(props) {
const options = ref<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 },
},
},
});
const agCharts = ref(null);
const 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);
agCharts.value.chart.applyTransaction({ add: [currentCandle] });
};
const 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);
agCharts.value.chart.applyTransaction({ update: [currentCandle] });
};
const scheduleNextTick = () => {
if (isRunning) {
animationFrameId = requestAnimationFrame(() => {
processTick();
scheduleNextTick();
});
}
};
const stopAllUpdates = () => {
if (candleIntervalId) {
clearInterval(candleIntervalId);
candleIntervalId = undefined;
}
if (animationFrameId) {
cancelAnimationFrame(animationFrameId);
animationFrameId = undefined;
}
};
const updateButton = () => {
const button = document.getElementById("toggleBtn");
if (button) {
button.textContent = isRunning ? "Stop" : "Start";
}
};
const toggleUpdates = () => {
if (isRunning) {
isRunning = false;
stopAllUpdates();
} else {
isRunning = true;
startNewCandle();
scheduleNextTick();
candleIntervalId = setInterval(
() => startNewCandle(),
CANDLE_INTERVAL_MS,
);
}
updateButton();
};
return {
options,
agCharts,
toggleUpdates,
};
},
});
createApp(ChartExample).mount("#app");