Documentation

Multi-level grouping and group summaries

Grouping itself is Community, and so is the group panel, and so are footer column totals. Enterprise adds exactly two things:

  1. a second and further grouping level,
  2. those same totals repeated on every group header.

Worth knowing before you reach for a licence key: if one level is enough, you already have everything.

One level — Community

import { DataGrid } from '@kanunilabs/datagrid-react';

<DataGrid
  dataSource={orders}
  rowKey="id"
  columns={columns}
  grouping={{ groupBy: 'region', panel: true }}
  summaries={[
    { columnId: 'units', type: 'sum' },
    { columnId: 'revenue', type: 'sum' },
  ]}
/>

panel: true puts the drop strip above the grid; dragging a header into it groups by that column. summaries draws the totals in the footer.

More levels — Enterprise

import { EnterpriseDataGrid } from '@kanunilabs/datagrid-react-enterprise';

<EnterpriseDataGrid
  licenseKey={key}
  dataSource={orders}
  rowKey="id"
  columns={columns}
  grouping={{
    groupBy: ['region', 'category', 'rep'],   // outermost first
    panel: true,
    groupSummaries: true,
    collapsedByDefault: true,
  }}
  summaries={[
    { columnId: 'units', type: 'sum' },
    { columnId: 'revenue', type: 'sum' },
  ]}
/>
OptionTypeDefault
grouping.groupBystring | string[] | nullnull
grouping.panelbooleanfalse
grouping.groupSummariesbooleanfalse
grouping.collapsedByDefaultbooleanfalse
grouping.showGroupedColumnsbooleanfalse

groupSummaries: true on its own shows nothing. The group header reuses the summaries array, so an empty one produces empty headers. There is no separate per-level or per-group definition list.

Totals land inside the header strip — ▸ Category: Toys (12) · Revenue: 1,240 — not in a footer row beneath the members, even though the header menu item is worded "Show group footers".

Summary types are sum, avg, min, max, count and custom. avg, min and max skip blanks; count counts non-empty cells.

summaries={[
  {
    columnId: 'margin',
    type: 'custom',
    calculate: (cells) => cells.reduce((a, c) => a + Number(c.value ?? 0), 0) / cells.length,
  },
]}

Inside a group header, calculate receives only that group's cells.

Reacting to it

controller.events.on('groupChanged', ({ groupBy, levels }) => {
  // groupBy = outermost level, levels = every level
});

columnStateChanged fires too, because the grouped column leaves the data area and comes back when grouping is removed.

From code: controller.groupBy(id), controller.setGroupSummaries(true), controller.expandAllGroups(), controller.collapseAllGroups().

See it working →

What this does not do

  • Community trims silently. Pass three levels to DataGrid and you get the first one; groupSummaries is forced to false. The warning is printed only under debugMode: true. Adding a second level in the Community UI replaces the first rather than nesting — a deliberate swap, not a no-op.
  • Group keys are String(value). No year/month/quarter bucketing for dates, no groupInterval, no groupValueGetter. Two values with the same text (1 and '1') merge into one group; null and undefined collect under a localised "(Blanks)".
  • valueFormatter is not applied to the header caption. A formatter takes a row, and a group strip has no single row. lookup columns are the exception — those resolve to the label rather than the code.
  • The runtime "group footers" toggle is React-only. The vanilla header menu has no such item; call controller.setGroupSummaries(...) yourself.
  • Grouping outranks the user's sort. Group columns are pushed to the front of the sort, so a user's own sort becomes the secondary key. They can still flip a group column's direction, and that is kept.
  • No auto group column. There is no autoGroupColumnDef equivalent: the group row is one full-width cell pinned left, and depth is shown with 18px of indent.
  • Group headers cannot be selected. Space on a focused header toggles it; there is no "select every row in this group" checkbox.
  • groupable: false only closes the UI. The header menu item is disabled and the column drops out of the panel list, but controller.groupBy('thatOne') still works.
  • Export carries the label, not the totals. A group row exports as "<label> (<count>)" in the first cell only. Styled Excel export does turn nesting into real outline levels; PDF only indents.
  • Group totals lag unsaved edits. Footer totals read pending edits; the cached group tree does not, so a group header can show the old number until the edit is committed.
  • Tree wins over grouping. Configure both and groupBy is ignored, warned once under debugMode.
  • Server-side grouping is the server's job. With a DataSource object the client computes nothing: return groups: RemoteGroupRow[] keyed on groupKeys length, with aggregates under ${columnId}:${type}. type: 'custom' is never sent (a JS reducer cannot run there), and expandAllGroups() is a deliberate no-op — it would mean fetching the whole table. collapseAllGroups() works.