# 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 `