Documentation
ProPivot is an enterprise-grade, open-source JavaScript pivot-table library that pivots millions of rows entirely in the browser — lightning fast, lightweight, feature-rich, highly configurable, and instantly embeddable in any project. Use it with React, Angular, Vue, any framework, or plain JavaScript.
Overview
ProPivot turns flat data into an interactive pivot grid — grouping, aggregating, formatting, and exporting — with no server round-trips. A framework-agnostic columnar engine sits behind a stable facade, so the same core powers the React component, the Vue component, the Angular component, the plain-script global build, the headless API, an optional Web Worker, and an opt-in DuckDB-WASM accelerator.
Install
Add the package with your favourite manager:
npm install @proteus/propivot
# or
pnpm add @proteus/propivot
# or
yarn add @proteus/propivot
No bundler? Drop in the browser global build and read window.ProPivot — see Plain script.
Step-by-step guide
From zero to an interactive pivot in five steps.
Add a container & the stylesheet
ProPivot renders into any element. Import the CSS once.
import '@proteus/propivot/propivot.css';
<div id="pivot" style="height: 480px"></div>
Describe your data
Pass an array of plain objects as the dataSource. An optional mapping sets captions and field types (e.g. a date hierarchy).
const data = [
{ region: 'West', category: 'Tech', sales: 9000, qty: 12 },
{ region: 'East', category: 'Furniture', sales: 1500, qty: 8 },
// …millions more
];
Create the pivot
A report is just an object: a slice of rows, columns and measures.
import { ProPivot } from '@proteus/propivot';
const pivot = new ProPivot({
container: '#pivot',
report: {
dataSource: { type: 'json', data },
slice: {
rows: [{ uniqueName: 'region' }],
columns: [{ uniqueName: 'category' }],
measures: [{ uniqueName: 'sales', aggregation: 'sum' }],
},
},
});
Make it interactive
Enable the toolbar and the drag-drop field list, format numbers, and colour cells by condition.
new ProPivot({
container: '#pivot',
toolbar: true,
report: {
dataSource: { type: 'json', data },
slice: {
rows: [{ uniqueName: 'region' }, { uniqueName: 'category' }],
columns: [{ uniqueName: 'category' }],
measures: [{ uniqueName: 'sales', aggregation: 'sum', format: 'cur' }],
},
formats: [{ name: 'cur', currencySymbol: '$', decimalPlaces: 0 }],
conditions: [
{ formula: '#value > 5000', measure: 'sales', format: { backgroundColor: '#c5e1a5' } },
],
},
});
React to clicks & export
Listen for events and export to CSV / Excel / PDF / image at runtime.
const pivot = new ProPivot({
container: '#pivot',
report,
cellclick: (cell) => console.log(cell.rows, cell.columns, cell.value),
});
pivot.exportTo('pdf', { filename: 'report', pageOrientation: 'landscape' });
It runs right here
The grid below is the actual library, mounted live from this page:
The report object
Everything is declarative. The most-used keys:
| Key | What it does |
|---|---|
dataSource | Your data: { type: 'json', data, mapping } or a filename (CSV/JSON). mapping sets captions & types. |
slice.rows / slice.columns | Fields to group on each axis — [{ uniqueName: 'region' }]. Supports per-field filter and sort. |
slice.measures | Values to aggregate — { uniqueName, aggregation, format, caption }, or a calculated formula. |
slice.reportFilters | Fields shown as a filter bar above the grid. |
formats | Named number formats referenced by a measure's format. |
conditions | Conditional formatting rules (#value dialect + colours). |
options.grid | Layout (compact / flat / classic), totals, grand totals. |
pivot.setReport(report) to swap the whole view, or pivot.getReport() to read the current one.React
Import the component, pass a report, and bind any event as a prop. The wrapper mounts and disposes the engine for you.
import { Pivot } from '@proteus/propivot/react';
import '@proteus/propivot/propivot.css';
const report = {
dataSource: { type: 'json', data },
slice: {
rows: [{ uniqueName: 'region' }, { uniqueName: 'category' }],
columns: [{ uniqueName: 'category' }],
measures: [{ uniqueName: 'sales', aggregation: 'sum', format: 'cur' }],
},
formats: [{ name: 'cur', currencySymbol: '$', decimalPlaces: 0 }],
};
export function SalesPivot() {
return (
<Pivot
report={report}
toolbar
style={{ height: 520 }}
cellclick={(cell) => console.log(cell.value)}
columnresize={(e) => console.log(e.ref.uniqueName, e.width)}
onReady={(pivot) => console.log('ready', pivot.getReport())}
/>
);
}
Props: report, toolbar, width, height, customizeCell, className, style, onReady, plus every event name as a callback prop. The component re-creates the pivot when the report identity changes.
Vue
Import the component, bind a :report, and listen to any event with @. The wrapper mounts and disposes the engine for you, and swaps the report in place when the bound value changes.
<script setup lang="ts">
import { ref } from 'vue';
import { Pivot, type ProPivot } from '@proteus/propivot/vue';
import '@proteus/propivot/propivot.css';
const report = ref({
dataSource: { type: 'json', data },
slice: {
rows: [{ uniqueName: 'region' }, { uniqueName: 'category' }],
columns: [{ uniqueName: 'category' }],
measures: [{ uniqueName: 'sales', aggregation: 'sum', format: 'cur' }],
},
formats: [{ name: 'cur', currencySymbol: '$', decimalPlaces: 0 }],
});
function onReady(pivot: ProPivot) { console.log('ready', pivot.getReport()); }
function onCellClick(cell: any) { console.log(cell.value); }
</script>
<template>
<Pivot
:report="report"
toolbar
style="height: 520px"
@cellclick="onCellClick"
@columnresize="(e) => console.log(e.ref.uniqueName, e.width)"
@ready="onReady"
/>
</template>
Props: report, toolbar, width, height, customizeCell — plus standard class / style fall-through. Events: every facade event is emitted, including cellclick, columnresize, columnreorder, columnpropertychange, and ready (which carries the ProPivot instance).
<Pivot> is a plain component you import where you use it. It works the same in <script setup>, the Options API, or Nuxt (client-side).Angular
Use the <pro-pivot> component. Bind [report] and listen to outputs.
Template
<pro-pivot
[report]="report"
[toolbar]="true"
[height]="520"
(cellclick)="onCellClick($event)"
(columnresize)="onResize($event)"
(columnreorder)="onReorder($event)"
(ready)="onReady($event)">
</pro-pivot>
Component
import { Component } from '@angular/core';
import { ProPivotComponent } from '@proteus/propivot/angular';
import '@proteus/propivot/propivot.css';
@Component({
selector: 'app-sales',
standalone: true,
imports: [ProPivotComponent],
templateUrl: './sales.component.html',
})
export class SalesComponent {
report = {
dataSource: { type: 'json', data: this.data },
slice: {
rows: [{ uniqueName: 'region' }],
columns: [{ uniqueName: 'category' }],
measures: [{ uniqueName: 'sales', aggregation: 'sum' }],
},
};
onCellClick(cell: any) { console.log(cell.value); }
onResize(e: any) { console.log(e.ref.uniqueName, e.width); }
onReorder(e: any) { console.log(e.fromZone, '→', e.toZone); }
onReady(pivot: any) { console.log('ready', pivot.version); }
}
@Output()s (including columnresize, columnreorder, columnpropertychange) and accepts the same inputs as the core facade.Plain script / any stack
No build step and no framework required — load the global bundle and read window.ProPivot. This is the path for Svelte, SolidJS, jQuery, Web Components, server-rendered pages, or an Android/iOS WebView. (For React, Vue, and Angular, prefer the dedicated wrappers above.)
<link rel="stylesheet" href="propivot.css" />
<script src="propivot.global.js"></script>
<div id="pivot" style="height:480px"></div>
<script>
const pivot = new window.ProPivot({
container: '#pivot',
report: {
dataSource: { type: 'json', data },
slice: {
rows: [{ uniqueName: 'region' }],
columns: [{ uniqueName: 'category' }],
measures: [{ uniqueName: 'sales', aggregation: 'sum' }],
},
},
});
</script>
Feature reference
A quick map of what's in the box. Each one has a focused, runnable example in the Examples gallery.
17 aggregations
Sum, average, median, distinct-count, stdev, plus the positional family — difference, %difference, running totals — along a configurable axis.
Calculated measures
Formula parser & evaluator: sum('sales')/sum('qty'). Mix calculated and native measures freely. Define/edit formulas in the column ▾ → Calculation tab (with a built-in function reference) or via setMeasureFormula(); gate the panel with columnProperties.showType / showFormula / editFormula.
Conditional formatting
Colour cells by threshold with the #value dialect, or bring your own classes via customizeCell.
Display formats
Rich column rendering: data bars, heatmaps, status tags, ratings, dates, currencies and more — per column, identical in exports.
Column UX
Drag to resize, drag to reorder across zones, and a per-column properties panel (type, internal field name, aggregation, heading, formatting, filter). The drag-drop field list is inline by default, or set options.fieldList: { mode: 'icon', placement } for a ⚙ button that opens it in a modal.
Date hierarchies
A date field typed year/quarter/month/day auto-expands into drillable levels.
Sorting & Top-N
Sort members or by a measure, and keep only the top/bottom N.
Five exports
CSV, HTML, real .xlsx, dependency-free .pdf, and an .svg/PNG image — all matching the on-screen grid.
Virtualized grid
Only visible rows hit the DOM, with frozen headers — smooth on high-cardinality data.
Web Worker
Offload aggregation off the main thread behind one async interface, with automatic fallback.
DuckDB-WASM accelerator
Opt-in SQL GROUPING SETS over millions of rows, with result-identical fallback to the built-in engine.
Drill-through
Double-click a cell to see the underlying source rows that aggregate into it.
Accessible & keyboard-first
A full ARIA grid with roving-tabindex arrow-key navigation; resize / reorder / field list also work on touch. More →
Label & value filters
Filter members by text or by a measure threshold (between, >, …), with a member search box. More →
Numeric binning
Group a numeric field into ranges by interval or custom breaks. More →
Range copy
Shift-select a rectangle and copy it as TSV (Ctrl/Cmd+C) into Excel / Sheets. More →
Dark mode, RTL & i18n
Built-in dark theme, right-to-left layout, and localizable chrome. More →
Aggregations
Set a measure's aggregation to any of:
sum · count · distinctcount · average · median · min · max · product · stdevp · stdevs · varp · vars · percent · percentofrow · percentofcolumn · index · difference · %difference · runningtotals
The positional family (difference, %difference, runningtotals) walks a positionalAxis ('rows' or 'columns'). Calculated measures use a formula instead of an aggregation.
Events & API
Common events
| Event | Fires when |
|---|---|
cellclick / celldoubleclick | A cell (or a row/column header) is clicked — payload carries the full row & column tuple, measure and value. |
columnresize | A column is drag-resized — { ref, width }. |
columnreorder | A column is moved between/within zones — { uniqueName, fromZone, toZone }. |
columnpropertychange | A column's heading / aggregation / format / filter changes. |
reportcomplete / ready | The report finished computing & rendering. |
update | The grid re-rendered after any interaction. |
copy | A selected cell range was copied — { rows, columns, text } (the TSV written to the clipboard). |
Useful methods
| Method | Purpose |
|---|---|
setReport(r) / getReport() | Swap or read the whole report. |
refresh() | Recompute & repaint. |
updateData({ data }) | Replace the dataset, keeping the slice. |
loadData(csvOrJsonOrArray) | Load raw data with no predefined mapping — infers the column list & types and builds a starter report. Static ProPivot.inferReport(input) returns the report without rendering. |
exportTo(type, params) | Export to csv / excel / pdf / image / html. |
expandAllData() / collapseAllData() | Expand or collapse every group. |
setLabelFilter(field, op, query) / setValueFilter(field, measure, op, value, value2?) | Apply a label (text) or value (measure-threshold) filter; pass an empty query / clear with setFilter(field, null). |
setBinning(field, binning) | Group a numeric dimension into ranges ({ interval } or { breaks }); null clears it. |
addCalculation({ caption, formula }) | Add a new calculated measure to Values (also the ƒ + button in the field list). Returns the resolved uniqueName. |
setMeasureFormula(ref, formula) | Change a measure's calculation (empty string reverts it to a plain sum). Also editable in the column ▾ → Calculation tab. |
validateFormula(formula) | Pre-flight a formula → { ok, message }, flagging unknown fields / aggregations / functions. |
on(evt, fn) / off(evt, fn) | Subscribe / unsubscribe to events. |
dispose() | Tear down and free resources. |
Accessibility & keyboard
The grid renders as an ARIA grid — rows and cells carry role, aria-rowindex/aria-colindex, aria-sort on sortable headers, aria-expanded on group rows, and aria-selected on values. It is fully keyboard operable with a single tab stop (roving tabindex):
| Key | Action |
|---|---|
Tab | Move focus into / out of the grid (one stop). |
| Arrow keys | Move the focused cell; Home/End jump to row ends, Ctrl+Home/Ctrl+End to the grid corners, PageUp/PageDown by a viewport. |
Enter / Space | Activate the cell — sort a header, expand/collapse a group row, or select a value. |
Shift+F10 | Open the column properties menu, or drill through a value cell. |
Filtering
Filters live on a row/column field's filter, or apply at runtime via the API and the column ▾ → Filter panel (which also has a member search box).
| Type | Example |
|---|---|
| Members | filter: { members: ['West', 'East'] } |
| Top/Bottom-N | filter: { type: 'top', measure: 'sales', quantity: 5 } |
| Label (text) | filter: { type: 'label', labelOperator: 'contains', query: 'North' } — also beginsWith / endsWith / equals / notContains / notEquals. |
| Value (threshold) | filter: { type: 'value', measure: 'sales', operator: 'greaterThan', value: 1000 } — also lessThan / greaterEqual / lessEqual / equal / notEqual / between (with value2). |
pivot.setLabelFilter('region', 'beginsWith', 'No');
pivot.setValueFilter('region', 'sales', 'greaterThan', 100000);
pivot.setValueFilter('region', 'sales', 'between', 50000, 200000);
pivot.setFilter('region', null); // clear
Numeric binning
Group a numeric dimension into ranges with binning on a row/column field — fixed-width interval buckets, or custom breaks. Buckets sort numerically (e.g. "100 - 200"), and drill-through still resolves the underlying rows.
slice: {
rows: [{ uniqueName: 'orderValue', binning: { interval: 250 } }], // 0–250, 250–500, …
// or: rows: [{ uniqueName: 'orderValue', binning: { breaks: [0, 100, 500] } }], // 0–100, 100–500, 500+
measures: [{ uniqueName: 'qty', aggregation: 'sum' }],
}
pivot.setBinning('orderValue', { interval: 250 }); // at runtime; null clears
Selection & copy
Select a rectangle of cells with Shift+click or Shift+arrow keys, then copy it as TSV with Ctrl/Cmd+C — pasteable straight into Excel or Sheets. Off-screen (virtualized) rows are included in the copied text, and a copy event fires with { rows, columns, text }.
Dark mode, RTL & localization
All three are options on the report:
new ProPivot({
container: '#pivot',
report: {
dataSource, slice,
options: {
theme: 'dark', // 'light' (default) | 'dark' | 'auto' (follows the OS)
rtl: true, // right-to-left layout (numbers stay LTR)
localization: { grid: {
fields: 'Champs', apply: 'Appliquer', all: 'Tout', none: 'Aucun',
searchMembers: 'Rechercher…', labelFilter: 'Filtre texte',
valueFilter: 'Filtre valeur', clearFilters: 'Effacer',
drillThrough: 'Détail', fullscreen: 'Plein écran',
grandTotalCaption: 'Total général', totals: 'Total',
} },
},
},
});
The dark theme covers the grid, toolbar, field list, popups and modals. Prefer your own palette? Override the CSS variables on .pp-root (--pp-accent, --pp-surface, --pp-header-bg, …). See it live on the What's new page.
csv, excel, pdf, html, blankMember, dateInvalidCaption and gridLabel (the grid's accessible name).Export
Every export mirrors the on-screen grid — including conditional colours, display formats and number formatting.
pivot.exportTo('csv', { filename: 'report' });
pivot.exportTo('excel', { filename: 'report', excelSheetName: 'Sales' });
pivot.exportTo('pdf', { filename: 'report', pageOrientation: 'landscape' });
pivot.exportTo('image', { filename: 'report' }); // SVG → PNG, in-browser
Scaling to millions
ProPivot is built for big data in the browser:
- Columnar engine — a single GROUPING-SETS scan computes every subtotal and grand total.
- Virtualized rendering — only the visible rows are in the DOM.
- Optional Web Worker — keep the UI thread free during aggregation.
- DuckDB-WASM accelerator — opt in for millions of rows; falls back to the built-in engine with identical results.
new ProPivot({
container: '#pivot',
worker: true,
workerUrl: '/assets/propivot.worker.js?v=cf31a6ae',
accelerator: 'duckdb', // opt-in
duckdb: { threshold: 200000 }, // switch on above N rows
report,
});
See it live on the 2,000,000-row demo.
More resources
- What's new — live demos of accessibility, filters, binning, range-copy, dark mode, RTL & localization.
- Feature examples — a focused, runnable demo per capability (with copy & download code).
- Feature gallery — many live pivots side by side.
- 2M-row demo — the engine & accelerator under load.
- Changelog · GitHub · Architecture