# Row grouping

> Group rows by one or more columns into collapsible sections with live subtotals on each group header.

`groupBy` groups the rows by the values of one or more columns: a portfolio by sector, orders by desk and then by trader. Each group has a header row that collapses it and shows subtotals for the columns that ask for one, and the subtotals stay current while the values tick.

Click a group header to collapse it. The headers show the average price and change and the total volume of their rows.

Example: Grouped by sector (grouping.svelte)

```svelte
<script lang="ts">
  import { Grid, type ColumnDef } from 'bo-grid';
  import { quotes } from './data';

  const columns: ColumnDef[] = [
    { type: 'text', key: 'symbol', header: 'Symbol', sub: 'name', width: 180 },
    { type: 'text', key: 'sector', header: 'Sector', width: 130 },
    // Group headers show these aggregates, live.
    { type: 'price', key: 'price', header: 'Avg price', groupAgg: 'avg' },
    { type: 'percent', key: 'change', header: 'Avg chg %', groupAgg: 'avg' },
    { type: 'volume', key: 'volume', header: 'Volume', flex: 1, groupAgg: 'sum' },
  ];
  let rows = $state(quotes());
</script>

<Grid {rows} {columns} height={320} groupBy={['sector']} />
```

data.ts:

```ts
// Small, fixed data for the docs' examples: every example reads the same
// symbols, so a reader compares features, not datasets.
// A type, not an interface: it then fits GridRow's index signature.
export type Quote = {
  id: number;
  symbol: string;
  name: string;
  sector: string;
  price: number;
  change: number;
  volume: number;
};

const BASE: Omit<Quote, 'id'>[] = [
  { symbol: 'VNM', name: 'Vinamilk', sector: 'Consumer', price: 68.5, change: 1.2, volume: 1_204_000 },
  { symbol: 'FPT', name: 'FPT Corp', sector: 'Technology', price: 132, change: -0.4, volume: 860_500 },
  { symbol: 'HPG', name: 'Hoa Phat', sector: 'Materials', price: 27.35, change: 0.74, volume: 3_412_900 },
  { symbol: 'VCB', name: 'Vietcombank', sector: 'Financials', price: 91.5, change: 0.55, volume: 1_986_200 },
  { symbol: 'MWG', name: 'Mobile World', sector: 'Consumer', price: 51.4, change: -1.1, volume: 2_250_400 },
  { symbol: 'SSI', name: 'SSI Securities', sector: 'Financials', price: 33.3, change: -0.45, volume: 4_104_700 },
  { symbol: 'VIC', name: 'Vingroup', sector: 'Real estate', price: 44.15, change: -0.11, volume: 1_530_100 },
  { symbol: 'GAS', name: 'PV Gas', sector: 'Energy', price: 71.8, change: 0.3, volume: 645_800 },
  { symbol: 'MSN', name: 'Masan', sector: 'Consumer', price: 76.2, change: 2.05, volume: 1_120_300 },
  { symbol: 'TCB', name: 'Techcombank', sector: 'Financials', price: 23.9, change: 0.84, volume: 5_870_000 },
  { symbol: 'VHM', name: 'Vinhomes', sector: 'Real estate', price: 41.6, change: -0.72, volume: 2_904_600 },
  { symbol: 'POW', name: 'PV Power', sector: 'Energy', price: 12.25, change: 1.66, volume: 6_315_200 },
];

/** A fresh copy of the quotes, so one example's edits never reach another. */
export function quotes(): Quote[] {
  return BASE.map((q, i) => ({ id: i + 1, ...q }));
}
```

## Grouping

Pass the column keys to group by, outermost first:

```svelte
<Grid {rows} {columns} height={480} groupBy={['sector']} />
<Grid {rows} {columns} height={480} groupBy={['sector', 'exchange']} /> <!-- nested -->
```

`groupBy` is a prop like any other: change it and the grid regroups. Grouping is for in-memory rows; for data grouped on a server, see [Server-side grouping](https://bonguynvan.github.io/bo-grid/docs/server-grouping/).

## Subtotals

Give a column a `groupAgg` (`'sum'`, `'avg'`, `'count'`, `'min'` or `'max'`) and every group header shows that aggregate of the column over the group's rows:

```ts
const columns: ColumnDef[] = [
  { type: 'text', key: 'symbol', header: 'Symbol' },
  { type: 'price', key: 'price', header: 'Avg price', groupAgg: 'avg' },
  { type: 'volume', key: 'volume', header: 'Volume', groupAgg: 'sum' },
];
```

The same `groupAgg` drives the grand-total row when `footer` is on; see [Cell selection & clipboard](https://bonguynvan.github.io/bo-grid/docs/selection/).

## Large grouped sets

Group headers are as tall as data rows, so a grouped grid virtualizes like a flat one and scrolls smoothly over many thousands of rows. While you scroll inside a group, its header stays pinned to the top, so you always know which group you are in. Subtotals recompute as a live feed changes values.

## A grouped portfolio

Positions grouped by sector, with live subtotals and a P&L heatmap:

Live example: https://bonguynvan.github.io/bo-grid/examples/portfolio/ (source: https://github.com/bonguynvan/bo-grid/blob/main/src/demo/examples/)

## Reference

- `groupBy`: `string[]` (default `[]`) — Column keys to group rows by, outermost first (nested groups with subtotals). In-memory mode only.

  ```svelte
  <Grid {rows} {columns} height={480} groupBy={['account', 'sym']} />
  ```

- `footer`: `boolean` (default `false`) — Show a pinned totals row: each column with a `groupAgg` shows that aggregate over all (filtered) rows. In-memory mode only.

- `groupAgg`: `AggKind` (default `undefined`) — When grouping is active (or with `footer`), show this aggregate of the column on group headers and the totals row.

  ```ts
  { type: 'volume', key: 'qty', header: 'Qty', groupAgg: 'sum' }
  ```

---
Page: https://bonguynvan.github.io/bo-grid/docs/grouping/ · All pages: https://bonguynvan.github.io/bo-grid/llms.txt
