In this section we add Server-Side Pivoting to create an example with the ability to 'Slice and Dice' data using the Server-Side Row Model (SSRM).
Enabling Pivoting Copy Link
To pivot on a column pivot=true should be set on the column definition. Additionally, the grid needs to be in pivot mode which is set through the grid option pivotMode=true.
In the snippet below a pivot is defined on the 'year' column and pivot mode is enabled:
// pivot mode enabled
const pivotMode = true;
const [columnDefs, setColumnDefs] = useState([
{ field: 'country', rowGroup: true },
// pivot enabled
{ field: 'year', pivot: true },
{ field: 'total' },
]);
<AgGridReact
pivotMode={pivotMode}
columnDefs={columnDefs}
/>For more configuration details see the section on Pivoting.
Pivoting on the Server Copy Link
The actual pivoting is performed on the server when using the Server-Side Row Model. When the grid needs more rows it makes a request via getRows(params) on the Server-Side Datasource with metadata containing row grouping details.
The properties relevant to pivoting in the request are shown below:
Columns that have pivot on them. |
Defines if pivot mode is on or off. |
Note in the snippet above that pivotCols contains all the columns the grid is pivoting on, and pivotMode is used to determine if pivoting is currently enabled in the grid.
Providing Pivot Result Columns Copy Link
Pivot Result Columns are the columns that are created as part of the pivot function. You must provide these to the grid in order for the grid to display the correct columns for the active pivot function.
For instance, when pivoting on the year field, you must provide columns to the grid corresponding to each distinct year present in the data, such as 2000, 2002, 2004, and so on.
Supplying Pivot Result Fields (Simple) Copy Link
The simplest way to provide pivot result columns is by supplying the fields containing your pivoted data to the pivotResultFields attribute in the getRows success callback. These fields are used to generate pivot result columns and appropriate column groups. By default, the grid expects the fields to be separated by an underscore ('_'), however, this can be altered via the serverSidePivotResultFieldSeparator grid option as shown below:
const [columnDefs, setColumnDefs] = useState([
{ field: 'country', rowGroup: true },
{ field: 'year', pivot: true }, // pivot on 'year'
{ field: 'gold', aggFunc: 'sum' },
{ field: 'silver', aggFunc: 'sum' },
{ field: 'bronze', aggFunc: 'sum' },
]);
const rowModelType = 'serverSide';
const pivotMode = true;
// specify the field separator, e.g. '2000_gold' should be '_' which is also the default
const serverSidePivotResultFieldSeparator = '_';
<AgGridReact
columnDefs={columnDefs}
rowModelType={rowModelType}
pivotMode={pivotMode}
serverSidePivotResultFieldSeparator={serverSidePivotResultFieldSeparator}
/>Note above that serverSidePivotResultFieldSeparator is not necessary as the default value is '_'.
The following snippet shows how to supply the pivotResultFields to the grid via the success callback:
const createDatasource = server => {
return {
// called by the grid when more rows are required
getRows: params => {
// get data for request from server
const response = server.getData(params.request);
if (response.success) {
// supply rows for requested block to grid
params.success({
rowData: response.rows,
pivotResultFields: response.pivotFields, // ['2000_gold', '2000_silver',...]
});
} else {
// inform grid request failed
params.fail();
}
}
};
}The example below demonstrates this, note the following:
- The pivot fields are returned from the server and then passed to the grid via the
getRowssuccess callback via thepivotResultFieldsproperty. These are logged to the console as a demonstration. - The grid splits the
pivotResultFieldsby_and creates the pivot result columns and column groups where the generated columns use the provided fields to access the data from the rows.
("use client");
import React, {
useCallback,
useMemo,
useRef,
useState,
StrictMode,
} from "react";
import { createRoot } from "react-dom/client";
import { AgGridReact, AgGridProvider } from "ag-grid-react";
import {
AutoGroupColumnDef,
ColDef,
ColGroupDef,
GridApi,
GridOptions,
GridReadyEvent,
IServerSideDatasource,
ModuleRegistry,
RowModelType,
SideBarDef,
enableDevValidations,
} from "ag-grid-community";
import {
ColumnMenuModule,
ColumnsToolPanelModule,
ContextMenuModule,
RowGroupingModule,
RowGroupingPanelModule,
ServerSideRowModelModule,
} from "ag-grid-enterprise";
import { FakeServer } from "./fakeServer";
import { IOlympicData } from "./interfaces";
if (process.env.NODE_ENV !== "production") {
enableDevValidations();
}
const modules = [
ColumnsToolPanelModule,
ColumnMenuModule,
ContextMenuModule,
RowGroupingModule,
RowGroupingPanelModule,
ServerSideRowModelModule,
];
const getServerSideDatasource: (server: any) => IServerSideDatasource = (
server: any,
) => {
return {
getRows: (params) => {
console.log("[Datasource] - rows requested by grid: ", params.request);
// get data for request from our fake server
const response = server.getData(params.request);
// simulating real server call with a 500ms delay
setTimeout(() => {
if (response.success) {
// supply data to grid
console.log(
"[Datasource] - pivotResultFields to be set in grid: ",
response.pivotFields,
);
params.success({
rowData: response.rows,
rowCount: response.lastRow,
pivotResultFields: response.pivotFields,
});
} else {
params.fail();
}
}, 500);
},
};
};
const GridExample = () => {
const containerStyle = useMemo(() => ({ width: "100%", height: "100%" }), []);
const gridStyle = useMemo(() => ({ height: "100%", width: "100%" }), []);
const [columnDefs, setColumnDefs] = useState<ColDef[]>([
{ field: "country", rowGroup: true, enableRowGroup: true },
{ field: "sport", enableRowGroup: true },
{ field: "year", pivot: true, enablePivot: true }, // pivot on 'year'
{ field: "gold", aggFunc: "sum", enableValue: true },
{ field: "silver", aggFunc: "sum", enableValue: true },
{ field: "bronze", aggFunc: "sum", enableValue: true },
]);
const defaultColDef = useMemo<ColDef>(() => {
return {
flex: 1,
minWidth: 100,
};
}, []);
const autoGroupColumnDef = useMemo<AutoGroupColumnDef>(() => {
return {
minWidth: 200,
};
}, []);
const sideBar = useMemo<
SideBarDef | string | string[] | boolean | null
>(() => {
return {
toolPanels: ["columns"],
};
}, []);
const onGridReady = useCallback((params: GridReadyEvent) => {
fetch("https://www.ag-grid.com/example-assets/olympic-winners.json")
.then((resp) => resp.json())
.then((data: IOlympicData[]) => {
// setup the fake server with entire dataset
const fakeServer = new FakeServer(data);
// create datasource with a reference to the fake server
const datasource = getServerSideDatasource(fakeServer);
// register the datasource with the grid
params.api!.setGridOption("serverSideDatasource", datasource);
});
}, []);
return (
<AgGridProvider modules={modules}>
<div style={containerStyle}>
<div style={gridStyle}>
<AgGridReact<IOlympicData>
columnDefs={columnDefs}
defaultColDef={defaultColDef}
autoGroupColumnDef={autoGroupColumnDef}
rowModelType={"serverSide"}
pivotMode={true}
sideBar={sideBar}
rowGroupPanelShow={"always"}
pivotPanelShow={"always"}
serverSidePivotResultFieldSeparator={"_"}
suppressAggFuncInHeader={true}
onGridReady={onGridReady}
/>
</div>
</div>
</AgGridProvider>
);
};
const root = createRoot(document.getElementById("root")!);
root.render(
<StrictMode>
<GridExample />
</StrictMode>,
);
// This fake server uses http://alasql.org/ to mimic how a real server
// might generate sql queries from the Server-Side Row Model request.
// To keep things simple it does the bare minimum to support the example.
export function FakeServer(allData) {
alasql.options.cache = false;
return {
getData: (request) => {
const result = executeQuery(request);
return {
success: true,
rows: result,
lastRow: getLastRowIndex(request, result),
pivotFields: getPivotFields(request),
};
},
};
function executeQuery(request) {
const { pivotCols, valueCols } = request;
const [pivotCol] = pivotCols;
if (valueCols.length === 0) {
return [];
}
const results = [];
valueCols.forEach((valueCol) => {
const pivotResults = executePivotQuery(request, pivotCol, valueCol);
pivotResults.forEach((pivotResult, i) => {
results[i] = { ...results[i], ...pivotResult };
});
});
return alasql(`SELECT * FROM ?${orderBySql(request)}`, [results]);
}
function orderBySql({ sortModel }) {
if (sortModel.length === 0) return '';
const sorts = sortModel.map(({ colId, sort }) => `\`${colId}\` ${sort.toUpperCase()}`);
return ` ORDER BY ${sorts.join(', ')}`;
}
function executePivotQuery(request, pivotCol, valueCol) {
const { groupKeys, rowGroupCols } = request;
const groupsToUse = rowGroupCols.slice(groupKeys.length, groupKeys.length + 1);
const selectGroupCols = groupsToUse.map((groupCol) => groupCol.id).join(', ');
const SQL = `SELECT ${selectGroupCols}, (${pivotCol.id} + '_${
valueCol.id
}') AS ${pivotCol.id}, ${valueCol.id} FROM ? PIVOT (${valueCol.aggFunc}([${
valueCol.id
}]) FOR ${pivotCol.id})${whereSql(request)}`;
console.log('[FakeServer] - about to execute query:', SQL);
return extractRowsForBlock(request, alasql(SQL, [allData]));
}
function whereSql({ rowGroupCols, groupKeys }) {
const whereParts = groupKeys
? groupKeys.map((key, i) => `${rowGroupCols[i].id} = ${typeof key === 'string' ? `'${key}'` : key}`)
: [];
return whereParts.length > 0 ? ` WHERE ${whereParts.join(' AND ')}` : '';
}
function extractRowsForBlock({ startRow, endRow }, results) {
const blockSize = endRow - startRow + 1;
return results.slice(startRow, startRow + blockSize);
}
function getPivotFields({ pivotCols, valueCols }) {
const [pivotCol] = pivotCols;
const result = flatten(
valueCols.map((valueCol) => {
const sql = `SELECT DISTINCT (${pivotCol.id} + '_${valueCol.id}') AS ${pivotCol.id} FROM ? ORDER BY ${pivotCol.id}`;
return alasql(sql, [allData]);
})
);
return flatten(result.map((x) => x[pivotCol.id]));
}
function getLastRowIndex({ startRow, endRow }, results) {
if (!results || results.length === 0) {
return null;
}
const currentLastRow = startRow + results.length;
return currentLastRow <= endRow ? currentLastRow : -1;
}
}
const flatten = (arrayOfArrays) => [].concat(...arrayOfArrays);
export interface IOlympicData {
athlete: string,
age: number,
country: string,
year: number,
date: string,
sport: string,
gold: number,
silver: number,
bronze: number,
total: number
} When using managed columns, you can use Pivot Callbacks to customise the pivot result column definitions.
Creating Pivot Result Columns (Advanced) Copy Link
It is also possible to create your own pivot result columns and provide them to the grid. This offers complete flexibility but can become complex when column groups are involved.
Pivot result columns are defined identically to the columns supplied to the grid options: you provide a list of Column Definitions passing a list of columns and / or column groups using the following grid API method:
Set explicit pivot column definitions yourself. Used for advanced use cases only. |
There is no limit or restriction as to the number of columns or groups you pass. However, it's important that the field (or value getter) that you set for the columns match.
Here is how pivot result columns can be created and supplied to the grid via setPivotResultColumns:
const createDatasource = server => {
return {
// called by the grid when more rows are required
getRows: params => {
// get data for request from server
const response = server.getData(params.request);
// add pivot result cols to the grid
addPivotResultCols(response, params.api)
if (response.success) {
// supply rows for requested block to grid
params.success({
rowData: response.rows,
});
} else {
// inform grid request failed
params.fail();
}
}
};
}
function addPivotResultCols(response, api) {
// create colDefs
var pivotColDefs = response.pivotFields.map(function (field) {
var headerName = field.split('_')[0]
return { headerName: headerName, field: field }
})
// supply pivot result columns to the grid
api.setPivotResultColumns(pivotColDefs)
}In the code above, addPivotResultCols does not create column groups for simplicity. However, the example below shows a more complex implementation that creates column groups. Note the following:
- Column definitions are created from the
pivotFieldsare returned from the server. - These column definitions are then supplied to the grid via
api.setPivotResultColumns().
("use client");
import React, {
useCallback,
useMemo,
useRef,
useState,
StrictMode,
} from "react";
import { createRoot } from "react-dom/client";
import { AgGridReact, AgGridProvider } from "ag-grid-react";
import {
AutoGroupColumnDef,
ColDef,
ColGroupDef,
GridApi,
GridOptions,
GridReadyEvent,
IServerSideDatasource,
IServerSideGetRowsRequest,
ModuleRegistry,
RowModelType,
SideBarDef,
enableDevValidations,
} from "ag-grid-community";
import {
ColumnMenuModule,
ColumnsToolPanelModule,
ContextMenuModule,
RowGroupingModule,
RowGroupingPanelModule,
ServerSideRowModelModule,
} from "ag-grid-enterprise";
import { FakeServer } from "./fakeServer";
import { IOlympicData } from "./interfaces";
if (process.env.NODE_ENV !== "production") {
enableDevValidations();
}
const modules = [
ColumnsToolPanelModule,
ColumnMenuModule,
ContextMenuModule,
RowGroupingModule,
RowGroupingPanelModule,
ServerSideRowModelModule,
];
const getServerSideDatasource: (server: any) => IServerSideDatasource = (
server: any,
) => {
return {
getRows: (params) => {
const request = params.request;
console.log("[Datasource] - rows requested by grid: ", params.request);
const response = server.getData(request);
// add pivot results cols to the grid
addPivotResultCols(request, response, params.api);
// simulating real server call with a 500ms delay
setTimeout(() => {
if (response.success) {
// supply data to grid
params.success({
rowData: response.rows,
rowCount: response.lastRow,
});
} else {
params.fail();
}
}, 500);
},
};
};
function addPivotResultCols(
request: IServerSideGetRowsRequest,
response: any,
api: GridApi,
) {
// check if pivot colDefs already exist
const existingPivotColDefs = api.getPivotResultColumns();
if (existingPivotColDefs && existingPivotColDefs.length > 0) {
return;
}
// create pivot colDef's based of data returned from the server
const pivotResultColumns = createPivotResultColumns(
request,
response.pivotFields,
);
// supply pivot result columns to the grid
api.setPivotResultColumns(pivotResultColumns);
}
function addColDef(
colId: string,
parts: string[],
res: (ColDef | ColGroupDef)[],
request: IServerSideGetRowsRequest,
): (ColDef | ColGroupDef)[] {
if (parts.length === 0) return [];
const first = parts[0];
const existing: ColGroupDef = res.find(
(r: ColDef | ColGroupDef) => "groupId" in r && r.groupId === first,
) as ColGroupDef;
if (existing) {
existing["children"] = addColDef(
colId,
parts.slice(1),
existing.children,
request,
);
} else {
const colDef: any = {};
const isGroup = parts.length > 1;
if (isGroup) {
colDef["groupId"] = first;
colDef["headerName"] = first;
} else {
const valueCol = request.valueCols.find((r) => r.field === first);
if (valueCol) {
colDef["colId"] = colId;
colDef["headerName"] = valueCol.displayName;
colDef["field"] = colId;
}
}
const children = addColDef(colId, parts.slice(1), [], request);
if (children.length > 0) {
colDef["children"] = children;
}
res.push(colDef);
}
return res;
}
// The supplied order is the pivot result columns' natural order, used when the YEAR pill in the pivot panel is
// cycled to no sort. This example supplies the year groups shuffled so that order is distinguishable from asc/desc.
// Only the groups move, so Gold/Silver/Bronze keep their order within each year.
const shuffleYearGroups: (yearGroups: ColGroupDef[]) => ColGroupDef[] = (
yearGroups: ColGroupDef[],
) => {
return yearGroups
.map((group) => ({ group, rank: window.agRandom() }))
.sort((a, b) => a.rank - b.rank)
.map((entry) => entry.group);
};
const createPivotResultColumns: (
request: IServerSideGetRowsRequest,
pivotFields: string[],
) => ColGroupDef[] = (
request: IServerSideGetRowsRequest,
pivotFields: string[],
) => {
if (request.pivotMode && request.pivotCols.length > 0) {
const pivotResultCols: ColGroupDef[] = [];
pivotFields.forEach((field) =>
addColDef(field, field.split("_"), pivotResultCols, request),
);
return shuffleYearGroups(pivotResultCols);
}
return [];
};
const GridExample = () => {
const containerStyle = useMemo(() => ({ width: "100%", height: "100%" }), []);
const gridStyle = useMemo(() => ({ height: "100%", width: "100%" }), []);
const [columnDefs, setColumnDefs] = useState<ColDef[]>([
{ field: "country", rowGroup: true, enableRowGroup: true },
{ field: "sport", enableRowGroup: true },
{ field: "year", pivot: true, enablePivot: true }, // pivot on 'year'
{ field: "gold", aggFunc: "sum", enableValue: true },
{ field: "silver", aggFunc: "sum", enableValue: true },
{ field: "bronze", aggFunc: "sum", enableValue: true },
]);
const defaultColDef = useMemo<ColDef>(() => {
return {
flex: 1,
minWidth: 100,
};
}, []);
const autoGroupColumnDef = useMemo<AutoGroupColumnDef>(() => {
return {
minWidth: 200,
};
}, []);
const sideBar = useMemo<
SideBarDef | string | string[] | boolean | null
>(() => {
return {
toolPanels: ["columns"],
};
}, []);
const onGridReady = useCallback((params: GridReadyEvent) => {
fetch("https://www.ag-grid.com/example-assets/olympic-winners.json")
.then((resp) => resp.json())
.then((data: IOlympicData[]) => {
// setup the fake server with entire dataset
const fakeServer = new FakeServer(data);
// create datasource with a reference to the fake server
const datasource = getServerSideDatasource(fakeServer);
// register the datasource with the grid
params.api!.setGridOption("serverSideDatasource", datasource);
});
}, []);
return (
<AgGridProvider modules={modules}>
<div style={containerStyle}>
<div style={gridStyle}>
<AgGridReact<IOlympicData>
columnDefs={columnDefs}
defaultColDef={defaultColDef}
autoGroupColumnDef={autoGroupColumnDef}
rowModelType={"serverSide"}
pivotMode={true}
sideBar={sideBar}
rowGroupPanelShow={"always"}
pivotPanelShow={"always"}
onGridReady={onGridReady}
/>
</div>
</div>
</AgGridProvider>
);
};
const root = createRoot(document.getElementById("root")!);
root.render(
<StrictMode>
<GridExample />
</StrictMode>,
);
// This fake server uses http://alasql.org/ to mimic how a real server
// might generate sql queries from the Server-Side Row Model request.
// To keep things simple it does the bare minimum to support the example.
export function FakeServer(allData) {
alasql.options.cache = false;
return {
getData: (request) => {
const result = executeQuery(request);
return {
success: true,
rows: result,
lastRow: getLastRowIndex(request, result),
pivotFields: getPivotFields(request),
};
},
};
function executeQuery(request) {
const { pivotCols, valueCols } = request;
const [pivotCol] = pivotCols;
if (valueCols.length === 0) {
return [];
}
const results = [];
valueCols.forEach((valueCol) => {
const pivotResults = executePivotQuery(request, pivotCol, valueCol);
pivotResults.forEach((pivotResult, i) => {
results[i] = { ...results[i], ...pivotResult };
});
});
return alasql(`SELECT * FROM ?${orderBySql(request)}`, [results]);
}
function orderBySql({ sortModel }) {
if (sortModel.length === 0) return '';
const sorts = sortModel.map(({ colId, sort }) => `\`${colId}\` ${sort.toUpperCase()}`);
return ` ORDER BY ${sorts.join(', ')}`;
}
function executePivotQuery(request, pivotCol, valueCol) {
const { groupKeys, rowGroupCols } = request;
const groupsToUse = rowGroupCols.slice(groupKeys.length, groupKeys.length + 1);
const selectGroupCols = groupsToUse.map((groupCol) => groupCol.id).join(', ');
const SQL = `SELECT ${selectGroupCols}, (${pivotCol.id} + '_${
valueCol.id
}') AS ${pivotCol.id}, ${valueCol.id} FROM ? PIVOT (${valueCol.aggFunc}([${
valueCol.id
}]) FOR ${pivotCol.id})${whereSql(request)}`;
console.log('[FakeServer] - about to execute query:', SQL);
return extractRowsForBlock(request, alasql(SQL, [allData]));
}
function whereSql({ rowGroupCols, groupKeys }) {
const whereParts = groupKeys
? groupKeys.map((key, i) => `${rowGroupCols[i].id} = ${typeof key === 'string' ? `'${key}'` : key}`)
: [];
return whereParts.length > 0 ? ` WHERE ${whereParts.join(' AND ')}` : '';
}
function extractRowsForBlock({ startRow, endRow }, results) {
const blockSize = endRow - startRow + 1;
return results.slice(startRow, startRow + blockSize);
}
function getPivotFields({ pivotCols, valueCols }) {
const [pivotCol] = pivotCols;
const result = flatten(
valueCols.map((valueCol) => {
const sql = `SELECT DISTINCT (${pivotCol.id} + '_${valueCol.id}') AS ${pivotCol.id} FROM ? ORDER BY ${pivotCol.id}`;
return alasql(sql, [allData]);
})
);
return flatten(result.map((x) => x[pivotCol.id]));
}
function getLastRowIndex({ startRow, endRow }, results) {
if (!results || results.length === 0) {
return null;
}
const currentLastRow = startRow + results.length;
return currentLastRow <= endRow ? currentLastRow : -1;
}
}
const flatten = (arrayOfArrays) => [].concat(...arrayOfArrays);
export interface IOlympicData {
athlete: string,
age: number,
country: string,
year: number,
date: string,
sport: string,
gold: number,
silver: number,
bronze: number,
total: number
} You can control the order of the pivot result columns by sorting the pivotColDefs array before passing it to api.setPivotResultColumns(pivotColDefs). That supplied order is the natural order, and it is what the grid shows until Pivot Column Sorting is applied - so unlike generated pivot result columns, supplied ones default to no sort rather than ascending. An explicit pivotSort of 'asc' or 'desc' orders the supplied column groups by header name instead, so sorting from a pivot column's pill reorders them without the grid asking the server for the columns again.
Example: Pivot Column Groups Copy Link
The example below demonstrates server-side Pivoting with multiple row groups where there are multiple value columns ('gold', 'silver', 'bronze') under the 'year' pivot column group. Note the following:
- Pivot mode is enabled through the grid option
pivotMode=true. - A pivot is placed on the Year column via
pivot=truedefined on the column definition. - Rows are grouped by Country and Sport with
rowGroup=truedefined on their column definitions. - The Gold, Silver and Bronze value columns have
aggFunc='sum'defined on their column definitions. - The
pivotColsandpivotModeproperties in the request are used by the server to perform pivoting. - New column group definitions are generated from the
pivotResultFieldsprovided by the success callback. - Open the browser's dev console to view the request supplied to the datasource.
("use client");
import React, {
useCallback,
useMemo,
useRef,
useState,
StrictMode,
} from "react";
import { createRoot } from "react-dom/client";
import { AgGridReact, AgGridProvider } from "ag-grid-react";
import "./styles.css";
import {
AutoGroupColumnDef,
ColDef,
ColGroupDef,
GridApi,
GridOptions,
GridReadyEvent,
IServerSideDatasource,
ModuleRegistry,
ProcessPivotResultColDef,
RowModelType,
enableDevValidations,
} from "ag-grid-community";
import {
ColumnMenuModule,
ColumnsToolPanelModule,
ContextMenuModule,
RowGroupingModule,
ServerSideRowModelModule,
} from "ag-grid-enterprise";
import { FakeServer } from "./fakeServer";
import { IOlympicData } from "./interfaces";
if (process.env.NODE_ENV !== "production") {
enableDevValidations();
}
const modules = [
ColumnsToolPanelModule,
ColumnMenuModule,
ContextMenuModule,
RowGroupingModule,
ServerSideRowModelModule,
];
const getServerSideDatasource: (server: any) => IServerSideDatasource = (
server: any,
) => {
return {
getRows: (params) => {
const request = params.request;
console.log("[Datasource] - rows requested by grid: ", params.request);
const response = server.getData(request);
// simulating real server call with a 500ms delay
setTimeout(() => {
if (response.success) {
// supply data to grid
params.success({
rowData: response.rows,
rowCount: response.lastRow,
pivotResultFields: response.pivotFields,
});
} else {
params.fail();
}
}, 500);
},
};
};
const GridExample = () => {
const gridRef = useRef<AgGridReact<IOlympicData>>(null);
const containerStyle = useMemo(() => ({ width: "100%", height: "100%" }), []);
const gridStyle = useMemo(() => ({ height: "100%", width: "100%" }), []);
const [columnDefs, setColumnDefs] = useState<ColDef[]>([
{ field: "country", rowGroup: true },
{ field: "sport", rowGroup: true },
{ field: "year", pivot: true }, // pivot on 'year'
{ field: "total", aggFunc: "sum" },
{ field: "gold", aggFunc: "sum" },
{ field: "silver", aggFunc: "sum" },
{ field: "bronze", aggFunc: "sum" },
]);
const defaultColDef = useMemo<ColDef>(() => {
return {
width: 150,
};
}, []);
const autoGroupColumnDef = useMemo<AutoGroupColumnDef>(() => {
return {
minWidth: 200,
};
}, []);
const processPivotResultColDef = useCallback((colDef: ColDef) => {
const pivotValueColumn = colDef.pivotValueColumn;
if (!pivotValueColumn) return;
// if column is not the total column, it should only be shown when expanded.
// this will enable expandable column groups.
if (pivotValueColumn.getColId() !== "total") {
colDef.columnGroupShow = "open";
}
}, []);
const onGridReady = useCallback((params: GridReadyEvent) => {
fetch("https://www.ag-grid.com/example-assets/olympic-winners.json")
.then((resp) => resp.json())
.then((data: IOlympicData[]) => {
// setup the fake server with entire dataset
const fakeServer = new FakeServer(data);
// create datasource with a reference to the fake server
const datasource = getServerSideDatasource(fakeServer);
// register the datasource with the grid
params.api!.setGridOption("serverSideDatasource", datasource);
});
}, []);
const expand = useCallback((key?: string, open = false) => {
if (key) {
gridRef.current!.api.setColumnGroupState([{ groupId: key, open: open }]);
return;
}
const existingState = gridRef.current!.api.getColumnGroupState();
const expandedState = existingState.map(
(s: { groupId: string; open: boolean }) => ({
groupId: s.groupId,
open: open,
}),
);
gridRef.current!.api.setColumnGroupState(expandedState);
}, []);
return (
<AgGridProvider modules={modules}>
<div style={containerStyle}>
<div className="example-wrapper">
<div style={{ marginBottom: "5px" }}>
<button onClick={() => expand("2000", true)}>Expand 2000</button>
<button onClick={() => expand("2000")}>Collapse 2000</button>
<button onClick={() => expand(undefined, true)}>Expand All</button>
<button onClick={() => expand(undefined)}>Collapse All</button>
</div>
<div style={gridStyle}>
<AgGridReact<IOlympicData>
ref={gridRef}
columnDefs={columnDefs}
defaultColDef={defaultColDef}
autoGroupColumnDef={autoGroupColumnDef}
rowModelType={"serverSide"}
pivotMode={true}
processPivotResultColDef={processPivotResultColDef}
onGridReady={onGridReady}
/>
</div>
</div>
</div>
</AgGridProvider>
);
};
const root = createRoot(document.getElementById("root")!);
root.render(
<StrictMode>
<GridExample />
</StrictMode>,
);
.example-wrapper {
display: flex;
flex-direction: column;
height: 100%;
}
#myGrid {
flex: 1 1 auto;
width: 100%;
}
// This fake server uses http://alasql.org/ to mimic how a real server
// might generate sql queries from the Server-Side Row Model request.
// To keep things simple it does the bare minimum to support the example.
export function FakeServer(allData) {
alasql.options.cache = false;
return {
getData: function (request) {
const result = executeQuery(request);
return {
success: true,
rows: result,
lastRow: getLastRowIndex(request, result),
pivotFields: getPivotFields(request),
};
},
};
function executeQuery(request) {
const pivotCols = request.pivotCols;
const pivotCol = pivotCols[0]; // 'alasql' can only pivot on a single column
// 'alasql' only supports pivoting on a single value column, to workaround this limitation we need to perform
// separate queries for each value column and combine the results
const results = [];
request.valueCols.forEach(function (valueCol) {
const pivotResults = executePivotQuery(request, pivotCol, valueCol);
// merge each row into existing results
for (let i = 0; i < pivotResults.length; i++) {
var pivotResult = pivotResults[i];
var result = results[i] || {};
Object.keys(pivotResult).forEach(function (key) {
result[key] = pivotResult[key];
});
results[i] = result;
}
});
return alasql('SELECT * FROM ?' + orderBySql(request), [results]);
}
function orderBySql(request) {
const sortModel = request.sortModel;
if (sortModel.length === 0) return '';
const sorts = sortModel.map(function (s) {
return '`' + s.colId + '` ' + s.sort.toUpperCase();
});
return ' ORDER BY ' + sorts.join(', ');
}
function executePivotQuery(request, pivotCol, valueCol) {
const groupKeys = request.groupKeys;
const groupsToUse = request.rowGroupCols.slice(groupKeys.length, groupKeys.length + 1);
const selectGroupCols = groupsToUse
.map(function (groupCol) {
return groupCol.id;
})
.join(', ');
const SQL_TEMPLATE = "SELECT {0}, ({1} + '_{2}') AS {1}, {2} FROM ? PIVOT (SUM([{2}]) FOR {1})";
const SQL = interpolate(SQL_TEMPLATE, [selectGroupCols, pivotCol.id, valueCol.id]) + whereSql(request);
console.log('[FakeServer] - about to execute query:', SQL);
const result = alasql(SQL, [allData]);
// workaround - 'alasql' doesn't support PIVOT + LIMIT
return extractRowsForBlock(request, result);
}
function whereSql(request) {
const rowGroups = request.rowGroupCols;
const groupKeys = request.groupKeys;
const whereParts = [];
if (groupKeys) {
groupKeys.forEach(function (key, i) {
const value = typeof key === 'string' ? "'" + key + "'" : key;
whereParts.push(rowGroups[i].id + ' = ' + value);
});
}
if (whereParts.length > 0) {
return ' WHERE ' + whereParts.join(' AND ');
}
return '';
}
function extractRowsForBlock(request, results) {
const blockSize = request.endRow - request.startRow + 1;
return results.slice(request.startRow, request.startRow + blockSize);
}
function getPivotFields(request) {
const pivotCol = request.pivotCols[0];
const template = "SELECT DISTINCT ({0} + '_{1}') AS {0} FROM ? ORDER BY {0}";
const result = flatten(
request.valueCols.map(function (valueCol) {
const args = [pivotCol.id, valueCol.id];
const sql = interpolate(template, args);
return alasql(sql, [allData]);
})
);
return flatten(
result.map(function (x) {
return x[pivotCol.id];
})
);
}
function getLastRowIndex(request, results) {
if (!results || results.length === 0) {
return null;
}
const currentLastRow = request.startRow + results.length;
return currentLastRow <= request.endRow ? currentLastRow : -1;
}
}
// IE Workaround - as templates literal are not supported
function interpolate(str, o) {
return str.replace(/{([^{}]*)}/g, function (a, b) {
const r = o[b];
return typeof r === 'string' || typeof r === 'number' ? r : a;
});
}
function flatten(arrayOfArrays) {
return [].concat.apply([], arrayOfArrays);
}
export interface IOlympicData {
athlete: string,
age: number,
country: string,
year: number,
date: string,
sport: string,
gold: number,
silver: number,
bronze: number,
total: number
} Example: Slice and Dice Copy Link
A mock data store running inside the browser is used in the example below. The purpose of the mock server is to demonstrate the interaction between the grid and the server. For your application, your server will need to understand the requests from the client and build SQL (or the SQL equivalent if using a no-SQL data store) to run the relevant query against the data store.
The example demonstrates the following:
Columns
Athlete, Age, Country, YearandSportall haveenableRowGroup=truewhich means they can be grouped on. To group, you drag the columns to the row group panel section. By default the example is grouping byCountryand thenYearas these columns haverowGroup=true.Columns
Gold, SilverandBronzeall haveenableValue=truewhich means they can be aggregated on. To aggregate, you drag the column to theValuessection. When you are grouping, all columns in theValuessection will be aggregated.You can turn the grid into Pivot Mode. To do this, you click the pivot mode checkbox. When the grid is in pivot mode, the grid behaves similarly to an Excel grid. This extra information is passed to your server as part of the request and it is your server's responsibility to return the data in the correct structure.
Columns
Age, Country, YearandSportall haveenablePivot=truewhich means they can be pivoted on when Pivot Mode is active. To pivot, you drag the column to the Pivot section.Note that when you pivot, it is not possible to drill all the way down the leaf levels.
In addition to grouping, aggregation and pivot, the example also demonstrates filtering. The columns Country and Year have grid-provided filters. The column Age has an example-provided custom filter. You can use whatever filter you want, as long as your server knows what to do with it.
("use client");
import React, {
useCallback,
useMemo,
useRef,
useState,
StrictMode,
} from "react";
import { createRoot } from "react-dom/client";
import { AgGridReact, AgGridProvider } from "ag-grid-react";
import {
AutoGroupColumnDef,
ColDef,
ColGroupDef,
GridApi,
GridOptions,
GridReadyEvent,
ModuleRegistry,
NumberFilterModule,
RowModelType,
SideBarDef,
enableDevValidations,
} from "ag-grid-community";
import {
ColumnMenuModule,
ColumnsToolPanelModule,
ContextMenuModule,
FiltersToolPanelModule,
RowGroupingModule,
RowGroupingPanelModule,
ServerSideRowModelModule,
SetFilterModule,
} from "ag-grid-enterprise";
import { getCountries } from "./countries";
import { createFakeServer, createServerSideDatasource } from "./server";
import { IOlympicData } from "./interfaces";
if (process.env.NODE_ENV !== "production") {
enableDevValidations();
}
const modules = [
NumberFilterModule,
ColumnsToolPanelModule,
FiltersToolPanelModule,
ColumnMenuModule,
ContextMenuModule,
RowGroupingModule,
ServerSideRowModelModule,
SetFilterModule,
RowGroupingPanelModule,
];
const countries = getCountries();
const GridExample = () => {
const containerStyle = useMemo(() => ({ width: "100%", height: "100%" }), []);
const gridStyle = useMemo(() => ({ height: "100%", width: "100%" }), []);
const [columnDefs, setColumnDefs] = useState<ColDef[]>([
{ field: "athlete", enableRowGroup: true, filter: false },
{
field: "age",
enableRowGroup: true,
enablePivot: true,
filter: "agNumberColumnFilter",
filterParams: {
filterOptions: ["equals", "lessThan", "greaterThan"],
maxNumConditions: 1,
},
},
{
field: "country",
enableRowGroup: true,
enablePivot: true,
rowGroup: true,
hide: true,
filter: "agSetColumnFilter",
filterParams: { values: countries },
},
{
field: "year",
enableRowGroup: true,
enablePivot: true,
rowGroup: true,
hide: true,
filter: "agSetColumnFilter",
filterParams: {
values: ["2000", "2002", "2004", "2006", "2008", "2010", "2012"],
},
},
{ field: "sport", enableRowGroup: true, enablePivot: true, filter: false },
{ field: "gold", aggFunc: "sum", filter: false, enableValue: true },
{ field: "silver", aggFunc: "sum", filter: false, enableValue: true },
{ field: "bronze", aggFunc: "sum", filter: false, enableValue: true },
]);
const defaultColDef = useMemo<ColDef>(() => {
return {
flex: 1,
minWidth: 150,
// restrict what aggregation functions the columns can have,
// include a custom function 'random' that just returns a
// random number
allowedAggFuncs: ["sum", "min", "max", "random"],
filter: true,
};
}, []);
const autoGroupColumnDef = useMemo<AutoGroupColumnDef>(() => {
return {
width: 180,
};
}, []);
const onGridReady = useCallback((params: GridReadyEvent) => {
fetch("https://www.ag-grid.com/example-assets/olympic-winners.json")
.then((resp) => resp.json())
.then((data: IOlympicData[]) => {
const fakeServer = createFakeServer(data);
const datasource = createServerSideDatasource(fakeServer);
params.api!.setGridOption("serverSideDatasource", datasource);
});
}, []);
return (
<AgGridProvider modules={modules}>
<div style={containerStyle}>
<div style={gridStyle}>
<AgGridReact<IOlympicData>
columnDefs={columnDefs}
defaultColDef={defaultColDef}
autoGroupColumnDef={autoGroupColumnDef}
rowModelType={"serverSide"}
rowGroupPanelShow={"always"}
pivotPanelShow={"always"}
sideBar={true}
maxConcurrentDatasourceRequests={1}
maxBlocksInCache={2}
purgeClosedRowNodes={true}
onGridReady={onGridReady}
/>
</div>
</div>
</AgGridProvider>
);
};
const root = createRoot(document.getElementById("root")!);
root.render(
<StrictMode>
<GridExample />
</StrictMode>,
);
export function getCountries() {
return [
'United States',
'Russia',
'Australia',
'Canada',
'Norway',
'China',
'Zimbabwe',
'Netherlands',
'South Korea',
'Croatia',
'France',
'Japan',
'Hungary',
'Germany',
'Poland',
'South Africa',
'Sweden',
'Ukraine',
'Italy',
'Czech Republic',
'Austria',
'Finland',
'Romania',
'Great Britain',
'Jamaica',
'Singapore',
'Belarus',
'Chile',
'Spain',
'Tunisia',
'Brazil',
'Slovakia',
'Costa Rica',
'Bulgaria',
'Switzerland',
'New Zealand',
'Estonia',
'Kenya',
'Ethiopia',
'Trinidad and Tobago',
'Turkey',
'Morocco',
'Bahamas',
'Slovenia',
'Armenia',
'Azerbaijan',
'India',
'Puerto Rico',
'Egypt',
'Kazakhstan',
'Iran',
'Georgia',
'Lithuania',
'Cuba',
'Colombia',
'Mongolia',
'Uzbekistan',
'North Korea',
'Tajikistan',
'Kyrgyzstan',
'Greece',
'Macedonia',
'Moldova',
'Chinese Taipei',
'Indonesia',
'Thailand',
'Vietnam',
'Latvia',
'Venezuela',
'Mexico',
'Nigeria',
'Qatar',
'Serbia',
'Serbia and Montenegro',
'Hong Kong',
'Denmark',
'Portugal',
'Argentina',
'Afghanistan',
'Gabon',
'Dominican Republic',
'Belgium',
'Kuwait',
'United Arab Emirates',
'Cyprus',
'Israel',
'Algeria',
'Montenegro',
'Iceland',
'Paraguay',
'Cameroon',
'Saudi Arabia',
'Ireland',
'Malaysia',
'Uruguay',
'Togo',
'Mauritius',
'Syria',
'Botswana',
'Guatemala',
'Bahrain',
'Grenada',
'Uganda',
'Sudan',
'Ecuador',
'Panama',
'Eritrea',
'Sri Lanka',
'Mozambique',
'Barbados',
];
}
export function createServerSideDatasource(fakeServer) {
class ServerSideDatasource {
constructor(private fakeServer: FakeServer) {}
getRows(params) {
this.fakeServer.getData(params.request, (resultForGrid, lastRow, pivotFields) => {
params.success({
rowData: resultForGrid,
rowCount: lastRow,
pivotResultFields: pivotFields,
});
});
}
}
return new ServerSideDatasource(fakeServer);
}
export function createFakeServer(data) {
// THIS IS NOT PRODUCTION CODE
// in your application, you should be implementing the server logic in your server, maybe in JavaScript, but
// also maybe in Java, C# or another server side language. The server side would then typically query a database
// or another data store to get the data, and the grouping, aggregation and pivoting would be done by the data store.
// This fake server is only intended to demonstrate the interface between AG Grid and the server side. The
// implementation details are not intended to be an example of how your server side should create results.
return new FakeServer(data);
}
class FakeServer {
constructor(allData) {
this.allData = allData;
}
getData(request, callback) {
let {
// Filtering
filterModel,
// Pivoting
pivotCols,
pivotMode,
// Grouping
groupKeys,
rowGroupCols,
// Aggregation
valueCols,
// Sorting
sortModel,
} = request;
// Pivot is only active if we have pivot columns and aggregate columns
const pivotActive = pivotMode && pivotCols.length > 0 && valueCols.length > 0;
/** Filter data */
let rowData = this.filterList(this.allData, filterModel);
/** Pivot data */
let pivotFields = null;
if (pivotActive) {
const pivotResult = this.pivot(pivotCols, rowGroupCols, valueCols, rowData);
// Pivoted row data
rowData = pivotResult.data;
// Aggregate instead by the pivot columns
valueCols = pivotResult.aggCols;
// Pivoted columns fields to allow grid to generate pivot result columns
pivotFields = pivotResult.pivotFields;
}
/** Group & Aggregate data */
if (rowGroupCols.length > 0) {
// When grouping we only return data for one group per request, so filter the other data out
rowData = this.filterOutOtherGroups(rowData, groupKeys, rowGroupCols);
// If this group isn't the bottom level, then group the rows rather than returning them
const showingGroupLevel = rowGroupCols.length > groupKeys.length;
if (showingGroupLevel) {
rowData = this.buildGroupsFromData(rowData, rowGroupCols, groupKeys, valueCols);
}
} else if (pivotMode) {
// When pivoting without groups, aggregate all data into one row
const rootGroup = this.aggregateList(rowData, valueCols);
rowData = [rootGroup];
}
/** Sort data */
rowData = this.sortList(rowData, sortModel);
const lastRow = rowData.length;
/** Paginate data */
if (request.startRow != null && request.endRow != null) {
rowData = rowData.slice(request.startRow, request.endRow);
}
// so that the example behaves like a server side call, we put
// it in a timeout to a) give a delay and b) make it asynchronous
setTimeout(function () {
callback(rowData, lastRow, pivotFields);
}, 1000);
}
sortList(data, sortModel) {
const sortPresent = sortModel && sortModel.length > 0;
if (!sortPresent) {
return data;
}
// do an in memory sort of the data, across all the fields
const resultOfSort = data.slice();
resultOfSort.sort(function (a, b) {
for (let k = 0; k < sortModel.length; k++) {
const sortColModel = sortModel[k];
const valueA = a[sortColModel.colId];
const valueB = b[sortColModel.colId];
// this filter didn't find a difference, move onto the next one
if (valueA == valueB) {
continue;
}
const sortDirection = sortColModel.sort === 'asc' ? 1 : -1;
if (valueA > valueB) {
return sortDirection;
} else {
return sortDirection * -1;
}
}
// no filters found a difference
return 0;
});
return resultOfSort;
}
filterList(data, filterModel) {
const filterPresent = filterModel && Object.keys(filterModel).length > 0;
if (!filterPresent) {
return data;
}
const resultOfFilter = [];
for (let i = 0; i < data.length; i++) {
const item = data[i];
if (filterModel.age) {
const age = item.age;
const allowedAge = parseInt(filterModel.age.filter);
if (filterModel.age.type == 'equals') {
if (age !== allowedAge) {
continue;
}
} else if (filterModel.age.type == 'lessThan') {
if (age >= allowedAge) {
continue;
}
} else {
if (age <= allowedAge) {
continue;
}
}
}
if (filterModel.year) {
if (filterModel.year.values.indexOf(item.year.toString()) < 0) {
// year didn't match, so skip this record
continue;
}
}
if (filterModel.country) {
if (filterModel.country.values.indexOf(item.country) < 0) {
continue;
}
}
resultOfFilter.push(item);
}
return resultOfFilter;
}
// function does pivoting. this is very funky logic, doing pivoting and creating pivot result columns on the fly.
// if you are using the AG Grid Enterprise Row Model, remember this would all be done on your server side with a
// database or something that does pivoting for you - this messy code is just for demo purposes on how to use
// ag-Gird, it's not supposed to be beautiful production quality code.
pivot(pivotCols, rowGroupCols, valueCols, data) {
const pivotData = [];
const aggColsList = [];
const pivotFields = new Set();
data.forEach(function (item) {
const pivotValues = [];
pivotCols.forEach(function (pivotCol) {
const pivotField = pivotCol.id;
const pivotValue = item[pivotField];
if (pivotValue !== null && pivotValue !== undefined && pivotValue.toString) {
pivotValues.push(pivotValue.toString());
} else {
pivotValues.push('-');
}
});
const pivotItem = {};
valueCols.forEach(function (valueCol) {
const valField = valueCol.id;
const pivotKey = pivotValues.join('_');
const colKey = `${pivotKey}_${valField}`;
if (!pivotFields.has(colKey)) {
pivotFields.add(colKey);
// add value col so server can aggregate later
aggColsList.push({
id: colKey,
field: colKey,
aggFunc: valueCol.aggFunc,
});
}
const value = item[valField];
pivotItem[colKey] = value;
});
rowGroupCols.forEach(function (rowGroupCol) {
const rowGroupField = rowGroupCol.id;
pivotItem[rowGroupField] = item[rowGroupField];
});
pivotData.push(pivotItem);
});
return {
data: pivotData,
aggCols: aggColsList,
pivotFields: Array.from(pivotFields),
};
}
buildGroupsFromData(rowData, rowGroupCols, groupKeys, valueCols) {
const rowGroupCol = rowGroupCols[groupKeys.length];
const field = rowGroupCol.id;
const mappedRowData = this.groupBy(rowData, field);
if (!mappedRowData) {
return [];
}
const groups = [];
const that = this;
for (const key in mappedRowData) {
const thisRowData = mappedRowData[key];
const groupItem = that.aggregateList(thisRowData, valueCols);
groupItem[field] = key;
groups.push(groupItem);
}
return groups;
}
aggregateList(rowData, valueCols) {
const result = {};
for (let i = 0; i < valueCols.length; i++) {
const col = valueCols[i];
const field = col.id;
// the aggregation we do depends on which agg func the user picked
switch (col.aggFunc) {
case 'sum':
let sum = 0;
for (let i = 0; i < rowData.length; i++) {
const row = rowData[i];
const value = row[field];
if (value === undefined) continue;
sum += value;
}
result[field] = sum;
break;
case 'min':
let min = null;
for (let i = 0; i < rowData.length; i++) {
const row = rowData[i];
const value = row[field];
if (value === undefined) continue;
if (min === null || min > value) {
min = value;
}
}
result[field] = min;
break;
case 'max':
let max = null;
for (let i = 0; i < rowData.length; i++) {
const row = rowData[i];
const value = row[field];
if (value === undefined) continue;
if (max === null || max < value) {
max = value;
}
}
result[field] = max;
break;
case 'random':
result[field] = window.agRandom(); // just make up a number
break;
default:
console.warn('unrecognised aggregation function: ' + valueCol.aggFunc);
break;
}
}
return result;
}
// if user is down some group levels, we take everything else out. eg
// if user has opened the two groups United States and 2002, we filter
// out everything that is not equal to United States and 2002.
filterOutOtherGroups(originalData, groupKeys, rowGroupCols) {
let filteredData = originalData;
const that = this;
// if we are inside a group, then filter out everything that is not
// part of this group
groupKeys.forEach(function (groupKey, index) {
const rowGroupCol = rowGroupCols[index];
const field = rowGroupCol.id;
filteredData = that.filter(filteredData, function (item) {
return item[field] == groupKey;
});
});
return filteredData;
}
groupBy(data, field) {
const result = {};
data.forEach(function (item) {
const key = item[field];
let listForThisKey = result[key];
if (!listForThisKey) {
listForThisKey = [];
result[key] = listForThisKey;
}
listForThisKey.push(item);
});
return result;
}
filter(data, callback) {
const result = [];
data.forEach(function (item) {
if (callback(item)) {
result.push(item);
}
});
return result;
}
}
export interface IOlympicData {
athlete: string,
age: number,
country: string,
year: number,
date: string,
sport: string,
gold: number,
silver: number,
bronze: number,
total: number
} Batching Data Requests Copy Link
You can stage multiple data requests by using the Columns Tool Panel. This allows multiple configuration changes to be applied in a single update, avoiding unnecessary intermediate recomputations or server requests. See SSRM Row Grouping â Deferred Column Configuration for a detailed example.