Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,8 @@ After the grid is initialised aggregations can be applied / retrieved / removed
## Retrieving Aggregated Children

The method `rowNode.getAggregatedChildren(colKey)` with Client Side Row Model returns the immediate children that contribute to the aggregation
of a group row. This is useful when implementing custom logic based on aggregated data.
of a group row. This is useful when implementing custom logic based on aggregated data or when
[Editing Group Rows](./grouping-edit/) to update child rows accordingly.

- For regular group columns, returns the direct children used for aggregation (respecting `suppressAggFilteredOnly`
and `groupAggFiltering` settings).
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
export const getData = () => [
// Europe - Corporate
{ id: 'fr-paris', region: 'Europe', segment: 'Corporate', country: 'France', amount: 30 },
{ id: 'fr-lyon', region: 'Europe', segment: 'Corporate', country: 'France', amount: 30 },
{ id: 'de-berlin', region: 'Europe', segment: 'Corporate', country: 'Germany', amount: 35 },
{ id: 'de-hamburg', region: 'Europe', segment: 'Corporate', country: 'Germany', amount: 25 },
{ id: 'es-madrid', region: 'Europe', segment: 'Corporate', country: 'Spain', amount: 28 },
{ id: 'es-barcelona', region: 'Europe', segment: 'Corporate', country: 'Spain', amount: 32 },
{ id: 'uk-london', region: 'Europe', segment: 'Corporate', country: 'UK', amount: 45 },
{ id: 'uk-manchester', region: 'Europe', segment: 'Corporate', country: 'UK', amount: 35 },
// Europe - Enterprise
{ id: 'it-rome', region: 'Europe', segment: 'Enterprise', country: 'Italy', amount: 40 },
{ id: 'it-milan', region: 'Europe', segment: 'Enterprise', country: 'Italy', amount: 20 },
{ id: 'pl-warsaw', region: 'Europe', segment: 'Enterprise', country: 'Poland', amount: 26 },
{ id: 'pl-krakow', region: 'Europe', segment: 'Enterprise', country: 'Poland', amount: 24 },
{ id: 'nl-amsterdam', region: 'Europe', segment: 'Enterprise', country: 'Netherlands', amount: 38 },
{ id: 'nl-rotterdam', region: 'Europe', segment: 'Enterprise', country: 'Netherlands', amount: 32 },
// Americas - Corporate
{ id: 'us-nyc', region: 'Americas', segment: 'Corporate', country: 'USA', amount: 70 },
{ id: 'us-la', region: 'Americas', segment: 'Corporate', country: 'USA', amount: 30 },
{ id: 'us-austin', region: 'Americas', segment: 'Corporate', country: 'USA', amount: 25 },
{ id: 'us-chicago', region: 'Americas', segment: 'Corporate', country: 'USA', amount: 55 },
{ id: 'ca-toronto', region: 'Americas', segment: 'Corporate', country: 'Canada', amount: 35 },
{ id: 'ca-vancouver', region: 'Americas', segment: 'Corporate', country: 'Canada', amount: 25 },
// Americas - Enterprise
{ id: 'br-sao-paulo', region: 'Americas', segment: 'Enterprise', country: 'Brazil', amount: 30 },
{ id: 'br-rio', region: 'Americas', segment: 'Enterprise', country: 'Brazil', amount: 22 },
{ id: 'mx-tijuana', region: 'Americas', segment: 'Enterprise', country: 'Mexico', amount: 28 },
{ id: 'mx-guadalajara', region: 'Americas', segment: 'Enterprise', country: 'Mexico', amount: 18 },
{ id: 'ar-buenos-aires', region: 'Americas', segment: 'Enterprise', country: 'Argentina', amount: 24 },
// Asia Pacific - Corporate
{ id: 'jp-tokyo', region: 'Asia Pacific', segment: 'Corporate', country: 'Japan', amount: 65 },
{ id: 'jp-osaka', region: 'Asia Pacific', segment: 'Corporate', country: 'Japan', amount: 45 },
{ id: 'au-sydney', region: 'Asia Pacific', segment: 'Corporate', country: 'Australia', amount: 42 },
{ id: 'au-melbourne', region: 'Asia Pacific', segment: 'Corporate', country: 'Australia', amount: 38 },
// Asia Pacific - Enterprise
{ id: 'cn-shanghai', region: 'Asia Pacific', segment: 'Enterprise', country: 'China', amount: 80 },
{ id: 'cn-beijing', region: 'Asia Pacific', segment: 'Enterprise', country: 'China', amount: 70 },
{ id: 'sg-singapore', region: 'Asia Pacific', segment: 'Enterprise', country: 'Singapore', amount: 55 },
{ id: 'kr-seoul', region: 'Asia Pacific', segment: 'Enterprise', country: 'South Korea', amount: 48 },
{ id: 'in-mumbai', region: 'Asia Pacific', segment: 'Enterprise', country: 'India', amount: 36 },
{ id: 'in-bangalore', region: 'Asia Pacific', segment: 'Enterprise', country: 'India', amount: 44 },
];
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { ensureGridReady, test, waitForGridContent } from '@utils/grid/test-utils';

test.agExample(import.meta, () => {
test.eachFramework('Example', async ({ page }) => {
await ensureGridReady(page);
await waitForGridContent(page);
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<div id="myGrid" class="ag-theme-quartz" style="height: 100%; width: 100%"></div>
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
import type { GridApi, GridOptions, GroupRowValueSetterFunc, ValueParserParams } from 'ag-grid-community';
import {
ClientSideRowModelModule,
ModuleRegistry,
NumberFilterModule,
TextEditorModule,
ValidationModule,
createGrid,
} from 'ag-grid-community';
import { RowGroupingModule, SetFilterModule } from 'ag-grid-enterprise';

import { getData } from './data';

let gridApi: GridApi<SalesRecord>;

interface SalesRecord {
id: string;
region: string;
segment: string;
country: string;
amount: number;
}

ModuleRegistry.registerModules([
RowGroupingModule,
ClientSideRowModelModule,
NumberFilterModule,
SetFilterModule,
TextEditorModule,
...(process.env.NODE_ENV !== 'production' ? [ValidationModule] : []),
]);

// Parse input to integer
const amountValueParser = (params: ValueParserParams): number | null => {
const numericValue = Number(params.newValue);
return Number.isFinite(numericValue) ? Math.round(numericValue) : params.oldValue ?? null;
};

/**
* Distributes a new group total equally among children.
*
* `aggregatedChildren` contains the immediate children used for aggregation:
* - For leaf groups: the data rows
* - For non-leaf groups: the child groups
*
* Calling `setDataValue` on a child group triggers `groupRowValueSetter` again,
* enabling recursive cascade through the entire group hierarchy.
*/
const cascadeGroupTotal: GroupRowValueSetterFunc<SalesRecord> = ({
column,
newValue,
eventSource,
aggregatedChildren,
}) => {
const total = Number(newValue);
if (!Number.isFinite(total) || !aggregatedChildren.length) {
return false;
}

// Distribute equally among children
// https://en.wikipedia.org/wiki/Largest_remainder_method
const count = aggregatedChildren.length;
const base = Math.floor(total / count);
let remainder = Math.round(total) - base * count;

// Apply the distributed values
let changed = false;
for (const child of aggregatedChildren) {
let value = base;
if (remainder > 0) {
value++;
remainder--;
}
if (child.setDataValue(column, value, eventSource)) {
changed = true;
}
}
return changed;
};

const gridOptions: GridOptions<SalesRecord> = {
columnDefs: [
{ field: 'region', rowGroup: true, hide: true },
{ field: 'segment', rowGroup: true, hide: true, filter: 'agSetColumnFilter' },
{ field: 'country', filter: 'agSetColumnFilter' },
{
headerName: 'Amount',
field: 'amount',
aggFunc: 'sum',
editable: true,
groupRowEditable: true,
filter: 'agNumberColumnFilter',
valueParser: amountValueParser,
groupRowValueSetter: cascadeGroupTotal,
},
],
autoGroupColumnDef: {
minWidth: 260,
cellRendererParams: {
suppressCount: true,
},
},
defaultColDef: {
flex: 1,
sortable: true,
filter: true,
resizable: true,
},
rowData: getData(),
groupAggFiltering: true,
groupDefaultExpanded: -1,
animateRows: true,
getRowId: ({ data }) => data.id,
};

// setup the grid after the page has finished loading
document.addEventListener('DOMContentLoaded', () => {
const gridDiv = document.querySelector<HTMLElement>('#myGrid')!;
gridApi = createGrid(gridDiv, gridOptions);
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
export const getData = (): SalesRecord[] => [
// Europe - Electronics
{ id: 'eu-fr-elec', region: 'Europe', country: 'France', product: 'Electronics', amount: 120 },
{ id: 'eu-de-elec', region: 'Europe', country: 'Germany', product: 'Electronics', amount: 150 },
{ id: 'eu-es-elec', region: 'Europe', country: 'Spain', product: 'Electronics', amount: 80 },
{ id: 'eu-uk-elec', region: 'Europe', country: 'UK', product: 'Electronics', amount: 140 },
{ id: 'eu-it-elec', region: 'Europe', country: 'Italy', product: 'Electronics', amount: 95 },
// Europe - Clothing
{ id: 'eu-fr-cloth', region: 'Europe', country: 'France', product: 'Clothing', amount: 60 },
{ id: 'eu-de-cloth', region: 'Europe', country: 'Germany', product: 'Clothing', amount: 90 },
{ id: 'eu-es-cloth', region: 'Europe', country: 'Spain', product: 'Clothing', amount: 50 },
{ id: 'eu-uk-cloth', region: 'Europe', country: 'UK', product: 'Clothing', amount: 75 },
{ id: 'eu-it-cloth', region: 'Europe', country: 'Italy', product: 'Clothing', amount: 85 },
// Europe - Food
{ id: 'eu-fr-food', region: 'Europe', country: 'France', product: 'Food', amount: 40 },
{ id: 'eu-de-food', region: 'Europe', country: 'Germany', product: 'Food', amount: 55 },
{ id: 'eu-es-food', region: 'Europe', country: 'Spain', product: 'Food', amount: 35 },
{ id: 'eu-uk-food', region: 'Europe', country: 'UK', product: 'Food', amount: 48 },
{ id: 'eu-it-food', region: 'Europe', country: 'Italy', product: 'Food', amount: 52 },
// Americas - Electronics
{ id: 'am-us-elec', region: 'Americas', country: 'USA', product: 'Electronics', amount: 200 },
{ id: 'am-ca-elec', region: 'Americas', country: 'Canada', product: 'Electronics', amount: 100 },
{ id: 'am-br-elec', region: 'Americas', country: 'Brazil', product: 'Electronics', amount: 70 },
{ id: 'am-mx-elec', region: 'Americas', country: 'Mexico', product: 'Electronics', amount: 65 },
{ id: 'am-ar-elec', region: 'Americas', country: 'Argentina', product: 'Electronics', amount: 45 },
// Americas - Clothing
{ id: 'am-us-cloth', region: 'Americas', country: 'USA', product: 'Clothing', amount: 80 },
{ id: 'am-ca-cloth', region: 'Americas', country: 'Canada', product: 'Clothing', amount: 45 },
{ id: 'am-br-cloth', region: 'Americas', country: 'Brazil', product: 'Clothing', amount: 30 },
{ id: 'am-mx-cloth', region: 'Americas', country: 'Mexico', product: 'Clothing', amount: 35 },
{ id: 'am-ar-cloth', region: 'Americas', country: 'Argentina', product: 'Clothing', amount: 28 },
// Americas - Food
{ id: 'am-us-food', region: 'Americas', country: 'USA', product: 'Food', amount: 60 },
{ id: 'am-ca-food', region: 'Americas', country: 'Canada', product: 'Food', amount: 35 },
{ id: 'am-br-food', region: 'Americas', country: 'Brazil', product: 'Food', amount: 25 },
{ id: 'am-mx-food', region: 'Americas', country: 'Mexico', product: 'Food', amount: 32 },
{ id: 'am-ar-food', region: 'Americas', country: 'Argentina', product: 'Food', amount: 22 },
// Asia Pacific - Electronics
{ id: 'ap-jp-elec', region: 'Asia Pacific', country: 'Japan', product: 'Electronics', amount: 180 },
{ id: 'ap-cn-elec', region: 'Asia Pacific', country: 'China', product: 'Electronics', amount: 220 },
{ id: 'ap-kr-elec', region: 'Asia Pacific', country: 'South Korea', product: 'Electronics', amount: 160 },
{ id: 'ap-au-elec', region: 'Asia Pacific', country: 'Australia', product: 'Electronics', amount: 85 },
{ id: 'ap-in-elec', region: 'Asia Pacific', country: 'India', product: 'Electronics', amount: 110 },
// Asia Pacific - Clothing
{ id: 'ap-jp-cloth', region: 'Asia Pacific', country: 'Japan', product: 'Clothing', amount: 70 },
{ id: 'ap-cn-cloth', region: 'Asia Pacific', country: 'China', product: 'Clothing', amount: 120 },
{ id: 'ap-kr-cloth', region: 'Asia Pacific', country: 'South Korea', product: 'Clothing', amount: 55 },
{ id: 'ap-au-cloth', region: 'Asia Pacific', country: 'Australia', product: 'Clothing', amount: 40 },
{ id: 'ap-in-cloth', region: 'Asia Pacific', country: 'India', product: 'Clothing', amount: 95 },
// Asia Pacific - Food
{ id: 'ap-jp-food', region: 'Asia Pacific', country: 'Japan', product: 'Food', amount: 45 },
{ id: 'ap-cn-food', region: 'Asia Pacific', country: 'China', product: 'Food', amount: 75 },
{ id: 'ap-kr-food', region: 'Asia Pacific', country: 'South Korea', product: 'Food', amount: 38 },
{ id: 'ap-au-food', region: 'Asia Pacific', country: 'Australia', product: 'Food', amount: 30 },
{ id: 'ap-in-food', region: 'Asia Pacific', country: 'India', product: 'Food', amount: 55 },
];
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { ensureGridReady, test, waitForGridContent } from '@utils/grid/test-utils';

test.agExample(import.meta, () => {
test.eachFramework('Example', async ({ page }) => {
await ensureGridReady(page);
await waitForGridContent(page);
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<div id="myGrid" class="ag-theme-quartz" style="height: 100%; width: 100%"></div>
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
import type { GridApi, GridOptions, GroupRowValueSetterFunc, ValueParserParams } from 'ag-grid-community';
import {
ClientSideRowModelModule,
ModuleRegistry,
NumberFilterModule,
TextEditorModule,
ValidationModule,
createGrid,
} from 'ag-grid-community';
import { ColumnsToolPanelModule, PivotModule, RowGroupingModule, SideBarModule } from 'ag-grid-enterprise';

import { getData } from './data';

interface SalesRecord {
id: string;
region: string;
country: string;
product: string;
amount: number;
}

let gridApi: GridApi<SalesRecord>;

ModuleRegistry.registerModules([
RowGroupingModule,
ClientSideRowModelModule,
NumberFilterModule,
TextEditorModule,
PivotModule,
SideBarModule,
ColumnsToolPanelModule,
...(process.env.NODE_ENV !== 'production' ? [ValidationModule] : []),
]);

// Parse input to integer
const amountValueParser = (params: ValueParserParams): number | null => {
const numericValue = Number(params.newValue);
return Number.isFinite(numericValue) ? Math.round(numericValue) : params.oldValue ?? null;
};

/**
* Distributes a new pivot total equally among children.
*
* In pivot mode, `aggregatedChildren` contains only rows matching the pivot keys.
* For example, editing the "Electronics 2024" cell returns only rows where
* product="Electronics" AND year=2024.
*
* `setDataValue` on leaf rows with pivot columns auto-resolves to the underlying
* value column. On group rows, it triggers `groupRowValueSetter` for recursive cascade.
*/
const cascadeGroupTotal: GroupRowValueSetterFunc<SalesRecord> = ({
column,
newValue,
eventSource,
aggregatedChildren,
}) => {
const total = Number(newValue);
if (!Number.isFinite(total) || !aggregatedChildren.length) {
return false;
}

// Distribute equally among children
// https://en.wikipedia.org/wiki/Largest_remainder_method
const count = aggregatedChildren.length;
const base = Math.floor(total / count);
let remainder = Math.round(total) - base * count;

// Apply the distributed values
let changed = false;
for (const child of aggregatedChildren) {
let value = base;
if (remainder > 0) {
value++;
remainder--;
}
if (child.setDataValue(column, value, eventSource)) {
changed = true;
}
}
return changed;
};

const gridOptions: GridOptions<SalesRecord> = {
columnDefs: [
{ field: 'region', rowGroup: true, hide: true },
{ field: 'country', rowGroup: true, hide: true },
{ field: 'product', pivot: true },
{
headerName: 'Amount',
field: 'amount',
aggFunc: 'sum',
editable: true,
groupRowEditable: true,
valueParser: amountValueParser,
groupRowValueSetter: cascadeGroupTotal,
},
],
autoGroupColumnDef: {
minWidth: 200,
cellRendererParams: {
suppressCount: true,
},
},
defaultColDef: {
flex: 1,
minWidth: 120,
sortable: true,
filter: true,
resizable: true,
},
pivotMode: true,
sideBar: 'columns',
rowData: getData(),
groupDefaultExpanded: -1,
getRowId: ({ data }) => data.id,
};

// setup the grid after the page has finished loading
document.addEventListener('DOMContentLoaded', () => {
const gridDiv = document.querySelector<HTMLElement>('#myGrid')!;
gridApi = createGrid(gridDiv, gridOptions);
});
Loading
Loading