# KanuniLabs documentation, full text
> Every guide page of the KanuniLabs PivotGrid and DataGrid documentation, as Markdown, in the order of the documentation sidebar. The API reference is not included; https://kanunilabs.com/llms.txt links to it.
---
Source: https://kanunilabs.com/docs/pivotgrid
# PivotGrid: Introduction
A fast, framework-agnostic PivotGrid for the web.
Read it at https://kanunilabs.com/docs/pivotgrid
---
Source: https://kanunilabs.com/docs/pivotgrid/installation
# Installation
Install the React bindings (the framework-agnostic core is pulled in automatically):
```bash
npm i @kanunilabs/pivotgrid-react
```
(Any package manager works — `yarn add` / `pnpm add` too.)
The React package declares its UI libraries as peer dependencies. npm 7+ installs
these automatically; if your setup doesn't, add the ones your app doesn't already have:
```bash
npm i @dnd-kit/core lucide-react zustand
```
**Not using React?** `@kanunilabs/pivotgrid` is the same grid over the same
engine, driven by a plain `createPivotGrid()` call — no framework peer at all
(`@dnd-kit`, `lucide-react` and `zustand` are not needed). The
[JavaScript guide](https://kanunilabs.com/docs/pivotgrid/javascript) covers it end to end, including
the Enterprise edition via `createEnterprisePivotGrid()`.
```bash
npm i @kanunilabs/pivotgrid
```
## Styles
Import the stylesheet once, near your app root:
```ts
import '@kanunilabs/pivotgrid-react/styles.css';
```
The JavaScript package ships the same stylesheet as `@kanunilabs/pivotgrid/styles.css`.
## The Web Worker: nothing to configure
The engine runs its aggregation in a Web Worker, and the worker is compiled into
the package and started from a Blob URL. There is no file to copy, no bundler
recipe and nothing to import — Vite, webpack, Next.js and Parcel all work as
they are. The same is true of the import and export workers.
### If you have a strict Content-Security-Policy
A Blob worker needs `worker-src` to allow `blob:`:
```
Content-Security-Policy: worker-src 'self' blob:
```
If you cannot change the policy, the grid falls back to loading the worker file
from `node_modules` on its own — and where that fails too, point `workerUrl` at
a copy you serve. The file ships in the package either way.
**Vite** — import the worker as an asset URL:
```jsx
import workerUrl from '@kanunilabs/pivotgrid-core/dist/pivot.worker.js?url';
```
**Next.js and other bundlers** — copy `pivot.worker.js` from
`node_modules/@kanunilabs/pivotgrid-core/dist/` into a served path (e.g.
`public/`) and point at it:
```jsx
```
The same route is worth taking if you would rather serve the worker from a CDN,
so it caches separately from your app bundle.
## TypeScript
Types ship with the packages — no `@types/*` needed. The public API is fully typed,
including `PivotField`, `PivotLocale`, and the component props.
Next: [render your first grid](https://kanunilabs.com/docs/pivotgrid/react).
---
Source: https://kanunilabs.com/docs/pivotgrid/nextjs
# Next.js setup
The grid renders on the client and uses a Web Worker. Since the worker is
compiled into the package, there is now exactly **one** step.
## Render client-side
The grid touches browser APIs, so load it without SSR:
```tsx
'use client';
import dynamic from 'next/dynamic';
const Grid = dynamic(() => import('./MyGrid'), { ssr: false });
```
That is the whole setup:
```tsx
```
## What happened to the copy-worker step?
It is gone. Next's bundler cannot statically resolve a worker URL inside
`node_modules`, so this page used to ask you to copy `pivot.worker.js` into
`public/` and pass `workerUrl`. The worker is now embedded in the package and
started from a Blob URL, which no bundler has to resolve — so the script, the
`predev`/`prebuild` hook and the prop all disappear.
Existing `workerUrl` code keeps working and still wins when it is set: it is the
right answer for a Content-Security-Policy without `worker-src blob:`, or when
you would rather serve the worker from a CDN. See
[Installation](https://kanunilabs.com/docs/pivotgrid/installation#if-you-have-a-strict-content-security-policy).
See it working in the [playground](https://kanunilabs.com/playground).
---
Source: https://kanunilabs.com/docs/pivotgrid/browser-support
# Supported browsers
The PivotGrid targets the current versions of the evergreen browsers:
**Chrome, Edge, Firefox and Safari**. Internet Explorer and the original,
pre-Chromium Edge are not supported: the packages are compiled to modern
JavaScript (ES2022 and later) and ship no polyfills.
This applies to every PivotGrid package: React and plain JavaScript, Community
and Enterprise, the npm builds and the `
```
The engine and its Web Worker are bundled in — the worker starts from a Blob
URL, so there is no file to serve. If a strict Content-Security-Policy blocks
`blob:` workers, pass `workerUrl` as described under
[Installation](https://kanunilabs.com/docs/pivotgrid/installation#if-you-have-a-strict-content-security-policy).
## Enterprise edition
`@kanunilabs/pivotgrid-enterprise` wraps the Community renderer — it does not
fork it — and fills the sockets it opens: the no-code calculated-field
designer, the visual prefilter builder, the drill-down modal, an eight-type
chart panel and styled Excel & PDF export. It is installed from the
[private registry](https://kanunilabs.com/docs/pivotgrid/registry) with your licence.
```ts
import { createEnterprisePivotGrid } from '@kanunilabs/pivotgrid-enterprise';
import '@kanunilabs/pivotgrid/styles.css'; // no separate Enterprise stylesheet
const grid = createEnterprisePivotGrid(host, {
licenseKey: 'KLAB1.…',
// everything createPivotGrid takes, plus — all default ON:
calculatedFields: true, // footer calculator → the designer
prefilter: true, // footer filter → the AND/OR builder
drillDown: true, // click a value cell → the source rows
charts: true, // footer chart button → the panel, hidden until pressed
chartType: 'column',
chartStacking: 'grouped', // or 'stacked' | 'normalized' (100%)
chartFormat: { title: 'Revenue', legend: 'bottom', dataLabels: true },
onChartFormatChange: (format) => save(format),
chartFloating: false, // true (or { x, y, width, height }) opens it as a window
drillDownPageSize: 50,
data,
initialFields: fields,
});
grid.openCalculatedFieldDesigner(); // or (fieldId) to edit one
grid.openPrefilterBuilder();
grid.openDrillDown({ records, rowPath: ['EMEA'] });
grid.toggleChart();
grid.setChartType('pie');
grid.setChartStacking('stacked');
grid.setChartFormat({ title: 'Q3', valueAxisTitle: 'Revenue' });
grid.isEnterpriseLicensed();
```
Without a valid key the grid **still works**, watermarked — your app never
breaks on a licensing problem. One licence key opens both Enterprise packages,
React and plain JavaScript alike; see
[Enterprise & licensing](https://kanunilabs.com/docs/pivotgrid/enterprise).
`drillDown` **chains** your own `onCellClick` rather than replacing it: your
handler runs first and sees every click, whether or not a modal follows. Pass
`drillDown: false` to keep the click for yourself and call `openDrillDown` when
you want the dialog.
### Charts without a charting library
The chart panel draws its types — column, bar, line, area, pie, donut,
dot plot, scatter, bubble, radar, combination, waterfall, funnel, treemap,
heatmap, range, sunburst and histogram — as SVG the package writes itself, and
downloads the chart as PNG or SVG with its title. The histogram counts the
categories in each band of the first visible measure; a picker beside it asks
for about 5, 10, 20 or 50 bands of one round width, and the choice is kept in
the chart's state (`bins`, absent at the default 10).
Scatter and bubble put one point per category at (first measure, second
measure); the bubble's size is the third measure. There is no charting
dependency, for a reason that decided itself: a library marked *external*
survives into the script-tag bundle as a bare `import('…')` that a classic
script cannot resolve, and a second `
```
The package looks for `globalThis.ExcelJS` and `globalThis.jspdf` first and
falls back to `import()` — a page that loads the tags in any order works, and a
click that lands before a tag has loaded is not remembered as a failure. If a
peer is nowhere, the export does nothing and the console says which tag to add.
A missing peer never throws into your click handler.
The Enterprise UMD bundles the Community renderer and the licensing core
(global `KanuniLabsPivotGridEnterprise`), so one stylesheet and one script tag
are enough.
### Where it stops
The [Enterprise parity ledger](https://kanunilabs.com/docs/pivotgrid/api/javascript-enterprise/variables/ENTERPRISE_PARITY_LEDGER)
grades every paid surface against the React edition and is exported from the
package, so the limits below are checked, not remembered:
- **PDF has no embedded Unicode font** (shared with React). Labels the built-in
font cannot draw fall back to English rather than rendering as glyph soup;
cell text outside Latin-1 is a known limit.
- **Very large pivots export as SpreadsheetML (`.xls`)**, above 150,000 engine
cells, and that path carries no conditional formatting, freeze pane or
outline levels (shared with React). Unlike React, the console says so when
the switch happens.
- **Chart series colours follow the theme by default here**, React's
`ChartFactory` only with `palette="theme"`; the chrome — axes, gridlines,
labels — follows it in both.
- **A prefilter on a date field with a `groupInterval`** compares the group key,
not the date, in both editions. The builder therefore offers such a field a
text box and no ordering operators, so what you type is what is compared.
## What is the same, what is different
| | React | JavaScript |
| --- | --- | --- |
| Engine, stylesheet, themes, worker | shared | shared |
| Config / props | `` | `createPivotGrid(host, props)` |
| Enterprise surfaces | render props | plain callbacks (sockets) |
| Cell click payload | synthetic event | native `MouseEvent` |
| Imperative API | `ref` → controller | `grid.controller` |
| Charts | `usePivotChartSync` + recharts | `createChartSync` + built-in SVG panel |
| Server-side rendering | renders markup | constructs on mount (needs a `document`) |
| Enterprise | `` | `createEnterprisePivotGrid()` |
SSR is the one honest difference: the JavaScript renderer produces DOM, so it is
created when the host element exists. Everything else is held to parity by the
ledgers and conformance suites that run on every commit.
---
Source: https://kanunilabs.com/docs/pivotgrid/features
# Features guide
Everything below is available in the **free** `@kanunilabs/pivotgrid-react`
and `@kanunilabs/pivotgrid` packages 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`:
```tsx
{ id: 'revenue', dataField: 'revenue', caption: 'Revenue', dataType: 'number',
area: 'data', summaryType: 'sum' }
```
Supported: `sum`, `count`, `avg`, `min`, `max`, `distinct` (distinct count) and
`custom`. The built-in summaries run 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. It runs
once per cell, subtotals and grand totals included, over that cell's own rows:
```tsx
{ id: 'priceRange', dataField: 'price', caption: 'Price Range', dataType: 'number',
area: 'data', summaryType: 'custom',
calculateCustomSummary: (o) => {
if (o.summaryProcess === 'start') { o.lo = Infinity; o.hi = -Infinity; }
if (o.summaryProcess === 'calculate') {
o.lo = Math.min(o.lo, o.value);
o.hi = Math.max(o.hi, o.value);
}
if (o.summaryProcess === 'finalize') o.totalValue = o.hi - o.lo;
} }
```
Rows arrive in source order, and anything you store on the options object
stays there between phases. The worker groups the rows; your function runs on
the main thread, because a function cannot be sent to a worker.
To reuse one summary across fields, register it and refer to it by name. The
function receives the cell's values:
```tsx
import { PivotEngineRegistry } from '@kanunilabs/pivotgrid-core';
const registry = new PivotEngineRegistry();
registry.registerAggregator('median', (values) => {
const s = [...values].sort((a, b) => a - b);
const m = s.length >> 1;
return s.length % 2 ? s[m] : (s[m - 1] + s[m]) / 2;
});
// pass `registry` to the grid, then:
{ id: 'medianPrice', dataField: 'price', caption: 'Median Price', dataType: 'number',
area: 'data', summaryType: 'custom', calculateCustomSummary: 'median' }
```
A name that is not registered stops the grid with an error that names it,
instead of quietly showing a sum.
## Summary display modes
Transform an aggregated value **after** it is computed with `summaryDisplayMode`:
```tsx
{ 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 |
| `percentOfParentRowTotal` | % of its parent row group, in the same column |
| `percentOfParentColumnTotal` | % of its parent column group, in the same row |
| `runningTotal` | a cumulative running total |
| `difference` | the difference from the previous value |
| `percentDifference` | the % difference from the previous value |
The percent modes are computed on a 0–100 scale. A field without a `format`
of its own shows them as the locale's percent with at most two decimals —
`37.51%` in English, `%37,51` in Turkish — on screen, in CSV and PDF, and in
Excel (number format `0.00"%"`). A `format` on the field replaces this; the
value it receives is still on the 0–100 scale.
### % of the parent total
With nested row fields, `percentOfParentRowTotal` answers "what share of its
group is this?" rather than "what share of everything?":
```tsx
// Rows: Region > Country. Each country shows its share of its region,
// each region its share of the grand total.
{ id: 'share', dataField: 'revenue', caption: 'Share of parent', dataType: 'number',
area: 'data', summaryType: 'sum', summaryDisplayMode: 'percentOfParentRowTotal' }
```
| Row | Revenue | Share of parent |
|---|---|---|
| EMEA | 100 | 50% (of the 200 grand total) |
| EMEA › Germany | 60 | 60% (of EMEA) |
| EMEA › France | 40 | 40% (of EMEA) |
| APAC | 100 | 50% |
| Grand total | 200 | 100% |
- The parent is the **immediate** parent group on that axis. A top-level group's
parent is the grand total, and the grand total itself shows 100%.
- Each column is divided separately: Germany in 2025 is a share of EMEA in 2025.
- A collapsed group shows its own share of its parent; its children are not
computed until it is opened.
- Hiding subtotal rows (`showRowTotals: false`) changes nothing: the parent's
total is still what the share is taken of.
- `percentOfParentColumnTotal` is the same rule across nested column fields,
within each row.
- A parent that totals 0 shows 0.
## Grouping
Group `date` and `number` fields with `groupInterval`:
```tsx
{ id: 'orderDate', dataField: 'orderDate', caption: 'Month', dataType: 'date',
area: 'column', groupInterval: 'month' }
```
Date intervals: `year`, `quarter`, `month`, `week`, `day`. `quarter`, `month`
and `day` gather the same period of every year into one member (every January
together); nest them under a `year` field to keep years apart.
`week` is the ISO 8601 week: weeks start on Monday, and week 1 is the week
holding the year's first Thursday. Each member is labelled with its ISO
week-year and number, so years never mix and the members sort in calendar
order:
```tsx
{ id: 'orderWeek', dataField: 'orderDate', caption: 'Week', dataType: 'date',
area: 'row', groupInterval: 'week' }
// 2026-12-28 → "2026-W53", 2027-01-01 → "2026-W53", 2027-01-04 → "2027-W01"
```
Like the other intervals, a date is read as the local calendar date it falls on.
Numbers take a bucket width as a number: `groupInterval: 100` groups into
0–99, 100–199, and so on. The strings `numeric` and `custom` have no effect, and
the grid reports them when the fields are set.
## Sorting
Sort a field's own values, or sort a row/column dimension by a measure:
```tsx
// 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
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":
```tsx
// 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 — unless you ask for the remainder:
```tsx
{ ...region, topN: 10, topNShowOthers: true, topNOthersCaption: 'Other regions' }
```
`topNShowOthers` folds everything below the cut into one extra row (or column)
carrying its own figures.
> **One honest limit: `avg` and `distinct` measures are left EMPTY in that
> row.** The remainder is combined from cells that are already aggregated,
> which is exact for `sum`, `count`, `min` and `max` — but an average needs the
> row counts behind each value and a distinct count needs the values
> themselves, and neither survives aggregation. Rather than print a plausible
> wrong number in a total row, those cells stay blank and the grid reports
> which measures they were through your `onError` handler.
>
> 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 before it existed.
## Value and label filters
Two more ways to choose which members of a row or column field stay — the
ones a spreadsheet pivot calls value and label filters. By the member's
**total**:
```tsx
// Regions that sold over a million, and nothing else.
{ id: 'region', dataField: 'region', caption: 'Region', dataType: 'string', area: 'row',
valueFilter: { dataField: 'revenue', operator: '>', value: 1_000_000 } }
```
`operator` is `>`, `>=`, `<`, `<=`, `=`, `!=`, `between` or `notBetween`; the
two range operators take a second bound in `value2` (inclusive, either order).
`dataField` is the **id** of a field in the data area, and the test reads its
summary — before any `summaryDisplayMode`, so a percent-of-total view still
filters on the amount.
By the member's **caption**:
```tsx
// Cities whose name starts with "an" — Ankara and Antalya.
{ ...city, labelFilter: { operator: 'startsWith', value: 'an' } }
```
`operator` is `equals`, `notEquals`, `contains`, `notContains`, `startsWith` or
`endsWith`. The match ignores case unless you set `caseSensitive: true`, and it
lowercases in the grid's `locale`, so in Turkish "iz" matches "İzmir". A blank
member reads as empty text.
They run per level, after the sort and **before** `topN`, so the two combine
the way the sentence reads: `valueFilter` over a million plus `topN: 3` is "the
top three of those over a million". As with Top-N, **the totals keep counting
every member** — the subtotals and the grand total do not shrink to the rows
on screen. A parent whose members are all filtered out stays as one row with
its own total. To leave rows out of the figures themselves, filter the source
data instead, with `layout.prefilter`.
Both filters are kept in the saved state, so a reader's choice survives a
reload, and a change through `controller.updateField` recomputes straight away.
A filter that cannot be honoured — a `dataField` not in the data area, an
unknown operator, a missing `value2` — leaves the level unfiltered and is
reported through your `onError` handler, rather than quietly showing nothing.
Readers set them on screen too. The menu of a row or column field (right-click
the field, or Shift+F10 on it) has **Filter by condition…**, and so does the
button at the bottom of its member list. The dialog has one line for the
caption and one for a total, with the data field to test. **None** removes that
line. A field with a condition shows the same filled filter icon as one with
members unticked. The dialog keeps a `caseSensitive: true` your code set; it
does not offer the switch itself.
## Filtering
Header filters are built in — click a row/column field header to include or
exclude values. You can also seed filters declaratively:
```tsx
{ 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](https://kanunilabs.com/docs/pivotgrid/enterprise).
## 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`:
```tsx
{ 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.
### The other three rule types
`PivotConditionRule.type` offers four kinds, and since `@kanunilabs/pivotgrid-core`
1.3.0 all four render. The three scale-based ones take their configuration in the same
`style` object:
```tsx
conditionalFormatting: [
// A colour scale across the measure's range.
{ type: 'colorScale', style: { colors: ['#f8fafc', '#2563eb'] } },
// An in-cell bar, painted UNDER the value so the number stays readable.
{ type: 'dataBar', style: { color: '#3b82f6', negativeColor: '#ef4444' } },
// A glyph before the value. Cut points are in the measure's own units;
// without them the range is split evenly between the icons.
{ type: 'iconSet', style: { icons: ['arrow-down', 'minus', 'arrow-up'], thresholds: [0, 100] } },
]
```
Two things are worth knowing:
**Totals are excluded from the range.** A grand total sits an order of
magnitude above the cells beside it; letting it set the top of a colour scale
would push every real value into the first stop — correct, and useless.
**Only `colorScale` survives an export.** A spreadsheet cell in our writers
carries a fill colour, so a colour scale exports as that colour. A bar and an
icon have no cell-level equivalent, so an exported file keeps the value without
them — and setting one of those rules says so through your `onError` handler
when the fields are set, once per message.
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
Grand totals, subtotals and the row-header shape are controlled by the `layout`
prop (`PivotLayoutState`):
```tsx
```
### The coarse form
There is a second, coarser spelling of the same switches — one option
per axis pair instead of one per axis:
```tsx
layout={{
showGrandTotals: 'both', // → showRowGrandTotals + showColumnGrandTotals
showSubTotals: 'both', // → showRowTotals + showColumnTotals
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.
### Column subtotals
Expanding a column group adds a **subtotal column** carrying that group's own
figure, beside its children — the column-axis twin of the subtotal rows. It
follows `totalsPosition`, so `'top'` puts it before the children.
```tsx
layout={{ showColumnTotals: true, totalsPosition: 'bottom' }}
```
It is **on by default**, like `showRowTotals`. Set `showColumnTotals: false`
for the column set you had before this landed.
> **New in 1.3.0.** Before it the option was accepted and reported as
> unimplemented through `onError`. Two things are worth knowing about how it
> works: the figure is not a second aggregation pass — the engine already
> totals every visible node, so the subtotal column addresses the group's own
> value — and it appears in **exports** too, because the exporters derive their
> columns from the same list the screen does.
>
> All four coarse options were declared and read by nothing until 2026-08-27,
> and this page used two of them in the example above.
### Column widths
Row-header columns start at 150px and data columns at 95px. Long member names
or large totals need more, and `defaultColumnWidths` sets the starting width
for every column of a kind, however many there turn out to be:
```tsx
```
`rowHeader` applies to each row-header column — one per row field in the
tabular layout, the single column in the tree layout — and `data` to each data
column. Either can be left out. The frozen row-header block is still kept
narrow enough to leave room for the data, so in a small container a wide
`rowHeader` is painted narrower than asked.
A width the user drags beats the default, and is saved with the layout when
`stateStoring` is on. To set one column from code, use the `ref`:
```tsx
const grid = useRef(null);
// Keys: `row-` for row-header column i (0 is the outermost row field),
// `leaf-` for data column i, counted left to right with totals included.
grid.current?.setColumnWidths({
...grid.current.getState().columnWidths,
'row-0': 320,
});
```
`setColumnWidths` replaces the whole map, so spread the current one to change a
single column; a column left out goes back to `defaultColumnWidths`. In the
JavaScript edition the option is the same key on `createPivotGrid`, and the
method is `grid.controller.setColumnWidths`.
## 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:
```tsx
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](https://kanunilabs.com/docs/pivotgrid/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:
```tsx
{ 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`:
```tsx
```
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`:
```tsx
```
`surfaceHeader` and `textHeader` colour the panels — the filter area, the field
areas, the footer, dialog headers — and, unless you say otherwise, the column
headers too. For a dark header band over light panels, give the band its own
pair:
```tsx
theme={{ surfaceHeader: '#f1f5f9', headerBackground: '#1e293b', headerText: '#f8fafc' }}
```
Try the presets live in the [Theming playground](https://kanunilabs.com/playground/pivotgrid/theming).
### Colour schemes
Both packages ship 36 ready-made colour schemes, light and dark, each one a
small stylesheet next to the grid's own. Browse them, or make your own from any
of them, in the [Theme Builder](https://kanunilabs.com/theme-builder) — it previews the pivot table
and exports the stylesheet. Import the one you want and name it by its id:
```tsx
import '@kanunilabs/pivotgrid-react/styles.css';
import '@kanunilabs/pivotgrid-react/themes/graphite.css';
```
The id is all the grid needs: it marks the grid root with `data-theme`, and
every popup it opens (field menus, the filter list, dialogs, the field chooser,
the drill-down) carries the same mark, so the scheme reaches all of them.
`themes/all.css` holds every scheme at once, for an app that lets its users
choose. The plain JavaScript package has the same files:
```ts
import '@kanunilabs/pivotgrid/styles.css';
import '@kanunilabs/pivotgrid/themes/graphite.css';
createPivotGrid(host, { data, initialFields: fields, theme: { id: 'graphite' } });
```
A scheme and your own colours combine: anything you set in the `theme` object
wins over the scheme, so a scheme can be the starting point for a brand colour:
```tsx
```
Without an id, or with an id no imported stylesheet knows, the grid keeps its
default look. A chart drawn with `ChartFactory` beside the grid follows the
scheme when you wrap it in `` and
pass `palette="theme"`, which derives the series colours from the scheme's
accent.
## Density
`density` sets the row rhythm. It changes no colours, so it combines with any
theme or scheme:
```tsx
```
| `density` | Rows | Also |
|---|---|---|
| `'compact'` | 24px | Type one size smaller; for screens that need the most rows. |
| `'normal'` (default) | 28px | The grid as it has always looked. |
| `'comfortable'` | 36px | Larger targets on the toolbar buttons. |
Header rows follow the same height. Menus, the filter list and dialogs keep
their size in every density. Changing the prop repaints in place, without
re-running the pivot engine. In the plain JavaScript package, pass `density` in
the config and switch it later with `grid.setDensity('comfortable')`.
## Localization & RTL
Twelve locales ship in the box; `ar` flips the layout to RTL automatically. See
the [Quick start](https://kanunilabs.com/docs/pivotgrid/react#localization) and the
[Localization playground](https://kanunilabs.com/playground/pivotgrid/localization).
## Performance
Built-in aggregation runs in a Web Worker over columnar storage with a
trie-based grouper, so the UI stays responsive on **a million rows** — try the
[Big Data Performance playground](https://kanunilabs.com/playground/pivotgrid/performance) (100 →
1,000,000 rows). The worker is compiled into the package and started from a Blob URL, so
there is nothing to configure in any bundler:
```tsx
```
A strict Content-Security-Policy is the one case that still needs attention —
see [Installation](https://kanunilabs.com/docs/pivotgrid/installation#if-you-have-a-strict-content-security-policy).
## Live updates
A dashboard that receives rows as they change does not have to hand the grid a
new `data` array every time. `applyTransaction` adds, updates and removes rows
without a rebuild: only the rows in the transaction are encoded and sent to the
worker, which regroups them and recomputes the groups they touched.
```tsx
const grid = useRef(null);
// on every message from the feed
grid.current?.applyTransaction({
update: [{ id: 'T-1042', desk: 'Rates', pnl: 18250 }],
add: [{ id: 'T-1043', desk: 'FX', pnl: -400 }],
remove: ['T-0999'],
});
```
In JavaScript the call is on the grid itself, `grid.applyTransaction({ … })`,
with `rowKey` in the `createPivotGrid` options.
- **Rows are matched by `rowKey`** — a field name, or a function returning a
string or number. Without it rows are matched by object identity: update with
the very object that is in your data, changed in place, and remove by passing
rows rather than keys.
- An update for a key the grid does not have is ignored, a remove wins over an
update of the same row, and added rows go to the end — the same rules as the
DataGrid's transactions.
- It returns what it applied, `{ added, updated, removed }`, and
`getRawData()` returns the new rows at once; the recomputed pivot follows a
moment later (the `engineCompleted` event, or the next render).
- The result is the pivot a rebuild from the new rows would show: the same
groups, totals, filters, sorting and custom summaries. The totals a change
touches are summed again from their rows, not adjusted by the difference, so
decimal totals do not drift over thousands of updates.
- Re-rendering with the **same** `data` array keeps the changes. A **new**
array replaces every row, as before.
- A transaction that arrives while the grid is still building, or while a
field is being moved, is applied as soon as that finishes — in order, without
starting the build again.
Measured on 2026-10-03 over a million rows (three grouping fields, two
measures; a transaction of 10 updates, 5 removes and 10 adds, in Node 24): about
17 ms of worker time per transaction against 230–290 ms to rebuild there, and
about 35 ms on the main thread against 160 ms to encode every row again. Most
of that main-thread time is finding the changed rows by key, which is one pass
over all rows.
Two things to know. The first transaction on a grid prepares the engine for
the ones after it with one regrouping of the stored data (about 0.3 s at a
million rows), without encoding anything again. And a value that no longer
occurs in any row stays in the engine's dictionary until the next full build —
a new `data` array, `setData` or **Refresh**.
### Feeds that tick many times a second
Every `applyTransaction` finds its rows by key in one pass over all rows and
sends the worker its own message. A feed that calls it a hundred times a second
pays that a hundred times for one picture. Queue the ticks instead:
```tsx
// on every message from the feed
grid.current?.applyTransactionAsync({ update: [tick] });
```
Queued transactions are merged and applied together once `asyncTransactionWait`
milliseconds (default 50) have passed since the first of them. The optional
second argument is a callback that runs when its batch has been applied.
`flushAsyncTransactions()` applies the queue at once, and
`controller.getPendingAsyncTransactionCount()` says how many transactions wait.
- **The merged batch leaves the rows that applying each transaction in order
would.** An add followed by an update is one add of the latest row; an add
followed by a remove leaves nothing; the last update of a row wins; a remove
drops an earlier update. Inside one transaction, "remove `7`, add `7`" is a
replacement.
- **The order of your calls is kept.** A synchronous `applyTransaction`,
`setData` or a new `data` array applies the queue first.
- **Destroying the grid drops the queue** without applying it.
In JavaScript the calls are on the grid, `grid.applyTransactionAsync(…)` and
`grid.flushAsyncTransactions()`, with `asyncTransactionWait` in the
`createPivotGrid` options. Measured on 2026-10-08 over a million rows in Node 24:
finding the rows for 100 single-row ticks took about 1.7 s one by one and about
24 ms merged, and the worker received one message instead of a hundred.
## Toolbar customization
Show or hide individual footer controls with the `toolbar` prop:
```tsx
```
---
Looking for calculated fields, charts or drill-down? Those are in the
[Enterprise](https://kanunilabs.com/docs/pivotgrid/enterprise) package. For every prop and type, see
the [API Reference](https://kanunilabs.com/docs/pivotgrid/api).
## 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
[hello@kanunilabs.com](mailto:hello@kanunilabs.com) or use the
[contact form](https://kanunilabs.com/contact); license holders can also open a
[support ticket](https://kanunilabs.com/dashboard/support). Every message is read and answered.
---
Source: https://kanunilabs.com/docs/pivotgrid/faq
# Frequently asked questions
## Is the free tier open source?
No. The free packages (`@kanunilabs/pivotgrid-core`, `@kanunilabs/pivotgrid-react`,
`@kanunilabs/pivotgrid`) are
**free to use** but proprietary — you may install and ship them in your apps, but the
source is not published and may not be redistributed on its own. See the LICENSE in each
package.
## What's the difference between free and Enterprise?
**Community** (the free `@kanunilabs/pivotgrid-react` and `@kanunilabs/pivotgrid` packages) is a fully usable pivot
table: pivoting, grouping, sorting, filtering, conditional formatting, theming,
localization, saved views, broad import, and CSV/JSON/clipboard/print export.
**Enterprise** adds advanced analytics and BI-grade output — calculated fields, the
prefilter builder, drill-down to source records, live chart integration, and styled
Excel/PDF export — delivered via a private registry with a signed license key.
## How does licensing work?
Each license is a signed key (`KLAB1….`) verified **offline** in the browser (no network
needed) using an embedded public key. Keys are bound to your domains and tier. Manage
domains in your [dashboard](https://kanunilabs.com/dashboard/licenses) — changing them re-issues the key.
## Which frameworks are supported?
React (18 & 19), and plain JavaScript with no framework at all.
`@kanunilabs/pivotgrid-react` is the React binding.
[`@kanunilabs/pivotgrid`](https://kanunilabs.com/docs/pivotgrid/javascript) is the same grid built straight into
the DOM: one `createPivotGrid(element, config)` call, no component tree, and a UMD build so
a `
```
The engine is bundled in; the optional export peers (exceljs, jsPDF) are not,
and degrade with an actionable error if an export needs them.
## Enterprise edition
`@kanunilabs/datagrid-enterprise` wraps the Community grid — it does not fork
it — and adds the paid surface: cell / batch / row / form / popup editing
with validation and undo, range selection, fill handle and clipboard,
master-detail, tree data, cell spans and notes, charts and sparklines, the
filter builder, the import wizard, styled Excel/PDF export, status and side
bars. It is installed from the
[private registry](https://kanunilabs.com/docs/datagrid/registry) with your license.
```ts
import { createEnterpriseGrid } from '@kanunilabs/datagrid-enterprise';
import '@kanunilabs/datagrid/styles.css';
import '@kanunilabs/datagrid-enterprise/styles.css';
const grid = createEnterpriseGrid(host, {
licenseKey: 'KLAB1.…',
// everything createGrid takes, plus:
editing: {
mode: 'batch', // 'cell' | 'batch' | 'row' | 'form' | 'popup'
commandColumn: true,
allowAdding: true,
allowDeleting: true,
rules: { product: [{ type: 'required' }] },
onSave: (changes) => api.save(changes),
},
statusBar: true,
sideBar: true,
charting: { crossFilter: true },
filterBuilder: true,
onImport: (rows) => append(rows),
toolbarExcel: true,
toolbarPdf: true,
// styledExport defaults to true — Excel carries the grid's formatting.
});
grid.chartPanel?.open({ categoryColumnId: 'category' });
grid.openFilterBuilder();
grid.isEnterpriseLicensed();
```
Without a valid key the grid **still works**, watermarked — your app never
breaks on a licensing problem. One license key opens both Enterprise
packages, React and plain JavaScript alike; see
[Enterprise & licensing](https://kanunilabs.com/docs/datagrid/enterprise).
The Enterprise UMD build is `dist/datagrid-enterprise.umd.js`
(global `KanuniLabsDataGridEnterprise`); it bundles the Community renderer and
the licensing core, so one script tag is enough.
## What is the same, what is different
| | React | JavaScript |
| --- | --- | --- |
| Engine, stylesheet, themes, worker | shared | shared |
| Config / props | `` | `createGrid(host, props)` |
| Custom cell content | React node | string or DOM `Node` |
| Imperative API | `onReady(controller)` | `grid.controller` |
| Server-side rendering | renders markup | constructs on mount (needs a `document`) |
| Enterprise | `` | `createEnterpriseGrid()` |
SSR is the one honest difference: the JavaScript renderer produces DOM, so it is
created when the host element exists. Everything else is held to parity by the
conformance suites that run on every commit.
---
Source: https://kanunilabs.com/docs/datagrid/faq
# FAQ
## Is the Community edition really free?
Yes. `@kanunilabs/datagrid-react` and `@kanunilabs/datagrid-core` are free to
use, including commercially, under the KanuniLabs Community License. They are
proprietary — the compiled packages are distributed, the source is not.
## Why is `rowKey` required?
Selection, focus and future live updates are keyed by row identity. Using array
position instead would break the moment rows sort or filter — a selected row
would appear to jump to a different record. Requiring `rowKey` makes that class
of bug impossible.
## Do I need the Web Worker?
No. Below ~20,000 rows the grid stays on the main thread because that is
genuinely faster. Above it, the offload happens automatically when a `Worker`
global is available; if not, the grid falls back to the synchronous path.
## My grid renders but feels slow on a big dataset
Most likely the worker did not start, so filtering and sorting are running on
the main thread. The worker ships inside the package and starts from a Blob URL,
so the usual cause is a Content-Security-Policy that forbids `blob:` workers —
see [Installation → The Web Worker](https://kanunilabs.com/docs/datagrid/installation#the-web-worker-nothing-to-configure).
In development the grid warns once in the console with the exact cause.
## Does it work with Next.js / SSR?
Yes, with no extra setup. The grid renders on the server without a worker or DOM
measurement and hydrates normally; popups are created on mount, so nothing is
portaled during SSR. The worker is embedded in the package, so Next.js needs no
file copied into `public/` — see [SSR](https://kanunilabs.com/docs/datagrid/ssr).
## Why does my CSV open with broken characters in Excel?
It shouldn't — exports are written with a UTF-8 BOM precisely because Excel
otherwise reads the file as ANSI. If you disabled it with `bom: false`, that's
the cause.
## Can I export to Excel?
Yes, in Enterprise (`EnterpriseDataGrid`, or `createEnterpriseGrid` in plain
JavaScript), with the optional `exceljs` peer installed (`npm i exceljs`). It
is loaded lazily, so apps that only use CSV don't pay for it. CSV export is in
the free Community grid.
## Does export include everything or just the visible page?
Everything in the current result — all filtered and sorted rows, not just the
page on screen. What it *does* respect is what you see per row: formatted
values and visible columns only.
## Can I edit cells?
Not in the Community edition. Editing is the anchor feature of Enterprise:
cell, batch, row, form and popup modes, validation, per-row CRUD handlers,
undo/redo, range selection, the fill handle and paste. See
[Editing](https://kanunilabs.com/docs/datagrid/editing).
## Does it support multi-level grouping?
Community ships single-level grouping. Multi-level grouping and per-group
aggregates are Enterprise.
## Is it accessible?
Yes — a full WAI-ARIA grid implementation: `role="grid"` with row/column
indices, virtual focus via `aria-activedescendant`, live-region announcements,
and a roving tabindex so the header is one tab stop instead of one per column.
## Which frameworks are supported?
React, and plain JavaScript with no framework at all.
`@kanunilabs/datagrid-react` is the React binding.
[`@kanunilabs/datagrid`](https://kanunilabs.com/docs/datagrid/javascript) is the same grid built
straight into the DOM: one `createGrid(element, config)` call, no component
tree, and a UMD build so a `