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 React, { useState, Fragment } from "react";
import { createRoot } from "react-dom/client";
import { AgCharts } from "ag-charts-react";
import {
AgCartesianChartOptions,
CandlestickSeriesModule,
ContextMenuModule,
CrosshairModule,
FlashOnUpdateModule,
LegendModule,
ModuleRegistry,
NumberAxisModule,
OrdinalTimeAxisModule,
} from "ag-charts-enterprise";
import { applyLiveUpdate, getInitialData } from "./data";
import clone from "clone";
ModuleRegistry.registerModules([
CandlestickSeriesModule,
ContextMenuModule,
CrosshairModule,
FlashOnUpdateModule,
LegendModule,
NumberAxisModule,
OrdinalTimeAxisModule,
]);
const ChartExample = () => {
const [options, setOptions] = useState<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,
},
});
const update = () => {
const nextOptions = clone(options);
nextOptions.data = applyLiveUpdate(nextOptions.data!);
setOptions(nextOptions);
};
return (
<Fragment>
<div className="example-controls">
<div className="controls-row">
<button onClick={update}>Update</button>
</div>
</div>
<AgCharts options={options} />
</Fragment>
);
};
const root = createRoot(document.getElementById("root")!);
root.render(<ChartExample />);
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 React, { useState, Fragment } from "react";
import { createRoot } from "react-dom/client";
import { AgCharts } from "ag-charts-react";
import {
BarSeriesModule,
CategoryAxisModule,
LegendModule,
ModuleRegistry,
NumberAxisModule,
} from "ag-charts-community";
import {
AgCartesianChartOptions,
ContextMenuModule,
CrosshairModule,
FlashOnUpdateModule,
} from "ag-charts-enterprise";
import { DataType, applyRandomUpdate, getInitialData } from "./data";
import clone from "clone";
let data: DataType[] = getInitialData();
let isRunning = false;
let updateInterval: ReturnType<typeof setInterval> | undefined;
ModuleRegistry.registerModules([
BarSeriesModule,
CategoryAxisModule,
ContextMenuModule,
CrosshairModule,
FlashOnUpdateModule,
LegendModule,
NumberAxisModule,
]);
const ChartExample = () => {
const [options, setOptions] = useState<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",
},
});
const update = () => {
const nextOptions = clone(options);
data = applyRandomUpdate(data);
nextOptions.data = data;
setOptions(nextOptions);
};
const startUpdates = () => {
if (isRunning) return;
isRunning = true;
updateButton();
update();
updateInterval = setInterval(update, 2000);
};
const stopUpdates = () => {
if (!isRunning) return;
isRunning = false;
updateButton();
if (updateInterval) {
clearInterval(updateInterval);
updateInterval = undefined;
}
};
const updateButton = () => {
const button = document.getElementById("toggleBtn");
if (button) {
button.textContent = isRunning ? "Stop Updates" : "Start Updates";
}
};
const toggleUpdates = () => {
if (isRunning) {
stopUpdates();
} else {
startUpdates();
}
};
return (
<Fragment>
<div className="example-controls">
<div className="controls-row">
<button id="toggleBtn" onClick={toggleUpdates}>
Start Updates
</button>
</div>
</div>
<AgCharts options={options} />
</Fragment>
);
};
const root = createRoot(document.getElementById("root")!);
root.render(<ChartExample />);
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 React, { useState, Fragment } from "react";
import { createRoot } from "react-dom/client";
import { AgCharts } from "ag-charts-react";
import {
BarSeriesModule,
CategoryAxisModule,
LegendModule,
ModuleRegistry,
NumberAxisModule,
} from "ag-charts-community";
import {
AgCartesianChartOptions,
ContextMenuModule,
CrosshairModule,
FlashOnUpdateModule,
} from "ag-charts-enterprise";
import { DataType, applyUpdate, getInitialData, getNextSector } from "./data";
import clone from "clone";
ModuleRegistry.registerModules([
BarSeriesModule,
CategoryAxisModule,
ContextMenuModule,
CrosshairModule,
FlashOnUpdateModule,
LegendModule,
NumberAxisModule,
]);
const ChartExample = () => {
const [options, setOptions] = useState<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",
},
});
const addSector = () => {
const nextOptions = clone(options);
const next = getNextSector(nextOptions.data!);
if (!next) return;
nextOptions.data = [...nextOptions.data!, { ...next }];
setOptions(nextOptions);
};
const removeSector = () => {
const nextOptions = clone(options);
if (nextOptions.data!.length <= 2) return;
nextOptions.data = nextOptions.data!.slice(0, -1);
setOptions(nextOptions);
};
const updateSectors = () => {
const nextOptions = clone(options);
nextOptions.data = applyUpdate(nextOptions.data!);
setOptions(nextOptions);
};
return (
<Fragment>
<div className="example-controls">
<div className="controls-row">
<button onClick={addSector}>Add Sector</button>
<button onClick={removeSector}>Remove Sector</button>
<button onClick={updateSectors}>Update</button>
</div>
</div>
<AgCharts options={options} />
</Fragment>
);
};
const root = createRoot(document.getElementById("root")!);
root.render(<ChartExample />);
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 React, { useState, Fragment } from "react";
import { createRoot } from "react-dom/client";
import { AgCharts } from "ag-charts-react";
import {
AgCartesianChartOptions,
ContextMenuModule,
CrosshairModule,
FlashOnUpdateModule,
LegendModule,
ModuleRegistry,
NumberAxisModule,
OhlcSeriesModule,
OrdinalTimeAxisModule,
} from "ag-charts-enterprise";
import { applyLiveUpdate, getInitialData } from "./data";
import clone from "clone";
let data = getInitialData();
let isRunning = false;
let updateInterval: ReturnType<typeof setInterval> | undefined;
ModuleRegistry.registerModules([
ContextMenuModule,
CrosshairModule,
FlashOnUpdateModule,
LegendModule,
NumberAxisModule,
OhlcSeriesModule,
OrdinalTimeAxisModule,
]);
const ChartExample = () => {
const [options, setOptions] = useState<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,
},
});
const update = () => {
const nextOptions = clone(options);
data = applyLiveUpdate(data);
nextOptions.data = data;
setOptions(nextOptions);
};
const startUpdates = () => {
if (isRunning) return;
isRunning = true;
updateButton();
update();
updateInterval = setInterval(update, 2000);
};
const stopUpdates = () => {
if (!isRunning) return;
isRunning = false;
updateButton();
if (updateInterval) {
clearInterval(updateInterval);
updateInterval = 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 setColor = (value: string) => {
const nextOptions = clone(options);
nextOptions.flashOnUpdate!.fill = value;
setOptions(nextOptions);
};
const setFlashDuration = (value: string) => {
const nextOptions = clone(options);
nextOptions.flashOnUpdate!.flashDuration = Number(value);
setOptions(nextOptions);
};
const setFadeDuration = (value: string) => {
const nextOptions = clone(options);
nextOptions.flashOnUpdate!.fadeOutDuration = Number(value);
setOptions(nextOptions);
};
return (
<Fragment>
<div className="example-controls">
<div className="controls-row">
<button id="toggleBtn" onClick={toggleUpdates}>
Start Updates
</button>
<label>Fill:</label>
<select onChange={(event) => setColor(event.target.value)}>
<option value="#cfeeff">Default (#cfeeff)</option>
<option value="#ffd6a5">Warm (#ffd6a5)</option>
<option value="#ffadad">Red (#ffadad)</option>
</select>
<label>Flash:</label>
<select onChange={(event) => setFlashDuration(event.target.value)}>
<option value="0">0ms</option>
<option value="100">100ms</option>
<option value="300">300ms</option>
<option value="500">500ms</option>
</select>
<label>Fade:</label>
<select onChange={(event) => setFadeDuration(event.target.value)}>
<option value="200">200ms</option>
<option value="500">500ms</option>
<option value="700">700ms</option>
<option value="1500">1500ms</option>
</select>
</div>
</div>
<AgCharts options={options} />
</Fragment>
);
};
const root = createRoot(document.getElementById("root")!);
root.render(<ChartExample />);
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.