This section introduces Integrated Charts that are created programmatically within an application.
import type { OnDestroy } from "@angular/core";
import { Component } from "@angular/core";
import { AgChartsEnterpriseModule } from "ag-charts-enterprise";
import { AgGridAngular } from "ag-grid-angular";
import type {
ChartRef,
ChartType,
ColDef,
FirstDataRenderedEvent,
GetRowIdParams,
GridApi,
GridReadyEvent,
ValueFormatterParams,
} from "ag-grid-community";
import {
CellStyleModule,
ClientSideRowModelApiModule,
ClientSideRowModelModule,
ColumnApiModule,
HighlightChangesModule,
ModuleRegistry,
NumberEditorModule,
NumberFilterModule,
TextEditorModule,
TextFilterModule,
enableDevValidations,
} from "ag-grid-community";
import { IntegratedChartsModule, RowGroupingModule } from "ag-grid-enterprise";
import "./styles.css";
// Enable extended validations only for development
if (process.env.NODE_ENV !== "production") {
enableDevValidations();
}
ModuleRegistry.registerModules([
ColumnApiModule,
ClientSideRowModelApiModule,
TextEditorModule,
TextFilterModule,
NumberEditorModule,
CellStyleModule,
ClientSideRowModelModule,
IntegratedChartsModule.with(AgChartsEnterpriseModule),
RowGroupingModule,
HighlightChangesModule,
NumberFilterModule,
]);
declare let __basePath: string;
function numberCellFormatter(params: ValueFormatterParams) {
return Math.floor(params.value)
.toString()
.replace(/(\d)(?=(\d{3})+(?!\d))/g, "$1,");
}
@Component({
selector: "my-app",
standalone: true,
imports: [AgGridAngular],
template: `<div id="myApp" class="wrapper">
<div style="padding-bottom: 4px">
<span>
<button (click)="onStopMessages()">■ Stop</button>
<button (click)="onStartLoad()">► Start</button>
</span>
<span style="margin-left: 30px">
<button (click)="updateChart('stackedColumn')">
Stacked Column Chart
</button>
<button (click)="updateChart('groupedColumn')">
Grouped Column Chart
</button>
<button (click)="updateChart('line')">Line Chart</button>
</span>
</div>
<ag-grid-angular
class="my-grid"
[columnDefs]="columnDefs"
[defaultColDef]="defaultColDef"
[columnTypes]="columnTypes"
[enableCharts]="true"
[suppressAggFuncInHeader]="true"
[getRowId]="getRowId"
[getChartToolbarItems]="getChartToolbarItems"
(gridReady)="onGridReady($event)"
(firstDataRendered)="onFirstDataRendered($event)"
/>
<div id="myChart" class="my-chart"></div>
</div>`,
})
export class AppComponent implements OnDestroy {
private gridApi?: GridApi;
private chartRef?: ChartRef;
private worker?: Worker;
columnDefs: ColDef[] = [
{ field: "product", chartDataType: "category", minWidth: 110 },
{ field: "book", chartDataType: "category", minWidth: 100 },
{ field: "current", type: "measure" },
{ field: "previous", type: "measure" },
{ headerName: "PL 1", field: "pl1", type: "measure" },
{ headerName: "PL 2", field: "pl2", type: "measure" },
{ headerName: "Gain-DX", field: "gainDx", type: "measure" },
{ headerName: "SX / PX", field: "sxPx", type: "measure" },
{ field: "trade", type: "measure" },
{ field: "submitterID", type: "measure" },
{ field: "submitterDealID", type: "measure" },
{ field: "portfolio" },
{ field: "dealType" },
{ headerName: "Bid", field: "bidFlag" },
];
defaultColDef: ColDef = {
editable: true,
flex: 1,
minWidth: 140,
filter: true,
};
columnTypes: Record<string, ColDef> = {
measure: {
chartDataType: "series",
cellClass: "number",
valueFormatter: numberCellFormatter,
cellRenderer: "agAnimateShowChangeCellRenderer",
},
};
getRowId = (params: GetRowIdParams) => String(params.data.trade);
getChartToolbarItems = () => [];
onGridReady(params: GridReadyEvent) {
this.gridApi = params.api;
// Create the worker once the grid API exists so the first 'setRowData' message is never dropped.
if (!this.worker) {
this.worker = new Worker(`${__basePath || "."}/dataUpdateWorker.js`);
this.worker.addEventListener("message", this.handleWorkerMessage);
this.worker.postMessage("start");
}
}
onFirstDataRendered(params: FirstDataRenderedEvent) {
this.chartRef = params.api.createRangeChart({
chartContainer: document.querySelector("#myChart") as HTMLElement,
cellRange: {
columns: [
"product",
"current",
"previous",
"pl1",
"pl2",
"gainDx",
"sxPx",
],
},
suppressChartRanges: true,
chartType: "groupedColumn",
aggFunc: "sum",
chartThemeOverrides: {
common: {
animation: {
enabled: false,
},
},
},
});
}
updateChart(chartType: ChartType) {
if (this.gridApi && this.chartRef) {
this.gridApi.updateChart({
type: "rangeChartUpdate",
chartId: this.chartRef.chartId,
chartType,
});
}
}
onStartLoad() {
this.worker?.postMessage("start");
}
onStopMessages() {
this.worker?.postMessage("stop");
}
// Arrow function so `this` stays bound when used as an event listener.
private handleWorkerMessage = (e: MessageEvent) => {
if (!this.gridApi) {
return;
}
if (e.data.type === "setRowData") {
this.gridApi.setGridOption("rowData", e.data.records);
} else if (e.data.type === "updateData") {
this.gridApi.applyTransactionAsync({ update: e.data.records });
}
};
// Tear down the worker on destroy so no update interval survives navigation.
ngOnDestroy() {
if (this.worker) {
this.worker.removeEventListener("message", this.handleWorkerMessage);
this.worker.terminate();
this.worker = undefined;
}
this.gridApi = undefined;
}
}
.number {
text-align: right;
}
.wrapper {
display: flex;
flex-direction: column;
height: 100%;
}
.my-grid {
flex-grow: 1;
}
.my-chart {
flex-grow: 1;
height: 135px;
display: inline-block;
margin-top: 0.5rem;
}
// NOTE: The details of this web worker are not important it's just used to simulate streaming updates in the grid.
// Constants
const UPDATES_PER_MESSAGE = 100;
const MILLISECONDS_BETWEEN_MESSAGES = 100;
const BOOK_COUNT = 5;
const TRADE_COUNT = 2;
const VALUE_FIELDS = [
"current",
"previous",
"pl1",
"pl2",
"gainDx",
"sxPx",
"_99Out",
];
const PRODUCTS = [
"Cobalt",
"Rubber",
"Wool",
"Amber",
"Corn",
"Nickel",
"Copper",
"Oats",
"Coffee",
"Wheat",
"Lead",
"Zinc",
"Tin",
"Coca",
];
const PORTFOLIOS = [
"Aggressive",
"Defensive",
"Income",
"Speculative",
"Hybrid",
];
// Global Variables
let globalRowData;
let nextBookId = 62472;
let nextTradeId = 24287;
let nextBatchId = 101;
let latestUpdateId = 0;
let intervalId;
/**
* Generates a random number between min and max
*/
function randomBetween(min, max) {
return Math.floor(Math.random() * (max - min + 1)) + min;
}
// build up the test data
function createRowData() {
globalRowData = [];
var thisBatch = nextBatchId++;
for (var k = 0; k < BOOK_COUNT; k++) {
for (var j = 0; j < PORTFOLIOS.length; j++) {
var portfolio = PORTFOLIOS[j];
for (var i = 0; i < PRODUCTS.length; i++) {
var product = PRODUCTS[i];
var book = "GL-" + ++nextBookId;
for (var l = 0; l < TRADE_COUNT; l++) {
var trade = createTradeRecord(product, portfolio, book, thisBatch);
globalRowData.push(trade);
}
}
}
}
// console.log('Total number of records sent to grid = ' + globalRowData.length);
}
function createTradeRecord(product, portfolio, book, batch) {
var current =
Math.floor(Math.random() * 10000) + (Math.random() < 0.45 ? 500 : 19000);
var previous = current + (Math.random() < 0.5 ? 500 : 19000);
return {
product: product,
portfolio: portfolio,
book: book,
trade: ++nextTradeId,
submitterID: randomBetween(10, 1000),
submitterDealID: randomBetween(10, 1000),
dealType: Math.random() < 0.2 ? "Physical" : "Financial",
bidFlag: Math.random() < 0.5 ? "Buy" : "Sell",
current: current,
previous: previous,
pl1: randomBetween(10000, 30000),
pl2: randomBetween(8000, 35000),
gainDx: randomBetween(35000, 1000),
sxPx: randomBetween(10000, 30000),
batch: batch,
};
}
function updateSomeItems(updateCount) {
var itemsToUpdate = [];
for (var k = 0; k < updateCount; k++) {
if (globalRowData.length === 0) {
continue;
}
var indexToUpdate = Math.floor(Math.random() * globalRowData.length);
var itemToUpdate = globalRowData[indexToUpdate];
var field = VALUE_FIELDS[Math.floor(Math.random() * VALUE_FIELDS.length)];
itemToUpdate[field] += randomBetween(-8000, 8200);
itemsToUpdate.push(itemToUpdate);
}
return itemsToUpdate;
}
function startUpdates(thisUpdateId) {
// Cancel any interval a previous 'start' left running so only one is ever active.
clearInterval(intervalId);
postMessage({
type: "start",
updateCount: UPDATES_PER_MESSAGE,
interval: MILLISECONDS_BETWEEN_MESSAGES,
});
function intervalFunc() {
// Check for cancellation before posting so Stop takes effect without an extra batch.
if (thisUpdateId !== latestUpdateId) {
clearInterval(intervalId);
return;
}
postMessage({
type: "updateData",
records: updateSomeItems(UPDATES_PER_MESSAGE),
});
}
intervalId = setInterval(intervalFunc, MILLISECONDS_BETWEEN_MESSAGES);
}
function stopUpdates() {
clearInterval(intervalId);
}
// Initialize Row Data
createRowData();
// Notify that row data is ready
postMessage({
type: "setRowData",
records: globalRowData,
});
// Event Listener for incoming messages
self.addEventListener("message", function (e) {
latestUpdateId++;
if (e.data === "start") {
startUpdates(latestUpdateId);
} else if (e.data === "stop") {
stopUpdates();
}
});
import '@angular/compiler';
import { provideHttpClient } from '@angular/common/http';
import { enableProdMode } from '@angular/core';
import { bootstrapApplication } from '@angular/platform-browser';
import { AppComponent } from './app.component';
if (new URLSearchParams(window.location.search).get('prod') !== 'false') {
enableProdMode();
}
const app = bootstrapApplication(AppComponent, {
providers: [provideHttpClient()],
});
The dummy financial application above shows some of the grid's integrated charting capabilities. Note the following:
- Pre-Defined Chart: A pre-defined chart is shown in a separate chart container below the grid.
- Dynamic Charts: Buttons positioned above the grid dynamically create different chart types.
- High Performance: 100 rows are randomly updated 10 times a second (1,000 updates per second). Try updating the example via Plunker with higher update frequencies and more data.
To learn how to create charts in your applications see the following sections for details:
- Range Chart API - create Range Charts using the Grid API
- Pivot Chart API - create Pivot Charts using the Grid API
- Cross Filter Chart API - create cross-filter charts where element clicks auto-filter grid and charts