Documentation

JavaScript

The PivotGrid does not need React. @kanunilabs/pivotgrid is a second renderer over the same engine (@kanunilabs/pivotgrid-core) and the same stylesheet — aggregation in the Web Worker, drag-and-drop between areas, filters, sorting, expand/collapse, saved layouts and theming behave identically, and a parity ledger checked in CI records, surface by surface, where the two renderers agree and where they deliberately do not.

npm i @kanunilabs/pivotgrid

A pivot in one call

import { createPivotGrid, type PivotField } from '@kanunilabs/pivotgrid';
import '@kanunilabs/pivotgrid/styles.css';
import '@kanunilabs/pivotgrid/themes/graphite.css'; // optional: one of 36 colour schemes

const data = [
  { region: 'EMEA', quarter: 'Q1', revenue: 1200 },
  { region: 'EMEA', quarter: 'Q2', revenue: 980 },
  { region: 'APAC', quarter: 'Q1', revenue: 1500 },
];

const fields: PivotField<(typeof data)[number]>[] = [
  { id: 'region', dataField: 'region', caption: 'Region', dataType: 'string', area: 'row' },
  { id: 'quarter', dataField: 'quarter', caption: 'Quarter', dataType: 'string', area: 'column' },
  { id: 'revenue', dataField: 'revenue', caption: 'Revenue', dataType: 'number', area: 'data', summaryType: 'sum' },
];

const grid = createPivotGrid(document.getElementById('host'), {
  gridId: 'sales',
  data,
  initialFields: fields,
  locale: 'tr',            // one of 12; 'ar' switches to RTL
  theme: { id: 'graphite' }, // the scheme imported above
  density: 'compact',      // 24px rows; 'normal' (28) is the default
});

// Later, with your own values:
// grid.setData(nextRows);          // replace every row
// grid.applyTransaction({ add: [row] }); // or change a few (Features guide → Live updates)
// grid.applyTransactionAsync({ update: [tick] }); // a fast feed: queued, applied together
grid.setLocale('de');            // relabels chrome, chart axes and dialogs
grid.setDensity('comfortable');  // 36px rows, repainted in place
grid.controller.expandAll();     // the engine, directly
// grid.destroy();               // when the host goes away
Runs in your browser: installs from npm and starts Vite. Nothing to set up.

The same data and fields as the React quick start — the field vocabulary (area, dataType, summaryType, format, groupInterval) is core's, not the renderer's, so it is identical in both.

The config mirrors the React props. Everything BasePivotGrid takes as a prop, createPivotGrid takes as a key: layout, stateStoring, toolbar, theme, striped, density, defaultColumnWidths, dictionary, rtl, workerUrl, keyboardNavigation, disabled, and the event callbacks (onCellClick, onFieldsChange, onLayoutChange, onExporting, onError, …). One difference in the shape of one payload: onCellClick receives the native MouseEvent, where React hands you a synthetic one.

The overlap is not total, and the renderer tells you where it ends rather than letting you guess: pass a React-only option and it says so in the console, naming what to write instead. The React-only keys are the three render slots (renderCalculatedFieldModal, renderPrefilterBuilder, renderNodeContextMenu), onOpenFullScreenChart (deprecated in React too), children and ref. Each has a plain-callback or Enterprise-package counterpart, and the warning names it.

grid.controller is the engine itself — the same PivotController the React binding exposes. Everything imperative (fields, layout, filters, expansion, export, the cell-click resolver) lives there; see the API reference. The JavaScript surface itself — createPivotGrid and PivotGridVanillaConfig — has its own generated pages.

Three differences worth knowing

  • Row headers are sticky, not a second table. One scroll container; the row-header cells pin themselves with computed insets, and their widths are budgeted against the viewport so the data never disappears behind them. A dragged row-header width is stored as the configured width, so a saved layout means the same thing at any window size.
  • Sockets, not render props. Where React takes renderCalculatedFieldModal, vanilla exposes onAddCalculatedField / onEditCalculatedField, onOpenPrefilter and onToggleChart. When a callback is present the footer draws the matching button; absent, nothing is drawn. The Enterprise package fills all four — an app may fill any.
  • Charts are a subscription. createChartSync(controller, getLocale, options, onSync) is the React hook usePivotChartSync as a plain subscription: 150 ms after the result, the expansion set or the locale changes, your transformer runs. The Community bridge ships no transformer; the Enterprise package does.

Script tag (UMD)

No bundler? The package ships a self-contained build:

<link rel="stylesheet" href="https://unpkg.com/@kanunilabs/pivotgrid/dist/styles.css" />
<script src="https://unpkg.com/@kanunilabs/pivotgrid/dist/pivotgrid.umd.js"></script>
<script>
  const { createPivotGrid } = KanuniLabsPivotGrid;
  createPivotGrid(document.getElementById('host'), { /* the same config */ });
</script>

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.

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 with your licence.

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.

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 <script> tag defines a global that a dynamic import never looks at. Bundling a library would have added it to every customer's page; leaving it out would have made charts unreachable from the one delivery path this package exists for.

With two or more series, column, bar, line and area charts offer a grouped, stacked or 100% stacked layout in the panel's toolbar. A 100% chart reads its axis in percent, and its tooltip shows the real value with the share beside it. The Format button sets a title, axis titles, the legend position, data labels and a linear or logarithmic value scale. Values print with the measure's own format.

The panel also carries what the React chart never had: axis captions in the grid's locale (so the axis agrees with the cells above it), a note whenever maxPoints or maxSeries dropped data, and a visually-hidden data table so a screen reader gets the numbers, not just "graphic".

The chart is plain data too. grid.getChartState() returns whether the panel is open, the type, the stacking, the format and the series hidden in the legend (by series key — the layout decides the series, so a position would name a different one next time); grid.setChartState(state) restores it. A saved report keeps the chart (parts.chart), so loading the report brings it back, the default report included. The controller reports the panel:

grid.controller.on('chartOpened', ({ state }) => {});
grid.controller.on('chartChanged', ({ state, changes }) => {}); // 'type' | 'stacking' | 'format' | 'hiddenSeries' | 'placement'
grid.controller.on('chartClosed', ({ state }) => {});

Pop out in the panel's toolbar lifts the chart off its dock and lays it over the pivot as a window: drag its toolbar to move it, its bottom-right corner to resize it, or use the arrow keys on its two grips (10px a press, 50px with Shift). It stays inside the grid, and Dock puts it back. The window's rectangle is part of the chart state (floating: { x, y, width, height }, no floating key while docked), so a saved report brings it back where it was.

Swap category and series turns the drawn chart around — each series (a column path and measure of the pivot) becomes a category and each row a series — with the same numbers; press it again to swap back. At most 20 rows become series. The swap is part of the chart state (switched: true) and of saved reports. While swapped a click filters nothing, since the categories are no longer rows. (chartIntegrationOptions.inverted is a different, code-only choice: it builds the chart from the pivot's column fields instead of its row fields.)

Unlink freezes the chart: new pivot results (a filter, an expanded row, new data) are kept aside and the chart keeps what it shows; Link draws the latest. The state has unlinked: true while it is; a report restored unlinked takes the pivot's first result and keeps it.

New window draws the same pivot in another window of its own — a copy of the chart that you can switch to another type, stacking or format while the first stays as it was. The windows are numbered ("Chart 2"…), their events carry that number as chartId, and a saved report keeps them (parts.chart.windows). From code:

const win = grid.openChartWindow({ type: 'pie' });  // a copy of the panel's chart, as a pie
grid.getChartWindows();                              // the windows open now
win.update({ stacking: 'stacked' });
win.close();
grid.controller.on('chartOpened', ({ chartId }) => {}); // 'chart-2'…, none for the panel

The series colours follow the grid's theme: the first series is the theme's accent (--pivot-primary) and the rest are derived from it, the way the DataGrid chart colours itself, dark themes included. chartColors takes a list of your own, or 'fixed' for the packaged eight; with your own chartIntegrationOptions.transformer, its colours are kept unless you set chartColors.

With chartCrossFilter: true, a click on a category (a bar, a slice, a cell) filters the pivot to it — every row field of the category, so "EMEA - Q1" keeps EMEA and Q1. A note under the chart says so and has a Clear button; the same click again clears it too, and the fields get back the filters they had before. Off by default: a chart that changes the grid when clicked is a surprise unless you asked for it.

From the keyboard the chart is then one tab stop: the arrow keys (and Home / End) move a focused category, named in a chip at the top of the chart (a live region, so a screen reader says it too) and outlined on the chart; Enter or Space does what the click does. The focus stays on the category when the filter narrows the chart to it, and the chip then adds that it is filtering the pivot.

grid.downloadChart({ format: 'jpeg', width: 1200 }) saves the chart in the format and size you choose ('png', 'jpeg' or 'svg'), and grid.getChartImageDataUrl(options) hands it back as a data: URL instead — to keep with a report or send to a server. One side keeps the chart's proportions; both centre it in the box you give.

Excel and PDF from a script tag

The export peers are optional and never bundled. A bundler resolves them the usual way; a script-tag page provides them as globals, which is what their own UMD builds define:

<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/exceljs.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/jspdf.umd.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/jspdf.plugin.autotable.js"></script>
<script src="https://unpkg.com/@kanunilabs/pivotgrid-enterprise/dist/pivotgrid-enterprise.umd.js"></script>

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

ReactJavaScript
Engine, stylesheet, themes, workersharedshared
Config / props<BasePivotGrid {...props} />createPivotGrid(host, props)
Enterprise surfacesrender propsplain callbacks (sockets)
Cell click payloadsynthetic eventnative MouseEvent
Imperative APIref → controllergrid.controller
ChartsusePivotChartSync + rechartscreateChartSync + built-in SVG panel
Server-side renderingrenders markupconstructs on mount (needs a document)
Enterprise<EnterprisePivotGrid>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.