PivotGrid

<PivotGrid> is a cross-tab aggregate view. Pick a row field and a column field; the grid shows aggregated values (count, sum, avg, min, max) at each intersection, plus row and column totals. It reads from useListContext so it composes inside <List>. Aggregation is pure client-side — for very large datasets, server-aggregate first and pass records via the data prop.

Installation

pnpm dlx shadcn@latest add @shadmin/pivot-grid

Usage

Inside a <List>:

import { List, PivotGrid } from "@/components/admin";
 
const OrderList = () => (
  <List perPage={5000}>
    <PivotGrid
      rowField="region"
      columnField="status"
      valueField="amount"
      aggregator="sum"
    />
  </List>
);

With explicit data (no <List> required):

import { PivotGrid } from "@/components/admin";
 
<PivotGrid
  data={orders}
  rowField="region"
  columnField="status"
  valueField="amount"
  aggregator="sum"
/>;

Props

PropRequiredTypeDefaultDescription
rowFieldRequiredstringRecord field whose distinct values become the row headers.
columnFieldRequiredstringRecord field whose distinct values become the column headers.
dataOptionalRecord<string, unknown>[]from useListContextExplicit data array. If omitted, data is read from the enclosing <List>.
valueFieldOptionalstringNumeric field to aggregate. If absent the aggregator is always "count".
aggregatorOptional"count" | "sum" | "avg" | "min" | "max""count"How to combine values for each (row, column) cell.
formatterOptional(value: number) => ReactNodeinteger or .toFixed(2)Formats each cell value. Defaults to integer string or two-decimal float.
emptyLabelOptionalstring"No data" (i18n)Message shown when there are no rows or columns to display.

aggregator

Five built-in aggregation modes:

ValueDescription
"count"Number of records in the intersection.
"sum"Sum of valueField for all records in the intersection.
"avg"Arithmetic mean of valueField.
"min"Minimum of valueField.
"max"Maximum of valueField.

When valueField is omitted, aggregator is effectively always "count" regardless of the prop value.

<PivotGrid
  rowField="department"
  columnField="month"
  valueField="revenue"
  aggregator="avg"
  formatter={(v) => `$${v.toFixed(2)}`}
/>

formatter

Supply a custom formatter to control cell rendering — useful for currency, percentages, or any visual treatment:

<PivotGrid
  rowField="region"
  columnField="status"
  valueField="amount"
  aggregator="sum"
  formatter={(v) =>
    new Intl.NumberFormat("en-US", {
      style: "currency",
      currency: "USD",
    }).format(v)
  }
/>

The default formatter renders integers as plain strings and floats with two decimal places.

Empty cells

Intersections with no records render an em dash () rather than 0. This distinguishes "no data" from a genuine zero value. Row and column totals always include only the non-empty intersections.

Performance note

Aggregation runs client-side on every render using useMemo. For datasets with thousands of records the cost is negligible, but for very large datasets (tens of thousands of rows) you should server-aggregate and pass pre-summarized records via the data prop so the browser receives a small cross-tab rather than the raw rows.

// Server returns pre-aggregated: [{ region, status, total_amount }]
<PivotGrid
  data={serverSummary}
  rowField="region"
  columnField="status"
  valueField="total_amount"
  aggregator="sum"
/>