Vesture

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.

PivotGrid

Preview

Sum of revenue, by region × quarter
regionQ1Q2Q3Q4Total
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

PropTypeDefaultDescription
dataT[]—Required. Flat records.
rows(keyof T)[]—Required. Row-group fields, outermost first.
columnskeyof T—Field whose values become columns; omit for a single value column.
valuekeyof 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.
columnOrderstring[]—Explicit column order; unlisted keys follow, sorted.

Totals & text

PropTypeDefaultDescription
showTotalsbooleantrueShorthand for rowTotals and columnTotals.
rowTotalsboolean—Right-hand total column. Needs 2+ columns.
columnTotalsboolean—Bottom totals row.
totalLabelstring"Total"
emptyValueReactNode"–"Shown for cells with no records.
rowHeadersstring[]—Header text per row field; defaults to the field names.
formatValue(value: number) => string—Defaults to locale number formatting.
captionstring—Table caption.

Customization

PropTypeDefaultDescription
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.