Data
PivotGridNew in 0.7.0
A pivot table: cross-tabulates flat records into nested row groups and columns with totals — built-in or custom aggregates, drill-down clicks, and full control over labels and cell rendering.
PivotGridPreview
| region | Q1 | Q2 | Q3 | Q4 | Total |
|---|---|---|---|---|---|
| East | $5,600 | – | $25,600 | – | $31,200 |
| North | $20,600 | $15,800 | $9,100 | – | $45,500 |
| South | $6,400 | $19,100 | – | – | $25,500 |
| West | – | – | – | $23,900 | $23,900 |
| Total | $32,600 | $34,900 | $34,700 | $23,900 | $126,100 |
Click any cell to see the drill-down payload.
Usage
tsx
import { PivotGrid } from "@vesture/react";
<PivotGrid
data={sales}
rows={["region", "product"]}
columns="quarter"
value="revenue"
aggregate="sum"
formatValue={(n) => `$${n.toLocaleString()}`}
onCellClick={({ rowPath, columnKey, value }) => openDetails(rowPath, columnKey, value)}
getCellStyle={({ kind, value }) =>
kind === "cell" && value && value > 10000 ? { fontWeight: 600 } : undefined
}
/>Behavior
- rows lists the fields that become row groups (outermost first); columns is the field whose distinct values become columns; value is the numeric field to aggregate. Blank values group under (blank).
- aggregate is "sum" | "count" | "avg" | "min" | "max" — or a function over each group's numeric values, so median, distinct count or percentiles are one line.
- Totals are computed from the underlying records, not from the displayed cells, so an average total is a true average rather than an average of averages. The same holds for custom aggregates.
- Cells with no records show emptyValue; count includes records whose value is missing, other aggregates skip them.
- onCellClick turns value cells into drill-down targets: it receives { kind, value, rowPath, columnKey } for cells, row/column totals and the grand total. Empty cells are not clickable.
- renderCell and getCellStyle receive the same info, so data bars, heat-coloring and sparkline cells are straightforward. renderCell returning undefined keeps the default content; null renders nothing.
- Order is natural (numeric-aware) by default; sortRows, sortColumns and columnOrder override it.
Props
Shape
| Prop | Type | Default | Description |
|---|---|---|---|
| data | T[] | — | Required. Flat records. |
| rows | (keyof T)[] | — | Required. Row-group fields, outermost first. |
| columns | keyof T | — | Field whose values become columns; omit for a single value column. |
| value | keyof T | — | Numeric field to aggregate (not needed for count). |
| aggregate | "sum" | "count" | "avg" | "min" | "max" | ((values: number[]) => number) | "sum" | |
| sortRows | (a: string[], b: string[]) => number | — | Compare two row paths. |
| sortColumns | (a: string, b: string) => number | — | Compare two column keys. |
| columnOrder | string[] | — | Explicit column order; unlisted keys follow, sorted. |
Totals & text
| Prop | Type | Default | Description |
|---|---|---|---|
| showTotals | boolean | true | Shorthand for rowTotals and columnTotals. |
| rowTotals | boolean | — | Right-hand total column. Needs 2+ columns. |
| columnTotals | boolean | — | Bottom totals row. |
| totalLabel | string | "Total" | |
| emptyValue | ReactNode | "–" | Shown for cells with no records. |
| rowHeaders | string[] | — | Header text per row field; defaults to the field names. |
| formatValue | (value: number) => string | — | Defaults to locale number formatting. |
| caption | string | — | Table caption. |
Customization
| Prop | Type | Default | Description |
|---|---|---|---|
| renderRowLabel | (label, level, path) => ReactNode | — | |
| renderColumnLabel | (key) => ReactNode | — | |
| renderCell | (info: PivotCellInfo) => ReactNode | — | undefined keeps the default; null renders nothing. |
| getCellStyle | (info) => CSSProperties | undefined | — | Per-cell inline style. |
| onCellClick | (info) => void | — | Makes non-empty cells clickable for drill-down. |