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
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 exposesonAddCalculatedField/onEditCalculatedField,onOpenPrefilterandonToggleChart. 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 hookusePivotChartSyncas 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
ChartFactoryonly withpalette="theme"; the chrome — axes, gridlines, labels — follows it in both. - A prefilter on a date field with a
groupIntervalcompares 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 | <BasePivotGrid {...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 | <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.