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 @@ -8,10 +8,25 @@
flex-direction: column;
gap: var(--ag-grid-size);
min-width: 260px;
flex: 1 1 auto;
min-height: 0;
}

.ag-calculated-column-expression-wrap {
position: relative;
display: flex;
flex: 1 1 auto;
.ag-text-area {
flex: 1 1 auto;
}

.ag-text-area-input-wrapper {
height: 100%;
}

.ag-text-area-input {
height: 100%;
resize: none;
}
}

.ag-calculated-column-expression-error {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
import { expect, test } from '@utils/grid/test-utils';

test.agExample(import.meta, () => {
test.eachFramework('calculated columns evaluate from aggregated group values', async ({ agIdFor }) => {
test.eachFramework('calculated columns are blank on group rows and evaluate on leaf rows', async ({ agIdFor }) => {
const solarGroupId = 'row-group-productType-Solar';

await expect(agIdFor.autoGroupCell(solarGroupId)).toContainText('Solar (2)', { useInnerText: true });
await expect(agIdFor.cell(solarGroupId, 'revenue')).toContainText('$220,000');
await expect(agIdFor.cell(solarGroupId, 'cost')).toContainText('$148,000');
await expect(agIdFor.cell(solarGroupId, 'profit')).toContainText('$72,000');
await expect(agIdFor.cell(solarGroupId, 'margin')).toContainText('33%');
// Calculated columns show no value on group rows.
await expect(agIdFor.cell(solarGroupId, 'profit')).toHaveText('');
await expect(agIdFor.cell(solarGroupId, 'margin')).toHaveText('');

await expect(agIdFor.cell('0', 'profit')).toContainText('$46,000');
await expect(agIdFor.cell('1', 'profit')).toContainText('$26,000');
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,29 +37,14 @@ Calculated columns are always read-only. They cannot be edited through cell edit

## Row Groups and Tree Data

Calculated columns are full columns, so [Row Grouping](./grouping/), [Tree Data](./tree-data/), [Aggregation](./aggregation/), sorting and filtering apply to them the same way they apply to any other column.
Calculated columns are full columns, so [Row Grouping](./grouping/), [Tree Data](./tree-data/), [Aggregation](./aggregation/), sorting and filtering apply to them the same way as any other column.

On a group row, the expression evaluates against the **aggregated** values of the columns it references. With the setup below, the `profit` cell on a region group shows `sum(revenue) - sum(cost)` for that region:
Calculated values appear on leaf rows only. Row group rows, Tree Data parents (including parents that carry their own data), group footers and the grand-total row show no calculated value.

```{% frameworkTransform=true %}
const gridOptions = {
columnDefs: [
{ field: 'region', rowGroup: true, hide: true },
{ field: 'revenue', aggFunc: 'sum' },
{ field: 'cost', aggFunc: 'sum' },
{ colId: 'profit', calculatedExpression: '[revenue] - [cost]', cellDataType: 'number' },
],
};
```

Because the inputs are aggregated before the expression runs, `[revenue] / [cost]` on a group is `sum(revenue) / sum(cost)`, not the average of each leaf row's ratio. Choose expressions whose group-level result is meaningful under this rule.
This avoids a misleading result. Evaluating the expression against aggregated inputs does not match the aggregate of the per-row results for non-linear expressions: `[revenue] / [cost]` on a group would be `sum(revenue) / sum(cost)`, not the average of each leaf row's ratio.

{% gridExampleRunner title="Row Groups with Calculated Columns" name="calculated-columns-row-groups" exampleHeight=420 /%}

A group cell stays blank unless every referenced column produces an aggregated value. Reference a column with no `aggFunc`, and group rows for that calculated column are blank while the leaf rows still evaluate. Filler group rows in Tree Data, which have no aggregates of their own, stay blank for the same reason.

Group and grand-total footer rows show the same calculated value as the group they total. Tree Data parent rows evaluate against their aggregated children in the same way as Row Group rows.

## References with Column Groups

When the Column Menu is registered, users can add a calculated column from the header menu using **Add Calculated Column**. Opening the header menu on a calculated column shows **Edit Calculated Column** for changing its title, type and expression, and **Remove Calculated Column** for removing it. Right-clicking cells in a calculated column also shows **Remove Calculated Column**.
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import type { AgEvent } from 'ag-stack';

import type { GridApi } from '../api/gridApi';
import { _setColGroupOpen } from '../columns/columnGroups/columnGroupState';
import { _applyColumnState } from '../columns/columnStateUtils';
import type { NamedBean } from '../context/bean';
import { BeanStub } from '../context/beanStub';
Expand Down Expand Up @@ -169,36 +170,34 @@ export class AlignedGridsService extends BeanStub implements NamedBean {
}

private processGroupOpenedEvent(groupOpenedEvent: ColumnGroupOpenedEvent): void {
const { colGroupSvc } = this.beans;
if (!colGroupSvc) {
return;
}
const beans = this.beans;
const colsGroupsById = beans.colModel.colsGroupsById;
for (const masterGroup of groupOpenedEvent.columnGroups) {
// likewise for column group
let otherColumnGroup: AgProvidedColumnGroup | null = null;
let otherColumnGroup: AgProvidedColumnGroup | undefined;

if (masterGroup) {
otherColumnGroup = colGroupSvc.getProvidedColGroup(masterGroup.getGroupId());
otherColumnGroup = colsGroupsById.get(masterGroup.getGroupId());
}

if (masterGroup && !otherColumnGroup) {
continue;
}

colGroupSvc.setColumnGroupOpened(otherColumnGroup, masterGroup.isExpanded(), 'alignedGridChanged');
_setColGroupOpen(beans, otherColumnGroup, masterGroup.isExpanded(), 'alignedGridChanged');
}
}

private processColumnEvent(colEvent: ColumnEvent): void {
// the column in the event is from the master grid. need to
// look up the equivalent from this (other) grid
const masterColumn = colEvent.column;
let otherColumn: AgColumn | null = null;
let otherColumn: AgColumn | undefined;

const beans = this.beans;
const { colResize, colModel, scrollVisibleSvc } = beans;
if (masterColumn) {
otherColumn = colModel.getColDefCol(masterColumn.getColId());
otherColumn = colModel.getNonPivotCol(masterColumn.getColId());
}
// if event was with respect to a master column, that is not present in this
// grid, then we ignore the event
Expand Down
15 changes: 10 additions & 5 deletions packages/ag-grid-community/src/api/gridApi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,15 @@ import type {
RowDropPositionIndicator,
SetRowDropPositionIndicatorParams,
} from '../dragAndDrop/rowDropHighlightService';
import type { ColDef, ColGroupDef, ColKey, ColumnChooserParams, HeaderLocation, IAggFunc } from '../entities/colDef';
import type {
ColAggFunc,
ColDef,
ColGroupDef,
ColKey,
ColumnChooserParams,
HeaderLocation,
IAggFunc,
} from '../entities/colDef';
import type { ChartRef, GridOptions, SelectAllMode } from '../entities/gridOptions';
import type { AgPublicEventType } from '../eventTypes';
import type {
Expand Down Expand Up @@ -1542,10 +1550,7 @@ export interface _AggregationGridApi<TData> {
* Sets the agg function for a column. `aggFunc` can be one of the built-in aggregations or a custom aggregation by name or direct function.
* @agModule `RowGroupingModule / PivotModule / TreeDataModule`
*/
setColumnAggFunc<TValue = any>(
key: ColKey<TData, TValue>,
aggFunc: string | IAggFunc<TData, TValue> | null | undefined
): void;
setColumnAggFunc<TValue = any>(key: ColKey<TData, TValue>, aggFunc: ColAggFunc<TData, TValue>): void;
}

/** @internal AG_GRID_INTERNAL - Not for public use. Can change / be removed at any time. */
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import type { NamedBean } from '../context/bean';
import { BeanStub } from '../context/beanStub';
import type { ColDef, ColGroupDef } from '../entities/colDef';
import type { AutoGenerateColumnDefsOptions } from '../entities/gridOptions';
import { _isPlainObject } from '../utils/mergeDeep';

export class AutoGenerateColumnsService extends BeanStub implements NamedBean {
beanName = 'autoGenColsSvc' as const;
Expand Down Expand Up @@ -118,11 +119,3 @@ function _isPrimitiveArray(arr: unknown[]): boolean {
const first = arr[0];
return first != null && !Array.isArray(first) && !_isPlainObject(first) && typeof first !== 'function';
}

function _isPlainObject(value: unknown): value is Record<string, unknown> {
if (value === null || typeof value !== 'object' || Array.isArray(value) || value instanceof Date) {
return false;
}
const proto = Object.getPrototypeOf(value);
return proto === Object.prototype || proto === null;
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { _getInnerWidth, _removeFromArray } from 'ag-stack';

import { dispatchColumnResizedEvent } from '../columns/columnEventUtils';
import { _columnsMatch, getWidthOfColsInList, isRowNumberCol, isSpecialCol } from '../columns/columnUtils';
import { getWidthOfColsInList, isSpecialCol } from '../columns/columnUtils';
import type { NamedBean } from '../context/bean';
import { BeanStub } from '../context/beanStub';
import type { BeanCollection } from '../context/context';
Expand Down Expand Up @@ -65,7 +65,7 @@ export class ColumnAutosizeService extends BeanStub implements NamedBean {
}

public autoSizeCols(params: AutoSizeColumnParams): void {
const { eventSvc, visibleCols, colModel } = this.beans;
const { eventSvc, colModel } = this.beans;

setWidthAnimation(this.beans, true);

Expand All @@ -80,13 +80,14 @@ export class ColumnAutosizeService extends BeanStub implements NamedBean {

const availableGridWidth = getAvailableWidth(this.beans);

const isLeftCol = (col: ColKey) => visibleCols.leftCols.some((leftCol) => _columnsMatch(leftCol, col));
const isRightCol = (col: ColKey) => visibleCols.rightCols.some((rightCol) => _columnsMatch(rightCol, col));

// We exclude all pinned columns here, we only want columns in the main viewport to be scaled up
// We exclude pinned columns here, we only want columns in the main viewport to be scaled up.
const colKeys = params.colKeys.filter((col) => {
const allowAutoSize = !colModel.getCol(col)?.colDef.suppressAutoSize;
return allowAutoSize && !isRowNumberCol(col) && !isLeftCol(col) && !isRightCol(col);
const resolved = colModel.getCol(col);
if (!resolved || resolved.colDef.suppressAutoSize || resolved.colKind === 'row-number') {
return false;
}
const pinned = resolved.pinned;
return !(resolved.displayed && (pinned === 'left' || pinned === 'right'));
});

this.sizeColumnsToFit(availableGridWidth, params.source, true, {
Expand Down Expand Up @@ -170,7 +171,7 @@ export class ColumnAutosizeService extends BeanStub implements NamedBean {
const updatedColumns: AgColumn[] = [];

for (const key of colKeys) {
if (!key || isSpecialCol(key)) {
if (!key) {
continue;
}
const column = colModel.getCol(key);
Expand All @@ -179,6 +180,9 @@ export class ColumnAutosizeService extends BeanStub implements NamedBean {
if (!column || columnsAutoSized.has(column) || column.colDef.suppressAutoSize) {
continue;
}
if (isSpecialCol(column)) {
continue;
}

// get how wide this col should be
const preferredWidth = autoWidthCalc!.getPreferredWidthForColumn(column, shouldSkipHeader);
Expand All @@ -198,7 +202,9 @@ export class ColumnAutosizeService extends BeanStub implements NamedBean {
}

if (updatedColumns.length) {
visibleCols.refresh(source);
// skipTreeBuild=true: autosize only changes widths, leaving liveCols/pins/visibility — and
// thus the section/group trees — unchanged.
visibleCols.refresh(source, true);
}
}

Expand All @@ -217,12 +223,12 @@ export class ColumnAutosizeService extends BeanStub implements NamedBean {
private autoSizeColumnGroupsByColumns(keys: ColKey[], source: ColumnEventType, stopAtGroup?: AgColumnGroup): void {
const { colModel, ctrlsSvc } = this.beans;
const columnGroups = new Set<AgColumnGroup>();
const columns = colModel.getColsForKeys(keys);

for (const col of columns) {
let parent = col.parent;
for (let i = 0, len = keys.length; i < len; ++i) {
const col = colModel.getCol(keys[i]);
let parent = col?.parent;
while (parent && parent != stopAtGroup) {
if (!parent.isPadding()) {
if (!parent.providedColumnGroup.padding) {
columnGroups.add(parent);
}
parent = parent.parent;
Expand Down Expand Up @@ -363,7 +369,7 @@ export class ColumnAutosizeService extends BeanStub implements NamedBean {
setWidthAnimation(beans, true);
}

const limitsMap: { [colId: string]: Omit<IColumnLimit, 'key'> } = {};
const limitsMap: { [colId: string]: Omit<IColumnLimit, 'key'> } = Object.create(null);
for (const { key, ...dimensions } of params?.columnLimits ?? []) {
limitsMap[typeof key === 'string' ? key : key.getColId()] = dimensions;
}
Expand Down Expand Up @@ -403,7 +409,7 @@ export class ColumnAutosizeService extends BeanStub implements NamedBean {
const colsToNotSpread: AgColumn[] = [];

for (const column of allDisplayedColumns) {
const isIncluded = params?.colKeys?.some((key) => _columnsMatch(column, key)) ?? true;
const isIncluded = params?.colKeys?.some((key) => columnsMatch(column, key)) ?? true;
if (column.colDef.suppressSizeToFit || !isIncluded) {
colsToNotSpread.push(column);
} else {
Expand All @@ -420,7 +426,7 @@ export class ColumnAutosizeService extends BeanStub implements NamedBean {
colsToNotSpread.push(column);
};

const currentWidths: Partial<Record<string, number>> = {};
const currentWidths: Partial<Record<string, number>> = Object.create(null);

// resetting cols to their original width makes the sizeColumnsToFit more deterministic,
// rather than depending on the current size of the columns. most users call sizeColumnsToFit
Expand Down Expand Up @@ -501,9 +507,8 @@ export class ColumnAutosizeService extends BeanStub implements NamedBean {
col.fireColumnWidthChangedEvent(source);
}

const visibleCols = beans.visibleCols;
visibleCols.setLeftValues(source);
visibleCols.updateBodyWidths();
const visibleCols = this.beans.visibleCols;
visibleCols.updateBodyWidths(visibleCols.setLeftValues(source));

if (silent) {
return;
Expand Down Expand Up @@ -627,3 +632,7 @@ function setWidthAnimation({ ctrlsSvc, gos }: BeanCollection, enable: boolean):
classList.remove(WIDTH_ANIMATION_CLASS);
}
}

function columnsMatch(column: AgColumn, key: ColKey): boolean {
return column === key || column.colId == key || column.colDef === key;
}
Original file line number Diff line number Diff line change
Expand Up @@ -58,8 +58,7 @@ export class BodyDropPivotTarget extends BeanStub implements DropListener {
}

/** Callback for when drag leaves */
// eslint-disable-next-line @typescript-eslint/no-unused-vars
public onDragLeave(draggingEvent: GridDraggingEvent): void {
public onDragLeave(_draggingEvent: GridDraggingEvent): void {
// if we are taking columns out of the center, then we remove them from the report
this.clearColumnsList();
}
Expand All @@ -71,21 +70,24 @@ export class BodyDropPivotTarget extends BeanStub implements DropListener {
}

/** Callback for when dragging */
// eslint-disable-next-line @typescript-eslint/no-unused-vars
public onDragging(draggingEvent: GridDraggingEvent): void {}
public onDragging(_draggingEvent: GridDraggingEvent): void {}

/** Callback for when drag stops */
// eslint-disable-next-line @typescript-eslint/no-unused-vars
public onDragStop(draggingEvent: GridDraggingEvent): void {
const { valueColsSvc, rowGroupColsSvc, pivotColsSvc } = this.beans;
if (this.columnsToAggregate.length > 0) {
valueColsSvc?.addColumns(this.columnsToAggregate, 'toolPanelDragAndDrop');
}
if (this.columnsToGroup.length > 0) {
rowGroupColsSvc?.addColumns(this.columnsToGroup, 'toolPanelDragAndDrop');
}
if (this.columnsToPivot.length > 0) {
pivotColsSvc?.addColumns(this.columnsToPivot, 'toolPanelDragAndDrop');
public onDragStop(_draggingEvent: GridDraggingEvent): void {
const { colModel, valueColsSvc, rowGroupColsSvc, pivotColsSvc } = this.beans;
colModel.beginColBatch();
try {
if (this.columnsToAggregate.length > 0) {
valueColsSvc?.addColumns(this.columnsToAggregate, 'toolPanelDragAndDrop');
}
if (this.columnsToGroup.length > 0) {
rowGroupColsSvc?.addColumns(this.columnsToGroup, 'toolPanelDragAndDrop');
}
if (this.columnsToPivot.length > 0) {
pivotColsSvc?.addColumns(this.columnsToPivot, 'toolPanelDragAndDrop');
}
} finally {
colModel.endColBatch('toolPanelDragAndDrop');
}
}

Expand Down
Loading
Loading