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 { Component } from "@angular/core";
import { AgCharts } from "ag-charts-angular";
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,
]);
@Component({
selector: "my-app",
standalone: true,
imports: [AgCharts],
template: `<div class="example-controls">
<div class="controls-row">
<button (click)="update()">Update</button>
</div>
</div>
<ag-charts
[options]="options"
></ag-charts>
`,
})
export class AppComponent {
public options;
constructor() {
this.options = {
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,
},
};
}
update = () => {
const options = clone(this.options);
options.data = applyLiveUpdate(options.data!);
this.options = options;
};
}
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();
// Angular entry point file
import '@angular/compiler';
import { bootstrapApplication } from '@angular/platform-browser';
import { AppComponent } from './app.component';
bootstrapApplication(AppComponent);
{
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 { Component } from "@angular/core";
import { AgCharts } from "ag-charts-angular";
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,
]);
@Component({
selector: "my-app",
standalone: true,
imports: [AgCharts],
template: `<div class="example-controls">
<div class="controls-row">
<button id="toggleBtn" (click)="toggleUpdates()">Start Updates</button>
</div>
</div>
<ag-charts
[options]="options"
></ag-charts>
`,
})
export class AppComponent {
public options;
constructor() {
this.options = {
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",
},
};
}
update = () => {
const options = clone(this.options);
data = applyRandomUpdate(data);
options.data = data;
this.options = options;
};
startUpdates = () => {
if (isRunning) return;
isRunning = true;
this.updateButton();
this.update();
updateInterval = setInterval(this.update, 2000);
};
stopUpdates = () => {
if (!isRunning) return;
isRunning = false;
this.updateButton();
if (updateInterval) {
clearInterval(updateInterval);
updateInterval = undefined;
}
};
updateButton = () => {
const button = document.getElementById("toggleBtn");
if (button) {
button.textContent = isRunning ? "Stop Updates" : "Start Updates";
}
};
toggleUpdates = () => {
if (isRunning) {
this.stopUpdates();
} else {
this.startUpdates();
}
};
}
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),
};
});
}
// Angular entry point file
import '@angular/compiler';
import { bootstrapApplication } from '@angular/platform-browser';
import { AppComponent } from './app.component';
bootstrapApplication(AppComponent);
{
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 { Component } from "@angular/core";
import { AgCharts } from "ag-charts-angular";
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,
]);
@Component({
selector: "my-app",
standalone: true,
imports: [AgCharts],
template: `<div class="example-controls">
<div class="controls-row">
<button (click)="addSector()">Add Sector</button>
<button (click)="removeSector()">Remove Sector</button>
<button (click)="updateSectors()">Update</button>
</div>
</div>
<ag-charts
[options]="options"
></ag-charts>
`,
})
export class AppComponent {
public options;
constructor() {
this.options = {
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",
},
};
}
addSector = () => {
const options = clone(this.options);
const next = getNextSector(options.data!);
if (!next) return;
options.data = [...options.data!, { ...next }];
this.options = options;
};
removeSector = () => {
const options = clone(this.options);
if (options.data!.length <= 2) return;
options.data = options.data!.slice(0, -1);
this.options = options;
};
updateSectors = () => {
const options = clone(this.options);
options.data = applyUpdate(options.data!);
this.options = options;
};
}
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();
// Angular entry point file
import '@angular/compiler';
import { bootstrapApplication } from '@angular/platform-browser';
import { AppComponent } from './app.component';
bootstrapApplication(AppComponent);
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 { Component } from "@angular/core";
import { AgCharts } from "ag-charts-angular";
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,
]);
@Component({
selector: "my-app",
standalone: true,
imports: [AgCharts],
template: `<div class="example-controls">
<div class="controls-row">
<button id="toggleBtn" (click)="toggleUpdates()">Start Updates</button>
<label>Fill:</label>
<select (change)="setColor($event.target.value)">
<option value="#cfeeff">Default (#cfeeff)</option>
<option value="#ffd6a5" selected="">Warm (#ffd6a5)</option>
<option value="#ffadad">Red (#ffadad)</option>
</select>
<label>Flash:</label>
<select (change)="setFlashDuration($event.target.value)">
<option value="0">0ms</option>
<option value="100">100ms</option>
<option value="300" selected="">300ms</option>
<option value="500">500ms</option>
</select>
<label>Fade:</label>
<select (change)="setFadeDuration($event.target.value)">
<option value="200">200ms</option>
<option value="500">500ms</option>
<option value="700" selected="">700ms</option>
<option value="1500">1500ms</option>
</select>
</div>
</div>
<ag-charts
[options]="options"
></ag-charts>
`,
})
export class AppComponent {
public options;
constructor() {
this.options = {
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,
},
};
}
update = () => {
const options = clone(this.options);
data = applyLiveUpdate(data);
options.data = data;
this.options = options;
};
startUpdates = () => {
if (isRunning) return;
isRunning = true;
this.updateButton();
this.update();
updateInterval = setInterval(this.update, 2000);
};
stopUpdates = () => {
if (!isRunning) return;
isRunning = false;
this.updateButton();
if (updateInterval) {
clearInterval(updateInterval);
updateInterval = undefined;
}
};
updateButton = () => {
const button = document.getElementById("toggleBtn");
if (button) {
button.textContent = isRunning ? "Stop Updates" : "Start Updates";
}
};
toggleUpdates = () => {
if (isRunning) {
this.stopUpdates();
} else {
this.startUpdates();
}
};
setColor = (value: string) => {
const options = clone(this.options);
options.flashOnUpdate!.fill = value;
this.options = options;
};
setFlashDuration = (value: string) => {
const options = clone(this.options);
options.flashOnUpdate!.flashDuration = Number(value);
this.options = options;
};
setFadeDuration = (value: string) => {
const options = clone(this.options);
options.flashOnUpdate!.fadeOutDuration = Number(value);
this.options = options;
};
}
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();
// Angular entry point file
import '@angular/compiler';
import { bootstrapApplication } from '@angular/platform-browser';
import { AppComponent } from './app.component';
bootstrapApplication(AppComponent);
{
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.