# Grouping & Pivot

> The groupBy row model, aggregates, the summaries row and the pivot helper; separate entry points that cost nothing until imported.

Page: https://svelte-lean.vercel.app/table/grouping-pivot · Markdown: https://svelte-lean.vercel.app/table/grouping-pivot.md · Index: https://svelte-lean.vercel.app/llms.txt

Grouping is a row model, summaries are an aggregate object, and a pivot is a data transformation. All three are separate entry points that cost nothing until imported.

- **Source**: grouping.tspivot.tscore.ts (prepareRows, paginateNodes)
- **Tests**: core.test.tsdata-table.dom.test.tstable.test.tstable.spec.ts (browser)
- **Fixture**: fixtures/table-basic
- **Benchmark**: table.bench.spec.ts

The engine takes an optional `getRowModel`; `groupBy()` is one. It runs the same search, filters and sorting as the default model, buckets the sorted rows by one or more columns, computes the aggregates the columns declare, and hands the result to the same pagination. Group rows are nodes with a stable id derived from the group path, so an expanded group stays expanded when the rows underneath re-sort.

This is what "supporting a feature does not mean shipping it" looks like in a build: the grouping entry measures 2149 B gzip and the pivot entry 526 B gzip. A production build of a plain `DataTable` contains neither module: the `table-basic` fixture asserts both exclusions in its module graph.

`entry points`

```ts
import { groupBy, summarize, aggregate } from '@svelte-lean/table/grouping';
import { pivot } from '@svelte-lean/table/pivot';

// A grouped row model: register it on the table.
<DataTable getRowModel={groupBy(['team', 'region'])} … />
```

## Row grouping

Derive group rows and aggregates with the optional grouping entry point.

RowGrouping.svelte

```svelte
<script lang="ts">
	import { DataTable } from '@svelte-lean/table';
	import { groupBy } from '@svelte-lean/table/grouping';
	import { memberColumns, pick } from '$lib/demo/columns';
	import { people } from '$lib/demo/data';

	const data = people(12);
	// revenue carries aggregate: 'sum' and progress aggregate: 'mean'; group rows show both.
	const columns = pick(memberColumns(), ['name', 'team', 'region', 'revenue', 'progress']);
</script>

<DataTable
	{data}
	{columns}
	getRowId={(row) => row.id}
	getRowModel={groupBy(['team'])}
	state={{ expanded: ['__group:["team:string:\"Engineering\""]'] }}
	pagination={false}
	label="Members by team"
/>
```

Rows grouped by team. Each group row is a header cell with a toggle button carrying aria-expanded, the member count and the aggregates of the revenue (sum) and progress (mean) columns. The Engineering group starts expanded through state.expanded and the group id format.

- Nested grouping is a list of column ids in order; each level adds a depth.
- Tree input (`getSubRows`) and grouping are separate row models; combining them throws.
- Search and filters apply to the rows before grouping, so an empty group disappears.

## Summaries

Render totals aligned with visible and pinned columns.

Summaries.svelte

```svelte
<script lang="ts">
	import { DataTable } from '@svelte-lean/table';
	import { summarize } from '@svelte-lean/table/grouping';
	import { memberColumns, pick } from '$lib/demo/columns';
	import { money, people } from '$lib/demo/data';

	const data = people(8);
	const columns = pick(memberColumns(), ['name', 'team', 'revenue', 'progress']);
	// summarize() returns one value per column with an aggregate; format it for the tfoot.
	const totals = summarize(data, columns);
	const summary = {
		name: 'Total',
		revenue: money.format(Number(totals.revenue)),
		progress: `${Math.round(Number(totals.progress))}% mean`
	};
</script>

<DataTable {data} {columns} getRowId={(row) => row.id} {summary} pagination={false} label="Members" />
```

summarize() returns one value per column that declares an aggregate; the specimen formats them and passes the object as summary, which renders a tfoot row keyed by column id. Pinned columns and hidden columns are respected because the row is rendered from the visible columns.

## Pivot

Turn long-form records into a compact cross-tab using an optional pure helper.

Pivot.svelte

```svelte
<script lang="ts">
	import { DataTable } from '@svelte-lean/table';
	import { pivot } from '@svelte-lean/table/pivot';
	import { money, people } from '$lib/demo/data';

	// Long-form members become one row per team with one column per region.
	const result = pivot(people(36), {
		row: (item) => item.team,
		column: (item) => item.region,
		value: (item) => item.revenue,
		aggregate: 'sum',
		rowLabel: 'Team'
	});
	const columns = result.columns.map((column) =>
		column.id === 'label' ? column : { ...column, format: (v: unknown) => money.format(Number(v)) }
	);
</script>

<DataTable data={result.data} {columns} getRowId={(row) => row.id} bordered pagination={false} label="Revenue by team and region" />
```

pivot() returns rows and columns for a second DataTable: one row per team, one column per region, a pinned label column and a total. The aggregate is the same function grouping uses. No workbook or chart dependency is involved.

## API

| Name | Of | Type | Default | Description |
| --- | --- | --- | --- | --- |
| [`getRowModel`](https://svelte-lean.vercel.app/table/api.md#prop-getrowmodel) | Data and identity | `(context: ModelContext<T>) => RowModel<T>` | `createRowModel` | Replaces the client row model; groupBy() from the grouping entry is the shipped alternative. |
| [`summary`](https://svelte-lean.vercel.app/table/api.md#prop-summary) | Events and composition | `Record<string, string \| number>` | `undefined` | A \<tfoot\> row keyed by leaf column id; summarize() from the grouping entry produces one. |
| [`aggregate`](https://svelte-lean.vercel.app/table/api.md#column-aggregate) | Sort, filter and aggregate | `'sum' \| 'mean' \| 'min' \| 'max' \| 'count' \| ((values: unknown[]) => unknown)` | `undefined` | Aggregate used by groupBy() and summarize(). |
| [`expanded`](https://svelte-lean.vercel.app/table/api.md#state-expanded) | TableState | `RowId[]` | `[]` | Expanded details, tree and group row ids. |
| [`Aggregate`](https://svelte-lean.vercel.app/table/api.md#type-aggregate) | Events and positions | `'sum' \| 'mean' \| 'min' \| 'max' \| 'count' \| ((values: unknown[]) => unknown)` | – | An aggregate name or function. |
| [`RowNode<T>`](https://svelte-lean.vercel.app/table/api.md#type-rownode-t-) | Row model | `{ id; data?; depth; children; kind: 'row' \| 'group'; label?; count?; aggregates? }` | – | A rendered row or a group row. |
| [`@svelte-lean/table/grouping`](https://svelte-lean.vercel.app/table/api.md#module--svelte-lean-table-grouping) | Entry points | `groupBy · summarize · aggregate` | – | A grouped row model for getRowModel, a totals object for summary, and the aggregate function both use. |
| [`@svelte-lean/table/pivot`](https://svelte-lean.vercel.app/table/api.md#module--svelte-lean-table-pivot) | Entry points | `pivot` | – | Long-form records to a cross-tab: rows, columns and totals for a second DataTable. |

## Related

- [Rows](https://svelte-lean.vercel.app/table/rows.md)
- [Sorting](https://svelte-lean.vercel.app/table/sorting.md)
- [Table overview](https://svelte-lean.vercel.app/table.md)
