Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improve Performance in Row Grouping #924

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
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
26 changes: 21 additions & 5 deletions packages/core/src/data-editor/row-grouping.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@ import type { DataEditorProps } from "./data-editor.js";
import type { DataGridProps } from "../internal/data-grid/data-grid.js";
import { whenDefined } from "../common/utils.js";

type Mutable<T> = {
-readonly [K in keyof T]: T[K];
};

export type RowGroup = {
/**
* The index of the header if the groups are all flattened and expanded
Expand Down Expand Up @@ -100,6 +104,7 @@ export function expandRowGroups(groups: readonly RowGroup[]): ExpandedRowGroup[]
}

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

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

let rowIndex = 0;
let contentIndex = 0;
for (const g of flattened) {
(g as any).contentIndex = contentIndex;
for (const g of flattened as Mutable<(typeof flattened)[number]>[]) {
g.contentIndex = contentIndex;
contentIndex += g.rows;

g.rowIndex = rowIndex;
rowIndex += g.isCollapsed ? 1 : g.rows + 1;
}

return flattened
Expand Down Expand Up @@ -244,6 +254,13 @@ export function useRowGroupingInner(
[options, rows]
);

const flattenedRowGroupsMap = React.useMemo(() => {
return flattenedRowGroups?.reduce<{ [rowIndex: number]: FlattenedRowGroup | undefined }>((acc, group) => {
acc[group.rowIndex] = group;
return acc;
}, {});
}, [flattenedRowGroups]);

const effectiveRows = React.useMemo(() => {
if (flattenedRowGroups === undefined) return rows;
return flattenedRowGroups.reduce((acc, group) => acc + (group.isCollapsed ? 1 : group.rows + 1), 0);
Expand All @@ -253,11 +270,10 @@ export function useRowGroupingInner(
if (options === undefined) return rowHeightIn;
if (typeof rowHeightIn === "number" && options.height === rowHeightIn) return rowHeightIn;
return (rowIndex: number) => {
const { isGroupHeader } = mapRowIndexToPath(rowIndex, flattenedRowGroups);
if (isGroupHeader) return options.height;
if (flattenedRowGroupsMap?.[rowIndex]) return options.height;
return typeof rowHeightIn === "number" ? rowHeightIn : rowHeightIn(rowIndex);
};
}, [flattenedRowGroups, options, rowHeightIn]);
}, [flattenedRowGroupsMap, options, rowHeightIn]);

const rowNumberMapperOut = React.useCallback(
(row: number): number | undefined => {
Expand Down
12 changes: 7 additions & 5 deletions packages/core/src/docs/examples/row-grouping.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -35,14 +35,10 @@ export default {

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

const [rowGrouping, setRowGrouping] = React.useState<RowGroupingOptions>(() => ({
groups: [
{
headerIndex: 0,
isCollapsed: false,
},
{
headerIndex: 10,
isCollapsed: true,
Expand All @@ -61,6 +57,12 @@ export const RowGrouping: React.VFC<any> = (p: { freezeColumns: number }) => {
headerIndex: 30,
isCollapsed: false,
},
...Array.from({ length: 100 }, (_value, i): RowGroupingOptions["groups"][number] => {
return {
headerIndex: (rows / 100) * i,
isCollapsed: false,
};
}),
],
height: 55,
navigationBehavior: "block",
Expand Down
137 changes: 128 additions & 9 deletions packages/core/test/row-grouping.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -131,8 +131,8 @@ describe("flattenRowGroups", () => {
};
const totalRows = 10;
const expected = [
{ headerIndex: 0, isCollapsed: false, depth: 0, path: [0], rows: 4, contentIndex: 0 },
{ headerIndex: 5, isCollapsed: true, depth: 0, path: [1], rows: 4, contentIndex: 4 },
{ rowIndex: 0, headerIndex: 0, isCollapsed: false, depth: 0, path: [0], rows: 4, contentIndex: 0 },
{ rowIndex: 5, headerIndex: 5, isCollapsed: true, depth: 0, path: [1], rows: 4, contentIndex: 4 },
];
const output = flattenRowGroups(rowGroupingOptions, totalRows);
expect(output).toEqual(expected);
Expand All @@ -158,9 +158,9 @@ describe("flattenRowGroups", () => {
};
const totalRows = 10;
const expected = [
{ headerIndex: 0, isCollapsed: false, depth: 0, path: [0], rows: 1, contentIndex: 0 },
{ headerIndex: 2, isCollapsed: false, depth: 1, path: [0, 0], rows: 1, contentIndex: 1 },
{ headerIndex: 4, isCollapsed: true, depth: 1, path: [0, 1], rows: 1, contentIndex: 2 },
{ rowIndex: 0, headerIndex: 0, isCollapsed: false, depth: 0, path: [0], rows: 1, contentIndex: 0 },
{ rowIndex: 2, headerIndex: 2, isCollapsed: false, depth: 1, path: [0, 0], rows: 1, contentIndex: 1 },
{ rowIndex: 4, headerIndex: 4, isCollapsed: true, depth: 1, path: [0, 1], rows: 1, contentIndex: 2 },
];
const output = flattenRowGroups(rowGroupingOptions, totalRows);
expect(output).toEqual(expected);
Expand All @@ -183,10 +183,10 @@ describe("flattenRowGroups", () => {
};
const totalRows = 7;
const expected = [
{ headerIndex: 0, isCollapsed: false, depth: 0, path: [0], rows: 0, contentIndex: 0 },
{ headerIndex: 1, isCollapsed: true, depth: 1, path: [0, 0], rows: 1, contentIndex: 0 },
{ headerIndex: 3, isCollapsed: false, depth: 1, path: [0, 1], rows: 1, contentIndex: 1 },
{ headerIndex: 5, isCollapsed: true, depth: 0, path: [1], rows: 1, contentIndex: 2 },
{ rowIndex: 0, headerIndex: 0, isCollapsed: false, depth: 0, path: [0], rows: 0, contentIndex: 0 },
{ rowIndex: 1, headerIndex: 1, isCollapsed: true, depth: 1, path: [0, 0], rows: 1, contentIndex: 0 },
{ rowIndex: 2, headerIndex: 3, isCollapsed: false, depth: 1, path: [0, 1], rows: 1, contentIndex: 1 },
{ rowIndex: 4, headerIndex: 5, isCollapsed: true, depth: 0, path: [1], rows: 1, contentIndex: 2 },
];
const output = flattenRowGroups(rowGroupingOptions, totalRows);
expect(output).toEqual(expected);
Expand Down Expand Up @@ -233,6 +233,125 @@ describe("useRowGroupingInner - getRowThemeOverride", () => {
const themeOverride = result.current.getRowThemeOverride?.(1);
expect(themeOverride).toEqual({ bgCell: "green" });
});

it("returns correct theme for non-group-header rows when some groups collapsed according to getRowThemeOverrideIn", () => {
const rowGroupingOptions = {
groups: [
{ headerIndex: 0, isCollapsed: false },
{ headerIndex: 3, isCollapsed: true },
{ headerIndex: 5, isCollapsed: false },
],
height: 30,
};

// eslint-disable-next-line unicorn/consistent-function-scoping
const getRowThemeOverrideIn = (row: number) => ({ bgCell: row % 2 === 0 ? "blue" : "green" });

const { result } = renderHook(() => useRowGroupingInner(rowGroupingOptions, 10, 20, getRowThemeOverrideIn));

const getRowThemeOverride = result.current.getRowThemeOverride;

// Assuming row 1 is not a group header
expect(getRowThemeOverride?.(1)).toEqual({ bgCell: "green" });
expect(getRowThemeOverride?.(2)).toEqual({ bgCell: "blue" });
expect(getRowThemeOverride?.(5)).toEqual({ bgCell: "green" });
});
});

describe("useRowGroupingInner - rowHeight", () => {
afterEach(async () => {
await cleanup();
});

it("applies provided group row height for group headers", () => {
const rowGroupingOptions: RowGroupingOptions = {
groups: [
{ headerIndex: 0, isCollapsed: false },
{ headerIndex: 3, isCollapsed: false },
{ headerIndex: 5, isCollapsed: false },
],
height: 30,
};

const { result } = renderHook(() => useRowGroupingInner(rowGroupingOptions, 5, 20, undefined));

expect(typeof result.current.rowHeight).toBe("function");

// Assuming row 1 is not a group header
const rowHeightFn = result.current.rowHeight as (row: number) => number;
expect(rowHeightFn(0)).toEqual(rowGroupingOptions.height);
expect(rowHeightFn(3)).toEqual(rowGroupingOptions.height);
expect(rowHeightFn(5)).toEqual(rowGroupingOptions.height);
});

it("applies provided group row height for group headers when some are collapsed", () => {
const rowGroupingOptions: RowGroupingOptions = {
groups: [
{ headerIndex: 0, isCollapsed: false },
{ headerIndex: 3, isCollapsed: true },
{ headerIndex: 5, isCollapsed: false },
],
height: 30,
};

const { result } = renderHook(() => useRowGroupingInner(rowGroupingOptions, 5, 20, undefined));

expect(typeof result.current.rowHeight).toBe("function");

// Assuming row 1 is not a group header
const rowHeightFn = result.current.rowHeight as (row: number) => number;
expect(rowHeightFn(0)).toEqual(rowGroupingOptions.height);
expect(rowHeightFn(3)).toEqual(rowGroupingOptions.height);
expect(rowHeightFn(4)).toEqual(rowGroupingOptions.height);
});

it("returns correct height for non-group-header rows", () => {
const rowGroupingOptions = {
groups: [
{ headerIndex: 0, isCollapsed: false },
{ headerIndex: 3, isCollapsed: false },
{ headerIndex: 5, isCollapsed: false },
],
height: 30,
};

// eslint-disable-next-line unicorn/consistent-function-scoping
const getRowHeightIn = (row: number) => (row % 2 === 0 ? 20 : 40);

const { result } = renderHook(() => useRowGroupingInner(rowGroupingOptions, 10, getRowHeightIn, undefined));

expect(typeof result.current.rowHeight).toBe("function");
const rowHeightFn = result.current.rowHeight as (row: number) => number;

// Assuming row 1 is not a group header
expect(rowHeightFn(1)).toEqual(40);
expect(rowHeightFn(2)).toEqual(20);
expect(rowHeightFn(5)).toEqual(rowGroupingOptions.height);
});

it("returns correct height for non-group-header rows when some groups collapsed", () => {
const rowGroupingOptions = {
groups: [
{ headerIndex: 0, isCollapsed: false },
{ headerIndex: 3, isCollapsed: true },
{ headerIndex: 5, isCollapsed: false },
],
height: 30,
};

// eslint-disable-next-line unicorn/consistent-function-scoping
const getRowHeightIn = (row: number) => (row % 2 === 0 ? 20 : 40);

const { result } = renderHook(() => useRowGroupingInner(rowGroupingOptions, 10, getRowHeightIn, undefined));

expect(typeof result.current.rowHeight).toBe("function");
const rowHeightFn = result.current.rowHeight as (row: number) => number;

// Assuming row 1 is not a group header
expect(rowHeightFn(1)).toEqual(40);
expect(rowHeightFn(2)).toEqual(20);
expect(rowHeightFn(5)).toEqual(40); // this will be the first row of the third group
});
});

describe("useRowGroupingInner - rows", () => {
Expand Down