Flash on Update signals that chart data has changed by briefly overlaying a flash animation, drawing the user's attention to the chart whenever data is updated, added or removed.
Enabling Flash On Update Copy Link
Flash on Update is disabled by default. To enable it, set flashOnUpdate.enabled to true.
import {
AgCartesianChartOptions,
AgCharts,
CandlestickSeriesModule,
ContextMenuModule,
CrosshairModule,
FlashOnUpdateModule,
LegendModule,
ModuleRegistry,
NumberAxisModule,
OrdinalTimeAxisModule,
} from "ag-charts-enterprise";
import { applyLiveUpdate, getInitialData } from "./data";
ModuleRegistry.registerModules([
CandlestickSeriesModule,
ContextMenuModule,
CrosshairModule,
FlashOnUpdateModule,
LegendModule,
NumberAxisModule,
OrdinalTimeAxisModule,
]);
const options: AgCartesianChartOptions = {
data: getInitialData(),
title: {
text: "AAPL Stock Price",
},
series: [
{
type: "candlestick",
xKey: "date",
xName: "Date",
openKey: "open",
highKey: "high",
lowKey: "low",
closeKey: "close",
},
],
axes: {
y: {
type: "number",
label: {
formatter: ({ value }) => `$${Number(value).toFixed(0)}`,
},
},
},
flashOnUpdate: {
enabled: true,
},
};
options.container = document.getElementById("myChart");
const chart = AgCharts.create(options);
function update() {
options.data = applyLiveUpdate(options.data!);
chart.update(options);
}
if (typeof window !== "undefined") {
// Attach external event handlers to window so they can be called from index.html
(<any>window).update = update;
}
import { createSeededRandom, random } from "./seededRandom";
export interface DataType {
date: Date;
open: number;
high: number;
low: number;
close: number;
}
function addBusinessDays(start: Date, days: number): Date {
const result = new Date(start);
let added = 0;
while (added < days) {
result.setDate(result.getDate() + 1);
const day = result.getDay();
if (day !== 0 && day !== 6) added++;
}
return result;
}
export function getInitialData(): DataType[] {
const seededRng = createSeededRandom(1234);
const data: DataType[] = [];
const startDate = new Date("2025-01-27");
let previousClose = 228.5;
for (let i = 0; i < 30; i++) {
const date = i === 0 ? new Date(startDate) : addBusinessDays(startDate, i);
const dailyChange = (seededRng() - 0.5) * 6;
const open = previousClose + (seededRng() - 0.5) * 1.5;
const close = open + dailyChange;
const wickUp = seededRng() * 2;
const wickDown = seededRng() * 2;
const high = Math.max(open, close) + wickUp;
const low = Math.min(open, close) - wickDown;
data.push({
date,
open: Math.round(open * 100) / 100,
high: Math.round(high * 100) / 100,
low: Math.round(low * 100) / 100,
close: Math.round(close * 100) / 100,
});
previousClose = close;
}
return data;
}
export function applyLiveUpdate(data: DataType[]): DataType[] {
const result = data.map((d) => ({ ...d }));
const last = result[result.length - 1];
const tick = (random() - 0.5) * 3;
last.close = Math.round((last.close + tick) * 100) / 100;
last.high = Math.round(Math.max(last.high, last.close) * 100) / 100;
last.low = Math.round(Math.min(last.low, last.close) * 100) / 100;
return result;
}
export function createSeededRandom(seed = 42): () => number {
let state = seed;
return () => {
state = (state * 16807) % 2147483647;
return (state - 1) / 2147483646;
};
}
export const random = createSeededRandom();
{
flashOnUpdate: {
enabled: true,
},
} Category Flash Copy Link
Flashing just the category band makes it easier to identify which categories have changed. Set item to 'category' to enable this.
import {
BarSeriesModule,
CategoryAxisModule,
LegendModule,
ModuleRegistry,
NumberAxisModule,
} from "ag-charts-community";
import {
AgCartesianChartOptions,
AgCharts,
ContextMenuModule,
CrosshairModule,
FlashOnUpdateModule,
} from "ag-charts-enterprise";
import { DataType, applyRandomUpdate, getInitialData } from "./data";
let data: DataType[] = getInitialData();
let isRunning = false;
let updateInterval: ReturnType<typeof setInterval> | undefined;
ModuleRegistry.registerModules([
BarSeriesModule,
CategoryAxisModule,
ContextMenuModule,
CrosshairModule,
FlashOnUpdateModule,
LegendModule,
NumberAxisModule,
]);
const options: AgCartesianChartOptions<DataType> = {
data,
title: {
text: "Stock Trading Volume",
},
series: [
{
type: "bar",
xKey: "ticker",
yKey: "buyVolume",
yName: "Buy Volume (M)",
stacked: true,
},
{
type: "bar",
xKey: "ticker",
yKey: "sellVolume",
yName: "Sell Volume (M)",
stacked: true,
},
],
axes: {
x: {
type: "category",
label: {
autoRotate: false,
},
},
},
flashOnUpdate: {
enabled: true,
item: "category",
},
};
options.container = document.getElementById("myChart");
const chart = AgCharts.create(options);
function update() {
data = applyRandomUpdate(data);
options.data = data;
chart.update(options);
}
function startUpdates() {
if (isRunning) return;
isRunning = true;
updateButton();
update();
updateInterval = setInterval(update, 2000);
}
function stopUpdates() {
if (!isRunning) return;
isRunning = false;
updateButton();
if (updateInterval) {
clearInterval(updateInterval);
updateInterval = undefined;
}
}
function updateButton() {
const button = document.getElementById("toggleBtn");
if (button) {
button.textContent = isRunning ? "Stop Updates" : "Start Updates";
}
}
function toggleUpdates() {
if (isRunning) {
stopUpdates();
} else {
startUpdates();
}
}
if (typeof window !== "undefined") {
// Attach external event handlers to window so they can be called from index.html
(<any>window).toggleUpdates = toggleUpdates;
}
export interface DataType {
ticker: string;
buyVolume: number;
sellVolume: number;
}
const TICKERS = [
"AAPL",
"MSFT",
"GOOGL",
"AMZN",
"NVDA",
"META",
"TSLA",
"NFLX",
"AMD",
"CRM",
];
const BASE_VALUES: Record<string, Omit<DataType, "ticker">> = {
AAPL: { buyVolume: 28, sellVolume: 24 },
MSFT: { buyVolume: 18, sellVolume: 15 },
GOOGL: { buyVolume: 12, sellVolume: 10 },
AMZN: { buyVolume: 22, sellVolume: 19 },
NVDA: { buyVolume: 35, sellVolume: 30 },
META: { buyVolume: 15, sellVolume: 12 },
TSLA: { buyVolume: 42, sellVolume: 38 },
NFLX: { buyVolume: 8, sellVolume: 6 },
AMD: { buyVolume: 25, sellVolume: 22 },
CRM: { buyVolume: 5, sellVolume: 4 },
};
let seed = NaN;
function random() {
seed = (seed * 16807) % 2147483647;
return (seed - 1) / 2147483646;
}
export function getInitialData(): DataType[] {
seed = 1234;
return TICKERS.map((ticker) => {
const base = BASE_VALUES[ticker];
const vary = (n: number) =>
Math.max(1, Math.round(n + (random() - 0.5) * n * 0.3));
return {
ticker,
buyVolume: vary(base.buyVolume),
sellVolume: vary(base.sellVolume),
};
});
}
export function applyRandomUpdate(data: DataType[]): DataType[] {
const count = 1 + Math.floor(random() * 3);
const indices = new Set<number>();
while (indices.size < count) {
indices.add(Math.floor(random() * data.length));
}
return data.map((item, i) => {
if (!indices.has(i)) return item;
const vary = (n: number) =>
Math.max(1, Math.round(n + (random() - 0.5) * n * 0.3));
return {
...item,
buyVolume: vary(item.buyVolume),
sellVolume: vary(item.sellVolume),
};
});
}
{
flashOnUpdate: {
enabled: true,
item: 'category',
},
}In this example:
- Click 'Start Updates' to begin a simulated data feed that updates 1–3 bars every 2 seconds.
- Only the changed bars flash.
- Click Stop Updates to pause the feed.
- Category Flash is only available on Category, Grouped Category, Unit Time and Ordinal Time axes.
Adding and Removing Data Copy Link
Category flash is also triggered when a category is added to the chart. Removing a category does not trigger a flash.
import {
BarSeriesModule,
CategoryAxisModule,
LegendModule,
ModuleRegistry,
NumberAxisModule,
} from "ag-charts-community";
import {
AgCartesianChartOptions,
AgCharts,
ContextMenuModule,
CrosshairModule,
FlashOnUpdateModule,
} from "ag-charts-enterprise";
import { DataType, applyUpdate, getInitialData, getNextSector } from "./data";
ModuleRegistry.registerModules([
BarSeriesModule,
CategoryAxisModule,
ContextMenuModule,
CrosshairModule,
FlashOnUpdateModule,
LegendModule,
NumberAxisModule,
]);
const options: AgCartesianChartOptions<DataType> = {
data: getInitialData(),
title: {
text: "Sector Trading Activity",
},
series: [
{
type: "bar",
xKey: "sector",
yKey: "institutional",
yName: "Institutional ($M)",
stacked: true,
},
{
type: "bar",
xKey: "sector",
yKey: "retail",
yName: "Retail ($M)",
stacked: true,
},
{
type: "bar",
xKey: "sector",
yKey: "etfFlows",
yName: "ETF Flows ($M)",
stacked: true,
},
],
axes: {
x: {
type: "category",
},
},
flashOnUpdate: {
enabled: true,
item: "category",
},
};
options.container = document.getElementById("myChart");
const chart = AgCharts.create(options);
function addSector() {
const next = getNextSector(options.data!);
if (!next) return;
options.data = [...options.data!, { ...next }];
chart.update(options);
}
function removeSector() {
if (options.data!.length <= 2) return;
options.data = options.data!.slice(0, -1);
chart.update(options);
}
function updateSectors() {
options.data = applyUpdate(options.data!);
chart.update(options);
}
if (typeof window !== "undefined") {
// Attach external event handlers to window so they can be called from index.html
(<any>window).addSector = addSector;
(<any>window).removeSector = removeSector;
(<any>window).updateSectors = updateSectors;
}
import { random } from "./seededRandom";
export interface DataType {
sector: string;
institutional: number;
retail: number;
etfFlows: number;
}
const ALL_SECTORS: DataType[] = [
{ sector: "Technology", institutional: 850, retail: 320, etfFlows: 180 },
{ sector: "Healthcare", institutional: 420, retail: 150, etfFlows: 95 },
{ sector: "Finance", institutional: 680, retail: 240, etfFlows: 150 },
{ sector: "Energy", institutional: 380, retail: 180, etfFlows: 85 },
{ sector: "Consumer", institutional: 520, retail: 280, etfFlows: 120 },
{ sector: "Industrial", institutional: 350, retail: 120, etfFlows: 70 },
{ sector: "Materials", institutional: 180, retail: 80, etfFlows: 45 },
{ sector: "Utilities", institutional: 150, retail: 60, etfFlows: 35 },
];
export function getInitialData(): DataType[] {
return ALL_SECTORS.slice(0, 5).map((d) => ({ ...d }));
}
export function getNextSector(data: DataType[]): DataType | undefined {
const names = new Set(data.map((d) => d.sector));
return ALL_SECTORS.find((s) => !names.has(s.sector));
}
export function applyUpdate(data: DataType[]): DataType[] {
const count = 1 + Math.floor(random() * 2);
const indices = new Set<number>();
while (indices.size < count) {
indices.add(Math.floor(random() * data.length));
}
return data.map((item, i) => {
if (!indices.has(i)) return item;
const vary = (n: number) =>
Math.max(10, Math.round(n + (random() - 0.5) * n * 0.4));
return {
...item,
institutional: vary(item.institutional),
retail: vary(item.retail),
etfFlows: vary(item.etfFlows),
};
});
}
export function createSeededRandom(seed = 42): () => number {
let state = seed;
return () => {
state = (state * 16807) % 2147483647;
return (state - 1) / 2147483646;
};
}
export const random = createSeededRandom();
In this example:
- Click 'Add Sector' to add a new sector to the chart — the new category flashes on arrival.
- Click 'Remove Sector' to remove the last sector — no flash occurs on removal.
- Click 'Update' to update values for one or two sectors and observe the category flash.
Customisation Copy Link
The appearance and timing of the flash effect can be customised.
import {
AgCartesianChartOptions,
AgCharts,
ContextMenuModule,
CrosshairModule,
FlashOnUpdateModule,
LegendModule,
ModuleRegistry,
NumberAxisModule,
OhlcSeriesModule,
OrdinalTimeAxisModule,
} from "ag-charts-enterprise";
import { applyLiveUpdate, getInitialData } from "./data";
let data = getInitialData();
let isRunning = false;
let updateInterval: ReturnType<typeof setInterval> | undefined;
ModuleRegistry.registerModules([
ContextMenuModule,
CrosshairModule,
FlashOnUpdateModule,
LegendModule,
NumberAxisModule,
OhlcSeriesModule,
OrdinalTimeAxisModule,
]);
const options: AgCartesianChartOptions = {
data,
title: {
text: "MSFT Stock Price",
},
series: [
{
type: "ohlc",
xKey: "date",
xName: "Date",
openKey: "open",
highKey: "high",
lowKey: "low",
closeKey: "close",
},
],
axes: {
y: {
type: "number",
label: {
formatter: ({ value }) => `$${Number(value).toFixed(0)}`,
},
},
},
flashOnUpdate: {
enabled: true,
fill: "#ffd6a5",
flashDuration: 300,
fadeOutDuration: 700,
},
};
options.container = document.getElementById("myChart");
const chart = AgCharts.create(options);
function update() {
data = applyLiveUpdate(data);
options.data = data;
chart.update(options);
}
function startUpdates() {
if (isRunning) return;
isRunning = true;
updateButton();
update();
updateInterval = setInterval(update, 2000);
}
function stopUpdates() {
if (!isRunning) return;
isRunning = false;
updateButton();
if (updateInterval) {
clearInterval(updateInterval);
updateInterval = 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 setColor(value: string) {
options.flashOnUpdate!.fill = value;
chart.update(options);
}
function setFlashDuration(value: string) {
options.flashOnUpdate!.flashDuration = Number(value);
chart.update(options);
}
function setFadeDuration(value: string) {
options.flashOnUpdate!.fadeOutDuration = Number(value);
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).setColor = setColor;
(<any>window).setFlashDuration = setFlashDuration;
(<any>window).setFadeDuration = setFadeDuration;
}
import { createSeededRandom, random } from "./seededRandom";
export interface DataType {
date: Date;
open: number;
high: number;
low: number;
close: number;
}
function addBusinessDays(start: Date, days: number): Date {
const result = new Date(start);
let added = 0;
while (added < days) {
result.setDate(result.getDate() + 1);
const day = result.getDay();
if (day !== 0 && day !== 6) added++;
}
return result;
}
export function getInitialData(): DataType[] {
const seededRng = createSeededRandom(5678);
const data: DataType[] = [];
const startDate = new Date("2025-01-27");
let previousClose = 185.4;
for (let i = 0; i < 30; i++) {
const date = i === 0 ? new Date(startDate) : addBusinessDays(startDate, i);
const dailyChange = (seededRng() - 0.5) * 6;
const open = previousClose + (seededRng() - 0.5) * 1.5;
const close = open + dailyChange;
const wickUp = seededRng() * 2;
const wickDown = seededRng() * 2;
const high = Math.max(open, close) + wickUp;
const low = Math.min(open, close) - wickDown;
data.push({
date,
open: Math.round(open * 100) / 100,
high: Math.round(high * 100) / 100,
low: Math.round(low * 100) / 100,
close: Math.round(close * 100) / 100,
});
previousClose = close;
}
return data;
}
export function applyLiveUpdate(data: DataType[]): DataType[] {
const result = data.map((d) => ({ ...d }));
const last = result[result.length - 1];
const tick = (random() - 0.5) * 3;
last.close = Math.round((last.close + tick) * 100) / 100;
last.high = Math.round(Math.max(last.high, last.close) * 100) / 100;
last.low = Math.round(Math.min(last.low, last.close) * 100) / 100;
return result;
}
export function createSeededRandom(seed = 42): () => number {
let state = seed;
return () => {
state = (state * 16807) % 2147483647;
return (state - 1) / 2147483646;
};
}
export const random = createSeededRandom();
{
flashOnUpdate: {
enabled: true,
fill: '#ffd6a5',
flashDuration: 300,
fadeOutDuration: 700,
},
}In this example:
- Use the 'Fill', 'Flash' and 'Fade' controls to adjust the flash appearance.
fill— the fill colour of the flash overlay.flashDuration— how long (in milliseconds) the flash remains at full opacity before fading.fadeOutDuration— how long (in milliseconds) the fade-out takes.
- Click 'Start Updates' to begin a simulated live price feed and observe the flash with the current settings.
- Click 'Stop Updates' to pause the feed.
Animation Copy Link
Flash on Update disables Animation by default, as the two are rarely used together. If you do need both, explicitly set animation: { enabled: true }.
When both are enabled, the flashDuration and fadeOutDuration values are scaled to fit within the data update animation phases, so the actual timing may differ from the configured values. Their ratio still controls how the duration is split between the hold and fade stages.