Grouping
Multi-level grouping with drag-to-group, collapsible groups, per-level aggregates, and drill-into detail rows — all computed in the database.
Enabling grouping
const options = {
/* ... */
grouping: { rollup: true, aggregates: [{ field: 'revenue', fn: 'sum', as: 'sum_revenue' }] },
};
Livewire: set public bool $groupable = true; and (optionally) return
aggregates() from the component.
Drag-to-group
When grouping is enabled a group panel appears above the grid. Drag a column header onto it to group by that column; drag several to nest levels. Each grouped column shows as a chip with a ✕ to remove it.
Programmatic (client core):
grid.groupBy('region');
grid.groupBy('category'); // second level
grid.ungroup('category');
grid.setGroups(['region']);
How it renders
Grouping produces a hierarchical, indented list of group header rows with a count and per-group aggregates, each collapsible via a chevron:
▾ Region: Nord — 1,666,667 rows · sum_revenue 3,350,158,788
▾ Category: Elektronikk — 833,333 rows · sum_revenue 1,675,214,197
▾ Category: Bok — 833,334 rows · sum_revenue 1,674,751,357
▾ Region: Sør — 1,666,667 rows · ...
grid.toggleGroup(path); // collapse / expand a group node
grid.isGroupCollapsed(path);
How it works on the server
The server runs one GROUP BY query per level (GROUP BY g1, then
GROUP BY g1, g2, ...), so each level's aggregates are computed directly over
that level's rows. This is correct for non-additive functions (avg,
median, percentile, countDistinct) — not just additive sum/count — and
avoids relying on ROLLUP ordering. The deepest query also fixes the
hierarchical ordering of the emitted headers.
// The client sends group[] with per-level aggregates; the server returns
// GridResponse.groups: [{ field, value, count, level, path, aggregates }]
Aggregate functions
sum, avg, min, max, count, countDistinct, median, percentile
(with p). median/percentile require the ElyraSQL driver.
grouping: {
rollup: true,
aggregates: [
{ field: 'revenue', fn: 'sum', as: 'sum_revenue' },
{ field: 'revenue', fn: 'percentile', p: 0.95, as: 'p95' },
],
}
Per-level aggregates are correct for every function, including non-additive ones
like group-level avg and percentile.
Detail rows (drill into a group)
Enable detailRows to let users expand an innermost group and reveal its
actual data rows (lazily fetched, filtered by the group path):
// Vue / Svelte
grouping: { detailRows: true, detailLimit: 200, aggregates: [/* ... */] }
// Livewire
public bool $groupDetailRows = true;
public int $groupDetailLimit = 200;
The leaf group's chevron toggles its detail rows; expanding fetches up to
detailLimit rows (WHERE groupField = value AND <current filter>, no grouping)
and renders them indented under the header. Non-leaf group chevrons still
collapse their subgroups.
Footer aggregates (whole result)
Independent of grouping, footer aggregates run over the entire filtered result and are correct for any function. They are cached per filter.
const options = {
aggregates: [
{ field: 'revenue', fn: 'sum', as: 'sum_revenue' },
{ field: 'margin', fn: 'avg', as: 'avg_margin' },
],
};