Skip to content

Commit 242df27

Browse files
Improve Performance in Row Grouping (#924)
* improve perf when getting rowHeight with grouping enabled * restore story values
1 parent 5761de6 commit 242df27

File tree

3 files changed

+156
-19
lines changed

3 files changed

+156
-19
lines changed

packages/core/src/data-editor/row-grouping.ts

Lines changed: 21 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,10 @@ import type { DataEditorProps } from "./data-editor.js";
44
import type { DataGridProps } from "../internal/data-grid/data-grid.js";
55
import { whenDefined } from "../common/utils.js";
66

7+
type Mutable<T> = {
8+
-readonly [K in keyof T]: T[K];
9+
};
10+
711
export type RowGroup = {
812
/**
913
* The index of the header if the groups are all flattened and expanded
@@ -100,6 +104,7 @@ export function expandRowGroups(groups: readonly RowGroup[]): ExpandedRowGroup[]
100104
}
101105

102106
export interface FlattenedRowGroup {
107+
readonly rowIndex: number;
103108
readonly headerIndex: number;
104109
readonly contentIndex: number; // the content index of the first row in the group
105110
readonly isCollapsed: boolean;
@@ -128,6 +133,7 @@ export function flattenRowGroups(rowGrouping: RowGroupingOptions, rows: number):
128133
rowsInGroup--; // the header isn't in the group
129134

130135
flattened.push({
136+
rowIndex: -1, // we will fill this in later
131137
headerIndex: group.headerIndex,
132138
contentIndex: -1, // we will fill this in later
133139
skip: skipChildren,
@@ -153,10 +159,14 @@ export function flattenRowGroups(rowGrouping: RowGroupingOptions, rows: number):
153159
processGroup(expandedGroups[i], nextHeaderIndex);
154160
}
155161

162+
let rowIndex = 0;
156163
let contentIndex = 0;
157-
for (const g of flattened) {
158-
(g as any).contentIndex = contentIndex;
164+
for (const g of flattened as Mutable<(typeof flattened)[number]>[]) {
165+
g.contentIndex = contentIndex;
159166
contentIndex += g.rows;
167+
168+
g.rowIndex = rowIndex;
169+
rowIndex += g.isCollapsed ? 1 : g.rows + 1;
160170
}
161171

162172
return flattened
@@ -244,6 +254,13 @@ export function useRowGroupingInner(
244254
[options, rows]
245255
);
246256

257+
const flattenedRowGroupsMap = React.useMemo(() => {
258+
return flattenedRowGroups?.reduce<{ [rowIndex: number]: FlattenedRowGroup | undefined }>((acc, group) => {
259+
acc[group.rowIndex] = group;
260+
return acc;
261+
}, {});
262+
}, [flattenedRowGroups]);
263+
247264
const effectiveRows = React.useMemo(() => {
248265
if (flattenedRowGroups === undefined) return rows;
249266
return flattenedRowGroups.reduce((acc, group) => acc + (group.isCollapsed ? 1 : group.rows + 1), 0);
@@ -253,11 +270,10 @@ export function useRowGroupingInner(
253270
if (options === undefined) return rowHeightIn;
254271
if (typeof rowHeightIn === "number" && options.height === rowHeightIn) return rowHeightIn;
255272
return (rowIndex: number) => {
256-
const { isGroupHeader } = mapRowIndexToPath(rowIndex, flattenedRowGroups);
257-
if (isGroupHeader) return options.height;
273+
if (flattenedRowGroupsMap?.[rowIndex]) return options.height;
258274
return typeof rowHeightIn === "number" ? rowHeightIn : rowHeightIn(rowIndex);
259275
};
260-
}, [flattenedRowGroups, options, rowHeightIn]);
276+
}, [flattenedRowGroupsMap, options, rowHeightIn]);
261277

262278
const rowNumberMapperOut = React.useCallback(
263279
(row: number): number | undefined => {

packages/core/src/docs/examples/row-grouping.stories.tsx

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -35,14 +35,10 @@ export default {
3535

3636
export const RowGrouping: React.VFC<any> = (p: { freezeColumns: number }) => {
3737
const { cols, getCellContent } = useMockDataGenerator(100);
38-
const rows = 1000;
38+
const rows = 100_000;
3939

4040
const [rowGrouping, setRowGrouping] = React.useState<RowGroupingOptions>(() => ({
4141
groups: [
42-
{
43-
headerIndex: 0,
44-
isCollapsed: false,
45-
},
4642
{
4743
headerIndex: 10,
4844
isCollapsed: true,
@@ -61,6 +57,12 @@ export const RowGrouping: React.VFC<any> = (p: { freezeColumns: number }) => {
6157
headerIndex: 30,
6258
isCollapsed: false,
6359
},
60+
...Array.from({ length: 100 }, (_value, i): RowGroupingOptions["groups"][number] => {
61+
return {
62+
headerIndex: (rows / 100) * i,
63+
isCollapsed: false,
64+
};
65+
}),
6466
],
6567
height: 55,
6668
navigationBehavior: "block",

packages/core/test/row-grouping.test.ts

Lines changed: 128 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -131,8 +131,8 @@ describe("flattenRowGroups", () => {
131131
};
132132
const totalRows = 10;
133133
const expected = [
134-
{ headerIndex: 0, isCollapsed: false, depth: 0, path: [0], rows: 4, contentIndex: 0 },
135-
{ headerIndex: 5, isCollapsed: true, depth: 0, path: [1], rows: 4, contentIndex: 4 },
134+
{ rowIndex: 0, headerIndex: 0, isCollapsed: false, depth: 0, path: [0], rows: 4, contentIndex: 0 },
135+
{ rowIndex: 5, headerIndex: 5, isCollapsed: true, depth: 0, path: [1], rows: 4, contentIndex: 4 },
136136
];
137137
const output = flattenRowGroups(rowGroupingOptions, totalRows);
138138
expect(output).toEqual(expected);
@@ -158,9 +158,9 @@ describe("flattenRowGroups", () => {
158158
};
159159
const totalRows = 10;
160160
const expected = [
161-
{ headerIndex: 0, isCollapsed: false, depth: 0, path: [0], rows: 1, contentIndex: 0 },
162-
{ headerIndex: 2, isCollapsed: false, depth: 1, path: [0, 0], rows: 1, contentIndex: 1 },
163-
{ headerIndex: 4, isCollapsed: true, depth: 1, path: [0, 1], rows: 1, contentIndex: 2 },
161+
{ rowIndex: 0, headerIndex: 0, isCollapsed: false, depth: 0, path: [0], rows: 1, contentIndex: 0 },
162+
{ rowIndex: 2, headerIndex: 2, isCollapsed: false, depth: 1, path: [0, 0], rows: 1, contentIndex: 1 },
163+
{ rowIndex: 4, headerIndex: 4, isCollapsed: true, depth: 1, path: [0, 1], rows: 1, contentIndex: 2 },
164164
];
165165
const output = flattenRowGroups(rowGroupingOptions, totalRows);
166166
expect(output).toEqual(expected);
@@ -183,10 +183,10 @@ describe("flattenRowGroups", () => {
183183
};
184184
const totalRows = 7;
185185
const expected = [
186-
{ headerIndex: 0, isCollapsed: false, depth: 0, path: [0], rows: 0, contentIndex: 0 },
187-
{ headerIndex: 1, isCollapsed: true, depth: 1, path: [0, 0], rows: 1, contentIndex: 0 },
188-
{ headerIndex: 3, isCollapsed: false, depth: 1, path: [0, 1], rows: 1, contentIndex: 1 },
189-
{ headerIndex: 5, isCollapsed: true, depth: 0, path: [1], rows: 1, contentIndex: 2 },
186+
{ rowIndex: 0, headerIndex: 0, isCollapsed: false, depth: 0, path: [0], rows: 0, contentIndex: 0 },
187+
{ rowIndex: 1, headerIndex: 1, isCollapsed: true, depth: 1, path: [0, 0], rows: 1, contentIndex: 0 },
188+
{ rowIndex: 2, headerIndex: 3, isCollapsed: false, depth: 1, path: [0, 1], rows: 1, contentIndex: 1 },
189+
{ rowIndex: 4, headerIndex: 5, isCollapsed: true, depth: 0, path: [1], rows: 1, contentIndex: 2 },
190190
];
191191
const output = flattenRowGroups(rowGroupingOptions, totalRows);
192192
expect(output).toEqual(expected);
@@ -233,6 +233,125 @@ describe("useRowGroupingInner - getRowThemeOverride", () => {
233233
const themeOverride = result.current.getRowThemeOverride?.(1);
234234
expect(themeOverride).toEqual({ bgCell: "green" });
235235
});
236+
237+
it("returns correct theme for non-group-header rows when some groups collapsed according to getRowThemeOverrideIn", () => {
238+
const rowGroupingOptions = {
239+
groups: [
240+
{ headerIndex: 0, isCollapsed: false },
241+
{ headerIndex: 3, isCollapsed: true },
242+
{ headerIndex: 5, isCollapsed: false },
243+
],
244+
height: 30,
245+
};
246+
247+
// eslint-disable-next-line unicorn/consistent-function-scoping
248+
const getRowThemeOverrideIn = (row: number) => ({ bgCell: row % 2 === 0 ? "blue" : "green" });
249+
250+
const { result } = renderHook(() => useRowGroupingInner(rowGroupingOptions, 10, 20, getRowThemeOverrideIn));
251+
252+
const getRowThemeOverride = result.current.getRowThemeOverride;
253+
254+
// Assuming row 1 is not a group header
255+
expect(getRowThemeOverride?.(1)).toEqual({ bgCell: "green" });
256+
expect(getRowThemeOverride?.(2)).toEqual({ bgCell: "blue" });
257+
expect(getRowThemeOverride?.(5)).toEqual({ bgCell: "green" });
258+
});
259+
});
260+
261+
describe("useRowGroupingInner - rowHeight", () => {
262+
afterEach(async () => {
263+
await cleanup();
264+
});
265+
266+
it("applies provided group row height for group headers", () => {
267+
const rowGroupingOptions: RowGroupingOptions = {
268+
groups: [
269+
{ headerIndex: 0, isCollapsed: false },
270+
{ headerIndex: 3, isCollapsed: false },
271+
{ headerIndex: 5, isCollapsed: false },
272+
],
273+
height: 30,
274+
};
275+
276+
const { result } = renderHook(() => useRowGroupingInner(rowGroupingOptions, 5, 20, undefined));
277+
278+
expect(typeof result.current.rowHeight).toBe("function");
279+
280+
// Assuming row 1 is not a group header
281+
const rowHeightFn = result.current.rowHeight as (row: number) => number;
282+
expect(rowHeightFn(0)).toEqual(rowGroupingOptions.height);
283+
expect(rowHeightFn(3)).toEqual(rowGroupingOptions.height);
284+
expect(rowHeightFn(5)).toEqual(rowGroupingOptions.height);
285+
});
286+
287+
it("applies provided group row height for group headers when some are collapsed", () => {
288+
const rowGroupingOptions: RowGroupingOptions = {
289+
groups: [
290+
{ headerIndex: 0, isCollapsed: false },
291+
{ headerIndex: 3, isCollapsed: true },
292+
{ headerIndex: 5, isCollapsed: false },
293+
],
294+
height: 30,
295+
};
296+
297+
const { result } = renderHook(() => useRowGroupingInner(rowGroupingOptions, 5, 20, undefined));
298+
299+
expect(typeof result.current.rowHeight).toBe("function");
300+
301+
// Assuming row 1 is not a group header
302+
const rowHeightFn = result.current.rowHeight as (row: number) => number;
303+
expect(rowHeightFn(0)).toEqual(rowGroupingOptions.height);
304+
expect(rowHeightFn(3)).toEqual(rowGroupingOptions.height);
305+
expect(rowHeightFn(4)).toEqual(rowGroupingOptions.height);
306+
});
307+
308+
it("returns correct height for non-group-header rows", () => {
309+
const rowGroupingOptions = {
310+
groups: [
311+
{ headerIndex: 0, isCollapsed: false },
312+
{ headerIndex: 3, isCollapsed: false },
313+
{ headerIndex: 5, isCollapsed: false },
314+
],
315+
height: 30,
316+
};
317+
318+
// eslint-disable-next-line unicorn/consistent-function-scoping
319+
const getRowHeightIn = (row: number) => (row % 2 === 0 ? 20 : 40);
320+
321+
const { result } = renderHook(() => useRowGroupingInner(rowGroupingOptions, 10, getRowHeightIn, undefined));
322+
323+
expect(typeof result.current.rowHeight).toBe("function");
324+
const rowHeightFn = result.current.rowHeight as (row: number) => number;
325+
326+
// Assuming row 1 is not a group header
327+
expect(rowHeightFn(1)).toEqual(40);
328+
expect(rowHeightFn(2)).toEqual(20);
329+
expect(rowHeightFn(5)).toEqual(rowGroupingOptions.height);
330+
});
331+
332+
it("returns correct height for non-group-header rows when some groups collapsed", () => {
333+
const rowGroupingOptions = {
334+
groups: [
335+
{ headerIndex: 0, isCollapsed: false },
336+
{ headerIndex: 3, isCollapsed: true },
337+
{ headerIndex: 5, isCollapsed: false },
338+
],
339+
height: 30,
340+
};
341+
342+
// eslint-disable-next-line unicorn/consistent-function-scoping
343+
const getRowHeightIn = (row: number) => (row % 2 === 0 ? 20 : 40);
344+
345+
const { result } = renderHook(() => useRowGroupingInner(rowGroupingOptions, 10, getRowHeightIn, undefined));
346+
347+
expect(typeof result.current.rowHeight).toBe("function");
348+
const rowHeightFn = result.current.rowHeight as (row: number) => number;
349+
350+
// Assuming row 1 is not a group header
351+
expect(rowHeightFn(1)).toEqual(40);
352+
expect(rowHeightFn(2)).toEqual(20);
353+
expect(rowHeightFn(5)).toEqual(40); // this will be the first row of the third group
354+
});
236355
});
237356

238357
describe("useRowGroupingInner - rows", () => {

0 commit comments

Comments
 (0)