Features guide
Everything below is available in the free @kanunilabs/pivotgrid-react
package unless a section is explicitly marked Enterprise. Each feature is
driven by props or per-field configuration — no imperative setup required.
Most features are configured on the PivotField objects you pass to
initialFields (or the controlled fields prop), or on the layout,
stateStoring, theme and toolbar props of BasePivotGrid.
Aggregation
Every measure (a field in the data area) declares a summaryType:
{ id: 'revenue', dataField: 'revenue', caption: 'Revenue', dataType: 'number',
area: 'data', summaryType: 'sum' }
Supported: sum, count, avg, min, max, distinct (distinct count) and
custom. All aggregation runs in the Web Worker, off the main thread.
Custom summaries
For bespoke rollups, provide calculateCustomSummary (start → calculate →
finalize), the same three-phase model used by classic pivot engines:
{ id: 'weightedAvg', dataField: 'price', caption: 'Weighted Avg', dataType: 'number',
area: 'data', summaryType: 'custom',
calculateCustomSummary: (o) => {
if (o.summaryProcess === 'start') o.totalValue = 0;
if (o.summaryProcess === 'calculate') o.totalValue += o.value ?? 0;
if (o.summaryProcess === 'finalize') o.totalValue = o.totalValue; // your math
} }
Summary display modes
Transform an aggregated value after it is computed with summaryDisplayMode:
{ id: 'revenue', dataField: 'revenue', caption: '% of Total', dataType: 'number',
area: 'data', summaryType: 'sum', summaryDisplayMode: 'percentOfTotal' }
| Mode | Shows each cell as… |
|---|---|
absolute | the raw aggregated value (default) |
percentOfTotal | % of the grand total |
percentOfRowTotal | % of its row total |
percentOfColumnTotal | % of its column total |
runningTotal | a cumulative running total |
difference | the difference from the previous value |
percentDifference | the % difference from the previous value |
Grouping
Group date and number fields with groupInterval:
{ id: 'orderDate', dataField: 'orderDate', caption: 'Month', dataType: 'date',
area: 'column', groupInterval: 'month' }
Date intervals: year, quarter, month, day. Numbers accept numeric
(bucketed) or a numeric bucket size, or custom.
Sorting
Sort a field's own values, or sort a row/column dimension by a measure:
// Sort Regions by descending Revenue rather than alphabetically.
{ id: 'region', dataField: 'region', caption: 'Region', dataType: 'string',
area: 'row', sortOrder: 'desc', sortBySummaryField: 'revenue' }
sortBySummaryField targets a data field by id. For an order the built-ins
cannot express, supply calculateCustomSort — a comparator that also receives
the two cells being compared.
Top-N
Not in a published package yet.
topNworks onmainand ships in the next release of@kanunilabs/pivotgrid-core. On the current published version (1.2.0) it is still accepted and ignored. This note goes away when that release lands.
Keep only the leading values of a row or column dimension. topN is applied
after the sort, so it means "top by whatever you just sorted by":
// The ten highest-revenue regions, and nothing else.
{ id: 'region', dataField: 'region', caption: 'Region', dataType: 'string',
area: 'row', sortOrder: 'desc', sortBySummaryField: 'revenue', topN: 10 }
It applies per level: a topN on Region and a different one on Category keeps
the top regions, and within each, the top categories.
Grand totals still cover the whole dataset. A "top 10 regions" view totals all regions, not the ten on screen, so the visible rows will not add up to the total. That is deliberate and matches how other grids behave without an "Others" row.
topNShowOthersandtopNOthersCaptionare not implemented. There is no "Others" row: the rows beyondtopNare dropped. SettingtopNShowOthersdoes not fail silently — it reports through youronErrorhandler (orconsole.error) naming the field.The reason it is not a small job: the remainder cannot be produced by adding up the rows that were cut, because
avg,min,maxanddistinctare not additive. A correct Others row needs a second aggregation pass over the source data, which is an engine change rather than a display one.All three of these were declared and read by nothing until 2026-08-27, and this page carried a worked example of the "Others" row that never existed.
Filtering
Header filters are built in — click a row/column field header to include or exclude values. You can also seed filters declaratively:
{ id: 'country', dataField: 'country', caption: 'Country', dataType: 'string',
area: 'filter', filterType: 'include', filterValues: new Set(['USA', 'Germany']) }
Fully keyboard-driven: the filter popup never needs the mouse — start typing to search, use ↑/↓ to move through the values, Space to toggle a checkbox, Backspace to jump straight back to the search box (deleting as you go), and Enter to apply.
The visual Prefilter builder (pre-aggregation, multi-condition) is an Enterprise feature — see Enterprise & licensing.
Conditional formatting
Style cells by value with expression rules. Each rule provides a formula
(value, cell) => boolean and a style applied when it returns true:
{ id: 'revenue', dataField: 'revenue', caption: 'Revenue', dataType: 'number',
area: 'data', summaryType: 'sum',
conditionalFormatting: [
{ type: 'expression', formula: (v) => v < 0, style: { color: '#dc2626', fontWeight: 600 } },
{ type: 'expression', formula: (v) => v > 100000, style: { backgroundColor: '#dcfce7' } },
] }
Rules are evaluated top-to-bottom and merged, so later rules win on conflicting
properties. style is a standard React CSSProperties object.
PivotConditionRule.type is typed as 'colorScale' | 'dataBar' | 'iconSet' | 'expression', but only expression is evaluated — on screen and in every
export path alike. The other three compile, and the rule is skipped. Colour
scales and data bars are reachable today by writing the comparison yourself in a
formula; icon sets are not reachable at all without a cellTemplate.
Since the next release they are no longer skipped in silence: setting one
reports through your onError handler (or console.error) when the fields are
set, naming the field and the rule type. Once per message, not per cell.
Note also that the export applies the first matching rule where the screen merges all of them, so a cell with two matching rules can look different in a downloaded file.
Totals & layout
The
layoutbridging below is not in a published package yet. It works onmainand ships in the next release of@kanunilabs/pivotgrid-core. On the published 1.2.0,showGrandTotals,showSubTotals,showColumnTotalsandtotalsDisplayModeare still accepted and ignored — use the granular flags.
Grand totals, subtotals and the row-header shape are controlled by the layout
prop (PivotLayoutState):
<BasePivotGrid
gridId="sales"
data={data}
initialFields={fields}
layout={{
showRowGrandTotals: true, // the grand total row
showColumnGrandTotals: true, // the grand total column
showRowTotals: true, // per-level subtotals on the row axis
totalsPosition: 'bottom', // 'top' | 'bottom'
rowHeaderLayout: 'tree', // 'tabular' (a column per field) | 'tree' (nested)
hideEmptySummaryCells: true, // drop empty rows/columns
dataFieldPosition: 'columns', // where measure captions live
}}
/>
The coarse form
There is a second, DevExtreme-shaped spelling of the same switches — one option per axis pair instead of one per axis:
layout={{
showGrandTotals: 'both', // → showRowGrandTotals + showColumnGrandTotals
showSubTotals: 'rows', // → showRowTotals
totalsDisplayMode: 'top', // → totalsPosition
}}
These are translated at the controller boundary, so either spelling works. If you pass both, the granular one wins — it is the more specific request.
Subtotals on the column axis do not exist.
showSubTotals: 'columns'andshowSubTotals: 'both'take effect for the row axis only, and the same goes forshowColumnTotals. Row subtotals are inserted while flattening the row tree; the column axis has no equivalent pass in the renderer, so this is a missing feature rather than a missing switch.It is not swallowed: asking for it reports through your
onErrorhandler (orconsole.error), once per message.All four of these were declared and read by nothing until 2026-08-27, and this page used two of them in the example above.
Import
The footer's Import menu ingests external data without code. It reads
CSV / TSV / TXT, Excel (.xlsx, .xls, .xlsm, .xlsb), XML and
JSON, plus clipboard paste (delimited text) and drag-and-drop onto
the grid. Column types are inferred with locale-aware parsing (EU/US decimals,
dates, booleans, leading-zero IDs stay strings). Large files stream with a
progress bar and can be cancelled.
Receive imported rows via onImportData, and optionally show a preview +
column-mapping step first:
<BasePivotGrid
gridId="sales"
data={data}
initialFields={fields}
previewImport // show a preview/mapping step before applying
onImportData={(rows) => setData(rows)}
/>
Export
The footer's Export menu produces CSV, JSON (list / tree / full session
snapshot), XML, Print and Copy to clipboard (tab-separated) in the Community
build. Styled Excel (WYSIWYG Pivot View or flat List View) and PDF export are
Enterprise features — in Community they appear in the menu with an "Enterprise" badge,
and become active when you render EnterprisePivotGrid. Large exports switch to a
streaming writer, and CSV output is sanitized against formula injection.
Cancellation and progress are plumbed through exportData's signal and
onProgress, but the built-in download menu passes neither — you reach them only
through your own PivotGridRef. Row assembly yields between chunks; the three
heaviest calls do not. See Styled Excel & PDF export for
what changes at 100,000 rows and what the file will not match on screen.
Hook onExporting to observe or cancel an export:
<BasePivotGrid
gridId="sales" data={data} initialFields={fields}
onExporting={(e) => { if (!userAllowed) e.cancel = true; }}
/>
Saved views & state persistence
The footer's report menu lets users save, load and set a default named view
(fields + layout). Persist the whole grid state across reloads with
stateStoring:
<BasePivotGrid
gridId="sales" data={data} initialFields={fields}
stateStoring={{ enabled: true, type: 'localStorage', storageKey: 'sales-pivot' }}
/>
Use type: 'custom' with onSave/onLoad to persist to your own backend.
Theming & striping
The grid is themed entirely with CSS variables. Pass a theme object, or toggle
zebra striping with striped:
<BasePivotGrid
gridId="sales" data={data} initialFields={fields}
striped
theme={{ primary: '#0284c7', surfaceHeader: '#e0f2fe', border: '#bae6fd' }}
/>
Try the presets live in the Theming playground.
Localization & RTL
Twelve locales ship in the box; ar flips the layout to RTL automatically. See
the Quick start and the
Localization playground.
Performance
Aggregation runs in a Web Worker over columnar storage with a trie-based grouper, so the UI stays responsive on 1M+ rows — try the Big Data Performance playground (1K → 2M rows). The worker is compiled into the package and started from a Blob URL, so there is nothing to configure in any bundler:
<BasePivotGrid data={data} initialFields={fields} />
A strict Content-Security-Policy is the one case that still needs attention — see Installation.
Toolbar customization
Show or hide individual footer controls with the toolbar prop:
<BasePivotGrid
gridId="sales" data={data} initialFields={fields}
toolbar={{ showExportButton: true, showImportFile: false, showCalculatedFields: false }}
/>
Looking for calculated fields, charts or drill-down? Those are in the Enterprise package. For every prop and type, see the API Reference.
Missing a feature? Tell us
The grid grows out of real-world requests — if something you need is missing, awkward, or plain broken, we genuinely want to hear it. Write to [email protected] or use the contact form; license holders can also open a support ticket. Every message is read and answered.