---
title: "Aggregation - Custom Functions"
enterprise: true
framework: angular
version: "36.1.0"
---

# Aggregation - Custom Functions

This section covers how custom aggregation functions can be supplied and used in the grid.

#### Registering Functions

```ts
import { Component } from "@angular/core";
import { HttpClient } from "@angular/common/http";
import { AgGridAngular } from "ag-grid-angular";
import {
  AutoGroupColumnDef,
  ClientSideRowModelModule,
  ColDef,
  ColGroupDef,
  GridApi,
  GridOptions,
  GridReadyEvent,
  IAggFuncParams,
  IAggFuncs,
  ModuleRegistry,
  enableDevValidations,
} from "ag-grid-community";
import {
  ColumnMenuModule,
  ColumnsToolPanelModule,
  ContextMenuModule,
  FiltersToolPanelModule,
  RowGroupingModule,
  SetFilterModule,
} from "ag-grid-enterprise";
if (process.env.NODE_ENV !== "production") {
  enableDevValidations();
}

ModuleRegistry.registerModules([
  ClientSideRowModelModule,
  ColumnsToolPanelModule,
  FiltersToolPanelModule,
  ColumnMenuModule,
  ContextMenuModule,
  RowGroupingModule,
  SetFilterModule,
]);
import { IOlympicData } from "./interfaces";

@Component({
  selector: "my-app",
  standalone: true,
  imports: [AgGridAngular],
  template: `<ag-grid-angular
    style="width: 100%; height: 100%;"
    [columnDefs]="columnDefs"
    [aggFuncs]="aggFuncs"
    [defaultColDef]="defaultColDef"
    [autoGroupColumnDef]="autoGroupColumnDef"
    [rowData]="rowData"
    (gridReady)="onGridReady($event)"
  /> `,
})
export class AppComponent {
  columnDefs: ColDef[] = [
    { field: "country", rowGroup: true, hide: true },
    { field: "total", aggFunc: "range" },
  ];
  aggFuncs: IAggFuncs = {
    range: (params: IAggFuncParams<IOlympicData>) => {
      const values = params.values;
      return values.length > 0
        ? Math.max(...values) - Math.min(...values)
        : null;
    },
  };
  defaultColDef: ColDef = {
    flex: 1,
    minWidth: 150,
  };
  autoGroupColumnDef: AutoGroupColumnDef = {
    minWidth: 220,
  };
  rowData!: IOlympicData[];

  constructor(private http: HttpClient) {}

  onGridReady(params: GridReadyEvent<IOlympicData>) {
    this.http
      .get<
        IOlympicData[]
      >("https://www.ag-grid.com/example-assets/olympic-winners.json")
      .subscribe((data) => (this.rowData = data));
  }
}
```

[Live example: Registering Functions](https://www.ag-grid.com/examples/aggregation-custom-functions/registering-functions/angular)

## Registering Custom Functions

Custom functions can be registered to the grid by name via the `aggFuncs` grid option. The functions can then be applied to columns by referencing the function name in the column definition. The default values of `"sum"`, `"min"`, `"max"`, `"first"`, `"last"`, `"count"` and `"avg"` can also be overwritten with custom implementations.

The above example demonstrates the following configuration to register a custom `"range"` function and applies it to the `total` column:

```ts
<ag-grid-angular
    [columnDefs]="columnDefs"
    [aggFuncs]="aggFuncs"
    /* other grid options ... */ />

this.columnDefs = [
    { field: 'total', aggFunc: 'range' },
];
this.aggFuncs = {
    'range': params => {
        const values = params.values;
        return values.length > 0 ? Math.max(...values) - Math.min(...values) : null;
    }
};
```

## Directly Applied Functions

For columns not [Configured via the UI](https://www.ag-grid.com/angular-data-grid/aggregation-columns/#configuring-via-the-ui), it can be simpler to directly apply custom functions to columns. This can be done by passing a custom function directly to the column `aggFunc` property.

> **Warning**
>
> Direct functions will not appear in the [Columns Tool Panel](https://www.ag-grid.com/angular-data-grid/tool-panel-columns/), work when [Saving and Applying Column State](https://www.ag-grid.com/angular-data-grid/column-state/#save-and-apply), or work with [Grid State](https://www.ag-grid.com/angular-data-grid/grid-state/). To use these features, register custom functions instead.

#### Directly Applied Functions

```ts
import { Component } from "@angular/core";
import { HttpClient } from "@angular/common/http";
import { AgGridAngular } from "ag-grid-angular";
import {
  AutoGroupColumnDef,
  ClientSideRowModelModule,
  ColDef,
  ColGroupDef,
  GridApi,
  GridOptions,
  GridReadyEvent,
  IAggFuncParams,
  ModuleRegistry,
  ValueFormatterParams,
  ValueGetterParams,
  enableDevValidations,
} from "ag-grid-community";
import {
  ColumnMenuModule,
  ColumnsToolPanelModule,
  ContextMenuModule,
  FiltersToolPanelModule,
  RowGroupingModule,
  SetFilterModule,
} from "ag-grid-enterprise";
if (process.env.NODE_ENV !== "production") {
  enableDevValidations();
}

ModuleRegistry.registerModules([
  ClientSideRowModelModule,
  ColumnsToolPanelModule,
  FiltersToolPanelModule,
  ColumnMenuModule,
  ContextMenuModule,
  RowGroupingModule,
  SetFilterModule,
]);
import { IOlympicData } from "./interfaces";

@Component({
  selector: "my-app",
  standalone: true,
  imports: [AgGridAngular],
  template: `<ag-grid-angular
    style="width: 100%; height: 100%;"
    [columnDefs]="columnDefs"
    [defaultColDef]="defaultColDef"
    [autoGroupColumnDef]="autoGroupColumnDef"
    [suppressAggFuncInHeader]="true"
    [rowData]="rowData"
    (gridReady)="onGridReady($event)"
  /> `,
})
export class AppComponent {
  columnDefs: ColDef[] = [
    {
      field: "country",
      rowGroup: true,
      hide: true,
    },
    {
      headerName: "Range in Total",
      field: "total",
      aggFunc: (params) => {
        const values = params.values;
        return values.length > 0
          ? Math.max(...values) - Math.min(...values)
          : null;
      },
    },
  ];
  defaultColDef: ColDef = {
    flex: 1,
    minWidth: 150,
  };
  autoGroupColumnDef: AutoGroupColumnDef = {
    field: "athlete",
    minWidth: 220,
  };
  rowData!: IOlympicData[];

  constructor(private http: HttpClient) {}

  onGridReady(params: GridReadyEvent<IOlympicData>) {
    this.http
      .get<
        IOlympicData[]
      >("https://www.ag-grid.com/example-assets/olympic-winners.json")
      .subscribe((data) => (this.rowData = data));
  }
}
```

[Live example: Directly Applied Functions](https://www.ag-grid.com/examples/aggregation-custom-functions/applied-functions/angular)

The above example demonstrates the following configuration to apply a custom `"range"` function to the `total` column:

```ts
<ag-grid-angular
    [columnDefs]="columnDefs"
    /* other grid options ... */ />

this.columnDefs = [
    {
        field: 'total',
        aggFunc: params => {
            const values = params.values;
            return values.length > 0 ? Math.max(...values) - Math.min(...values) : null;
        }
    }
];
```

## Multiple Group Levels

When rows are grouped by more than one column, a custom aggregation runs from the lowest group level upwards.

- For the lowest-level groups, `params.values` contains the raw cell values from the leaf rows.
- For higher-level groups, `params.values` contains the aggregated results from the child groups, not the original row values.

This means a custom aggregation function must be able to combine its own previous results. For simple aggregations like `sum`, this works naturally: adding partial sums gives the same total as adding every leaf. For aggregations like `range`, it does not: combining child ranges with `Math.max(...values) - Math.min(...values)` gives the range *of the child ranges*, not the original min/max spread across the leaves.

To support multiple group levels, return an `IAggFuncResult` instead of a raw scalar. This wrapper stores the displayed aggregate value, plus any extra data the next level up needs to aggregate correctly.

```ts

interface IAggFuncResult&lt;TAggValue = number | bigint | null&gt; {
  // Returns a string representation of the aggregated value. Used also for sorting. 
  toString(): string;

  // The aggregated scalar value. 
  value?: TAggValue;

  // The count of aggregated values. Present on `avg` results. 
  count?: number;

  // Returns the numeric representation of the aggregated value. Used also for sorting. 
  toNumber?(): TAggValue;

}
```

For example, a range aggregation can return the displayed range as `value`, and also store `min` and `max`. Parent groups then combine the child `min` and `max` values to compute the correct parent range. The built-in `avg` and `count` aggregations use the same wrapper shape.

### Reading Child Values

Parent groups read each child's value using [`getDataValue`](https://www.ag-grid.com/angular-data-grid/row-object/#reference-data-getDataValue). The mode argument controls what is returned:

- `'data'` returns the stored wrapper unchanged, so the parent can read fields like `min` and `max` off it.
- `'value'` unwraps the wrapper to a scalar by calling `toNumber()` when present, otherwise reading the `value` field. The wrapper is returned unchanged if neither is available.

For child group rows, `getDataValue(column, 'data')` returns the `IAggFuncResult` produced by the child aggregation. For leaf rows, it returns the leaf cell value — usually a primitive read from the column's `field`, or a wrapper if the column has a `valueGetter` that returns one.

A custom aggregation function therefore needs to handle both cases: raw leaf values at the lowest level, and `IAggFuncResult` wrappers from child groups at higher levels.

### Range Example

The example below applies a `range` aggregation across two grouping levels (country, then year). The custom function reads each child via `getDataValue`, falling back to the stored `min`/`max` when the child is a sub-group.

#### Multiple Group Levels

```ts
import { Component } from "@angular/core";
import { HttpClient } from "@angular/common/http";
import { AgGridAngular } from "ag-grid-angular";
import {
  AutoGroupColumnDef,
  ClientSideRowModelModule,
  ColDef,
  ColGroupDef,
  GridApi,
  GridOptions,
  GridReadyEvent,
  IAggFuncParams,
  IAggFuncResult,
  IAggFuncs,
  ModuleRegistry,
  enableDevValidations,
} from "ag-grid-community";
import {
  ColumnMenuModule,
  ColumnsToolPanelModule,
  ContextMenuModule,
  RowGroupingModule,
  SetFilterModule,
} from "ag-grid-enterprise";
if (process.env.NODE_ENV !== "production") {
  enableDevValidations();
}

ModuleRegistry.registerModules([
  ClientSideRowModelModule,
  ColumnsToolPanelModule,
  ColumnMenuModule,
  ContextMenuModule,
  RowGroupingModule,
  SetFilterModule,
]);
import { IOlympicData } from "./interfaces";

@Component({
  selector: "my-app",
  standalone: true,
  imports: [AgGridAngular],
  template: `<ag-grid-angular
    style="width: 100%; height: 100%;"
    [columnDefs]="columnDefs"
    [defaultColDef]="defaultColDef"
    [autoGroupColumnDef]="autoGroupColumnDef"
    [aggFuncs]="aggFuncs"
    [rowData]="rowData"
    (gridReady)="onGridReady($event)"
  /> `,
})
export class AppComponent {
  columnDefs: ColDef[] = [
    { field: "country", rowGroup: true, hide: true },
    { field: "year", rowGroup: true, hide: true },
    { headerName: "Range", field: "total", aggFunc: "range" },
  ];
  defaultColDef: ColDef = {
    flex: 1,
    minWidth: 150,
  };
  autoGroupColumnDef: AutoGroupColumnDef = {
    field: "athlete",
    minWidth: 220,
  };
  aggFuncs: IAggFuncs = {
    range: rangeAggFunc,
  };
  rowData!: IOlympicData[];

  constructor(private http: HttpClient) {}

  onGridReady(params: GridReadyEvent<IOlympicData>) {
    this.http
      .get<
        IOlympicData[]
      >("https://www.ag-grid.com/example-assets/olympic-winners.json")
      .subscribe((data) => (this.rowData = data));
  }
}

class RangeResult implements IAggFuncResult<number> {
  constructor(
    readonly value: number,
    readonly min: number,
    readonly max: number,
  ) {}

  toNumber() {
    return this.value;
  }

  toString() {
    return this.value.toFixed(2);
  }
}

function rangeAggFunc(
  params: IAggFuncParams<IOlympicData>,
): RangeResult | null {
  // Read each immediate child via `getDataValue(col, 'data')`:
  //  - leaf children return the raw `total` number
  //  - sub-group children return the RangeResult this function produced one
  //    level down — its `min`/`max` let the parent recompute the range
  //    without re-walking descendant leaves.
  let min = Infinity;
  let max = -Infinity;
  for (const child of params.aggregatedChildren) {
    const childValue = child.getDataValue(params.column, "data");
    if (typeof childValue === "number") {
      min = Math.min(min, childValue);
      max = Math.max(max, childValue);
    } else if (childValue instanceof RangeResult) {
      min = Math.min(min, childValue.min);
      max = Math.max(max, childValue.max);
    }
  }
  return Number.isFinite(min) ? new RangeResult(max - min, min, max) : null;
}
```

[Live example: Multiple Group Levels](https://www.ag-grid.com/examples/aggregation-custom-functions/multi-group-levels/angular)

A class is a convenient way to define the wrapper: `toNumber()` and `toString()` live on the prototype, and `instanceof` makes it easy to detect a wrapper alongside a `typeof` check for primitive leaf values.

Here is the configuration used in the example above:

```ts
/** Carries `min`/`max` alongside the scalar `value` so the parent recomputes in O(N). */
class RangeResult implements IAggFuncResult<number> {
    constructor(
        readonly value: number,
        readonly min: number,
        readonly max: number,
    ) {}

    toNumber() { return this.value; }
    toString() { return this.value.toFixed(2); }
}

function rangeAggFunc(params) {
    let min = Infinity;
    let max = -Infinity;
    for (const child of params.aggregatedChildren) {
        const childValue = child.getDataValue(params.column, 'data');
        if (typeof childValue === 'number') {
            // Leaf rows expose the raw `total` number.
            min = Math.min(min, childValue);
            max = Math.max(max, childValue);
        } else if (childValue instanceof RangeResult) {
            // Sub-group rows expose the wrapper produced one level down.
            min = Math.min(min, childValue.min);
            max = Math.max(max, childValue.max);
        }
    }
    return Number.isFinite(min) ? new RangeResult(max - min, min, max) : null;
}
```

### Ratio Example

A ratio of two sums (gold to silver medals) uses the same pattern: carry both running totals on the wrapper so the parent can divide at any level. Giving leaf rows the same wrapper shape via a `valueGetter` lets the aggregation function read every child uniformly, without branching on leaf vs. group.

#### Ratio Across Levels

```ts
import { Component } from "@angular/core";
import { HttpClient } from "@angular/common/http";
import { AgGridAngular } from "ag-grid-angular";
import {
  AutoGroupColumnDef,
  ClientSideRowModelModule,
  ColDef,
  ColGroupDef,
  GridApi,
  GridOptions,
  GridReadyEvent,
  IAggFuncParams,
  IAggFuncResult,
  IAggFuncs,
  ModuleRegistry,
  ValueGetterParams,
  enableDevValidations,
} from "ag-grid-community";
import {
  ColumnMenuModule,
  ColumnsToolPanelModule,
  ContextMenuModule,
  RowGroupingModule,
  SetFilterModule,
} from "ag-grid-enterprise";
if (process.env.NODE_ENV !== "production") {
  enableDevValidations();
}

ModuleRegistry.registerModules([
  ClientSideRowModelModule,
  ColumnsToolPanelModule,
  ColumnMenuModule,
  ContextMenuModule,
  RowGroupingModule,
  SetFilterModule,
]);
import { IOlympicData } from "./interfaces";

@Component({
  selector: "my-app",
  standalone: true,
  imports: [AgGridAngular],
  template: `<ag-grid-angular
    style="width: 100%; height: 100%;"
    [columnDefs]="columnDefs"
    [aggFuncs]="aggFuncs"
    [defaultColDef]="defaultColDef"
    [autoGroupColumnDef]="autoGroupColumnDef"
    [rowData]="rowData"
    (gridReady)="onGridReady($event)"
  /> `,
})
export class AppComponent {
  columnDefs: ColDef[] = [
    { field: "country", rowGroup: true, hide: true },
    { field: "year", rowGroup: true, hide: true },
    { field: "total", aggFunc: "sum" },
    {
      headerName: "Gold to Silver",
      colId: "goldSilverRatio",
      aggFunc: "ratio",
      valueGetter: leafRatioValueGetter,
    },
  ];
  aggFuncs: IAggFuncs = {
    ratio: ratioAggFunc,
  };
  defaultColDef: ColDef = {
    flex: 1,
    minWidth: 150,
  };
  autoGroupColumnDef: AutoGroupColumnDef = {
    field: "athlete",
    minWidth: 220,
  };
  rowData!: IOlympicData[];

  constructor(private http: HttpClient) {}

  onGridReady(params: GridReadyEvent<IOlympicData>) {
    this.http
      .get<
        IOlympicData[]
      >("https://www.ag-grid.com/example-assets/olympic-winners.json")
      .subscribe((data) => (this.rowData = data));
  }
}

class RatioResult implements IAggFuncResult<number | null> {
  readonly value: number | null;

  constructor(
    readonly gold: number,
    readonly silver: number,
  ) {
    this.value = silver ? gold / silver : null;
  }

  toNumber(): number | null {
    return this.value;
  }

  toString() {
    const value = this.value;
    return value === null ? "" : value.toFixed(2);
  }
}

// Leaf rows always expose a `RatioResult` so the aggFunc reads every child uniformly.
// Rows with no silvers carry `silver: 0` — `toString` blanks the cell, while `gold`/`silver`
// stay available for the parent group's running totals.
function leafRatioValueGetter(
  params: ValueGetterParams<IOlympicData>,
): RatioResult | undefined {
  if (!params.data) {
    return undefined;
  }
  const { gold, silver } = params.data;
  return new RatioResult(gold, silver);
}
function ratioAggFunc(params: IAggFuncParams<IOlympicData>): RatioResult {
  let gold = 0;
  let silver = 0;
  for (const child of params.aggregatedChildren) {
    // Every child — leaf or sub-group — exposes a `RatioResult` here. `'data'` mode returns
    // it as-is; `'value'` would unwrap via `toNumber()` and lose the `gold`/`silver` totals.
    const ratio = child.getDataValue(params.column, "data");
    if (ratio instanceof RatioResult) {
      gold += ratio.gold;
      silver += ratio.silver;
    }
  }
  return new RatioResult(gold, silver);
}
```

[Live example: Ratio Across Levels](https://www.ag-grid.com/examples/aggregation-custom-functions/multi-level-ratio/angular)

Here is the configuration used in the example above:

```ts
class RatioResult implements IAggFuncResult<number | null> {
    readonly value: number | null;

    constructor(
        readonly gold: number,
        readonly silver: number,
    ) {
        this.value = silver ? gold / silver : null;
    }

    toNumber() { return this.value; }
    toString() { const v = this.value; return v === null ? '' : v.toFixed(2); }
}

// Every leaf returns a `RatioResult` so the aggFunc reads each child the same way. Rows with no
// silvers carry `silver: 0` — `toString` blanks the cell, while `gold`/`silver` stay available
// for the parent group's running totals. Footer/filler rows have no `data`; return undefined
// so the cell is left empty.
function leafRatioValueGetter(params) {
    if (!params.data) {
        return undefined;
    }
    const { gold, silver } = params.data;
    return new RatioResult(gold, silver);
}

function ratioAggFunc(params) {
    let gold = 0;
    let silver = 0;
    for (const child of params.aggregatedChildren) {
        const ratio = child.getDataValue(params.column, 'data');
        if (ratio instanceof RatioResult) {
            gold += ratio.gold;
            silver += ratio.silver;
        }
    }
    return new RatioResult(gold, silver);
}
```

This pattern works whenever each child can be summarised by a few numbers that the parent combines:

- **Range** — carry `min` and `max`; the parent takes the lowest `min` and highest `max`.
- **Weighted average** — carry `sum` and `count`; the parent adds them up, then divides.
- **Standard deviation** — carry `sum`, `sum-of-squares`, and `count`; the parent adds all three.
- **Ratio of sums** — carry the two running sums; the parent adds and divides.

The pattern only works when the values stored on the wrapper are enough on their own to combine into the parent summary, without revisiting the leaves. Aggregations like median or percentile do not have that property: a parent's median cannot be derived from its children's medians. For those, walk every descendant leaf directly via `params.rowNode.getAggregatedChildren(params.column, true)`.

> **Note**
>
> Custom aggregation functions can also be used with [Editing Group Rows](https://www.ag-grid.com/angular-data-grid/grouping-edit/). When a group row is edited, the built-in distribution automatically handles `sum`, `avg`, `min`, `max`, `first`, and `last`. For custom aggregation functions, define a per-aggregation distribution strategy or provide a custom `groupRowValueSetter` callback.
