Skip to content
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import {
afterEach, beforeEach, describe, expect, it, jest,
} from '@jest/globals';

import {
afterTest,
beforeTest,
createDataGrid,
flushAsync,
} from '../../../grid_core/__tests__/__mock__/helpers/utils';

describe('Summary', () => {
beforeEach(beforeTest);
afterEach(afterTest);

describe('column lookup map performance optimization (T1316562)', () => {
const SUMMARY_COUNT = 100;
const GROUP_COUNT = 4;

Comment thread
dmirgaev marked this conversation as resolved.
const dataSource = [
{
id: 1, name: 'Alice', value: 10, category: 'A', region: 'X',
},
{
id: 2, name: 'Bob', value: 20, category: 'A', region: 'Y',
},
{
id: 3, name: 'Carol', value: 30, category: 'B', region: 'X',
},
{
id: 4, name: 'Dave', value: 40, category: 'B', region: 'Y',
},
];

const groupSummaryItems = Array.from(
{ length: SUMMARY_COUNT },
(_, i) => ({
column: i % 2 === 0 ? 'value' : 'id',
summaryType: 'sum' as const,
showInGroupFooter: false,
name: `summary_${i}`,
}),
);

it('should use columnMap optimization and avoid O(n*m) columnOption calls on refresh', async () => {
const { instance } = await createDataGrid({
dataSource,
columns: [
{ dataField: 'id' },
{ dataField: 'name' },
{ dataField: 'value' },
{ dataField: 'category', groupIndex: 0 },
{ dataField: 'region', groupIndex: 1 },
],
summary: { groupItems: groupSummaryItems },
});

await flushAsync();

const columnsController = instance.getController('columns');
const spy = jest.spyOn(columnsController, 'columnOption');

instance.refresh().catch(() => {});
Comment thread
dmirgaev marked this conversation as resolved.
Comment thread
dmirgaev marked this conversation as resolved.
await flushAsync();

const worstCaseMinCalls = SUMMARY_COUNT * GROUP_COUNT;

expect(spy.mock.calls.length).toBeLessThan(worstCaseMinCalls);

spy.mockRestore();
});
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
import {
describe, expect, it,
} from '@jest/globals';
import type { Column } from '@ts/grids/grid_core/columns_controller/types';

import { getColumnFromMap, getSummaryCellIndex } from '../utils';

const makeColumn = (overrides: Partial<Column> = {}): Column => ({
...overrides,
});

describe('getSummaryCellIndex', () => {
describe('when isGroupRow is false (default)', () => {
it('should return column.index', () => {
const column = makeColumn({ index: 5 });

expect(getSummaryCellIndex(column)).toBe(5);
});

it('should return -1 when column.index is undefined', () => {
const column = makeColumn();

expect(getSummaryCellIndex(column)).toBe(-1);
});

it('should ignore prevColumn when isGroupRow is false', () => {
const column = makeColumn({ index: 3 });
const prevColumn = makeColumn({ index: 10, type: 'groupExpand' });

expect(getSummaryCellIndex(column, prevColumn, false)).toBe(3);
});
});

describe('when isGroupRow is true', () => {
describe('groupExpand handling', () => {
it('should return prevColumn.index when prevColumn.type is groupExpand', () => {
const column = makeColumn({ index: 5 });
const prevColumn = makeColumn({ index: 2, type: 'groupExpand' });

expect(getSummaryCellIndex(column, prevColumn, true)).toBe(2);
});

it('should return prevColumn.index when column.type is groupExpand', () => {
const column = makeColumn({ index: 5, type: 'groupExpand' });
const prevColumn = makeColumn({ index: 7 });

expect(getSummaryCellIndex(column, prevColumn, true)).toBe(7);
});

it('should return -1 when column.type is groupExpand and prevColumn is undefined', () => {
const column = makeColumn({ index: 5, type: 'groupExpand' });

expect(getSummaryCellIndex(column, undefined, true)).toBe(-1);
});

it('should return -1 when prevColumn.type is groupExpand and prevColumn.index is undefined', () => {
const column = makeColumn({ index: 5 });
const prevColumn = makeColumn({ type: 'groupExpand' });

expect(getSummaryCellIndex(column, prevColumn, true)).toBe(-1);
});
});

describe('groupIndex handling', () => {
it('should return column.index when groupIndex is not defined', () => {
const column = makeColumn({ index: 4 });

expect(getSummaryCellIndex(column, undefined, true)).toBe(4);
});

it('should return -1 when groupIndex is defined', () => {
const column = makeColumn({ index: 4, groupIndex: 0 });

expect(getSummaryCellIndex(column, undefined, true)).toBe(-1);
});

it('should return -1 when groupIndex is 0 (falsy but defined)', () => {
const column = makeColumn({ index: 8, groupIndex: 0 });

expect(getSummaryCellIndex(column, undefined, true)).toBe(-1);
});

it('should return column.index when groupIndex is undefined and prevColumn has no groupExpand type', () => {
const column = makeColumn({ index: 6 });
const prevColumn = makeColumn({ index: 3 });

expect(getSummaryCellIndex(column, prevColumn, true)).toBe(6);
});
});
});
});

describe('getColumnFromMap', () => {
const colA = makeColumn({ index: 0, dataField: 'fieldA' });
const colB = makeColumn({ index: 1, dataField: 'fieldB' });
const getColumnMap = (): Map<string | number, Column> => (
new Map<string | number, Column>([
[0, colA],
['fieldA', colA],
[1, colB],
['fieldB', colB],
])
);

it('should return column by numeric index', () => {
const columnMap = getColumnMap();

expect(getColumnFromMap(0, columnMap)).toBe(colA);
expect(getColumnFromMap(1, columnMap)).toBe(colB);
});

it('should return column by string dataField', () => {
const columnMap = getColumnMap();

expect(getColumnFromMap('fieldA', columnMap)).toBe(colA);
expect(getColumnFromMap('fieldB', columnMap)).toBe(colB);
});

it('should return undefined for undefined identifier', () => {
const columnMap = getColumnMap();

expect(getColumnFromMap(undefined, columnMap)).toBeUndefined();
});

it('should return undefined for identifier not in the map', () => {
const columnMap = getColumnMap();

expect(getColumnFromMap(999, columnMap)).toBeUndefined();
expect(getColumnFromMap('nonExistent', columnMap)).toBeUndefined();
});

it('should work with an empty map', () => {
const emptyMap = new Map<string | number, Column>();

expect(getColumnFromMap(0, emptyMap)).toBeUndefined();
expect(getColumnFromMap('fieldA', emptyMap)).toBeUndefined();
expect(getColumnFromMap(undefined, emptyMap)).toBeUndefined();
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,8 @@ import type { RowsView } from '../../grid_core/views/m_rows_view';
import AggregateCalculator from '../m_aggregate_calculator';
import gridCore from '../m_core';
import dataSourceAdapterProvider from '../m_data_source_adapter';
import { getSummaryCellIndex } from './utils';
import type { ColumnMap, SummaryItem } from './types';
import { getColumnFromMap, getSummaryCellIndex } from './utils';

const DATAGRID_TOTAL_FOOTER_CLASS = 'dx-datagrid-total-footer';
const DATAGRID_SUMMARY_ITEM_CLASS = 'dx-datagrid-summary-item';
Expand Down Expand Up @@ -471,9 +472,9 @@ const data = (Base: ModuleType<DataController>) => class SummaryDataControllerEx
private _processGroupItem(groupItem, options) {
const that = this;

if (!options.summaryGroupItems) {
options.summaryGroupItems = that.option('summary.groupItems') || [];
}
options.summaryGroupItems ??= that.option('summary.groupItems') || [];
Comment thread
dmirgaev marked this conversation as resolved.
Outdated
options.summaryColumnMap ??= this._buildColumnLookupMap();

if (groupItem.rowType === 'group') {
let groupColumnIndex = -1;
let afterGroupColumnIndex = -1;
Expand All @@ -490,32 +491,96 @@ const data = (Base: ModuleType<DataController>) => class SummaryDataControllerEx
}
});

groupItem.summaryCells = this._calculateSummaryCells(options.summaryGroupItems, getGroupAggregates(groupItem.data), options.visibleColumns, (summaryItem, column) => {
if (summaryItem.showInGroupFooter) {
return -1;
}
groupItem.summaryCells = this._calculateSummaryCells(
options.summaryGroupItems,
getGroupAggregates(groupItem.data),
options.visibleColumns,
(summaryItem, column) => {
if (summaryItem.showInGroupFooter) {
return -1;
}

if (summaryItem.alignByColumn && column && !isDefined(column.groupIndex) && (column.index !== afterGroupColumnIndex)) {
return column.index;
}
return groupColumnIndex;
}, true);
if (summaryItem.alignByColumn
&& column
&& !isDefined(column.groupIndex)
&& (column.index !== afterGroupColumnIndex)
) {
return column.index;
}

return groupColumnIndex;
},
true,
options.summaryColumnMap,
);
}

if (groupItem.rowType === DATAGRID_GROUP_FOOTER_ROW_TYPE) {
groupItem.summaryCells = this._calculateSummaryCells(options.summaryGroupItems, getGroupAggregates(groupItem.data), options.visibleColumns, (summaryItem, column) => (summaryItem.showInGroupFooter && that._isDataColumn(column) ? column.index : -1));
groupItem.summaryCells = this._calculateSummaryCells(
options.summaryGroupItems,
getGroupAggregates(groupItem.data),
options.visibleColumns,
(summaryItem, column) => (
summaryItem.showInGroupFooter && that._isDataColumn(column) ? column.index : -1
),
false,
options.summaryColumnMap,
);
}

return groupItem;
}

private _calculateSummaryCells(summaryItems, aggregates, visibleColumns, calculateTargetColumnIndex, isGroupRow?) {
const that = this;
// The map is built once per _processItems cycle (via options) and discarded after.
private _buildColumnLookupMap(): ColumnMap {
const columnMap: ColumnMap = new Map();
const allColumns = [
...this._columnsController.getColumns(),
...(this._columnsController._commandColumns ?? []),
Comment thread
dmirgaev marked this conversation as resolved.
Outdated
];

for (const column of allColumns) {
const copiedColumn = { ...column };
// The method registers each column under a few keys: index, name, dataField, and caption.
// This is because the developer can specify summaryItem.column (and summaryItem.showInColumn)
// in any of these forms — number for column index and string for all the rest.
const keys = [
column.index,
Comment thread
dmirgaev marked this conversation as resolved.
column.name,
column.dataField,
column.caption,
].filter((key) => (
key !== undefined && !columnMap.has(key)
));
Comment thread
dmirgaev marked this conversation as resolved.

Comment thread
dmirgaev marked this conversation as resolved.
for (const key of keys) {
columnMap.set(key, copiedColumn);
}
}

return columnMap;
}

private _calculateSummaryCells(
summaryItems: SummaryItem[],
Comment thread
dmirgaev marked this conversation as resolved.
Outdated
aggregates,
visibleColumns,
calculateTargetColumnIndex,
isGroupRow?,
columnMap?: ColumnMap,
) {
const summaryCells: any = [];
Comment thread
dmirgaev marked this conversation as resolved.
Outdated
const summaryCellsByColumns = {};
Comment thread
dmirgaev marked this conversation as resolved.
Outdated

Comment thread
dmirgaev marked this conversation as resolved.
each(summaryItems, (summaryIndex, summaryItem) => {
const column = that._columnsController.columnOption(summaryItem.column);
const showInColumn = summaryItem.showInColumn && that._columnsController.columnOption(summaryItem.showInColumn) || column;
const column = columnMap
? getColumnFromMap(summaryItem.column, columnMap)
: this._columnsController.columnOption(summaryItem.column);
const showInColumn = (summaryItem.showInColumn
&& (columnMap
? getColumnFromMap(summaryItem.showInColumn, columnMap)
: this._columnsController.columnOption(summaryItem.showInColumn)))
|| column;
Comment thread
dmirgaev marked this conversation as resolved.
Outdated
const columnIndex = calculateTargetColumnIndex(summaryItem, showInColumn);

if (columnIndex >= 0) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import type { Column } from '@ts/grids/grid_core/columns_controller/types';

export interface SummaryItem {
/** Number as a column index or string as a column name, dataField, or caption. */
column?: string | number | undefined;
/** Number as a column index or string as a column name, dataField, or caption. */
showInColumn?: string | number | undefined;
showInGroupFooter?: boolean;
alignByColumn?: boolean;
summaryType?: string | undefined;
valueFormat?: unknown;
name?: string | undefined;
skipEmptyValues?: boolean;
alignment?: string | undefined;
cssClass?: string | undefined;
customizeText?: ((itemInfo: { value?: string | number | Date; valueText: string }) => string);
displayFormat?: string | undefined;
}

export type ColumnMap = Map<string | number, Column>;
Original file line number Diff line number Diff line change
Expand Up @@ -18,3 +18,10 @@ export function getSummaryCellIndex(

return !isDefined(column.groupIndex) ? cellIndex : -1;
}

export function getColumnFromMap(
identifier: string | number | undefined,
columnMap: Map<string | number, Column>,
): Column | undefined {
return identifier !== undefined ? columnMap.get(identifier) : undefined;
}
Loading