Documentation

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' }
ModeShows each cell as…
absolutethe raw aggregated value (default)
percentOfTotal% of the grand total
percentOfRowTotal% of its row total
percentOfColumnTotal% of its column total
runningTotala cumulative running total
differencethe difference from the previous value
percentDifferencethe % 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. topN works on main and 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.

topNShowOthers and topNOthersCaption are not implemented. There is no "Others" row: the rows beyond topN are dropped. Setting topNShowOthers does not fail silently — it reports through your onError handler (or console.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, max and distinct are 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 layout bridging below is not in a published package yet. It works on main and ships in the next release of @kanunilabs/pivotgrid-core. On the published 1.2.0, showGrandTotals, showSubTotals, showColumnTotals and totalsDisplayMode are 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' and showSubTotals: 'both' take effect for the row axis only, and the same goes for showColumnTotals. 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 onError handler (or console.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.