ProPivot

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.

Enterprise-grade Open source (MIT) React · Angular · Vue Any framework Millions of rows 100% client-side Lightweight Highly configurable

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.

1

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

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
];
3

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' }],
    },
  },
});
4

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' } },
    ],
  },
});
5

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:

KeyWhat it does
dataSourceYour data: { type: 'json', data, mapping } or a filename (CSV/JSON). mapping sets captions & types.
slice.rows / slice.columnsFields to group on each axis — [{ uniqueName: 'region' }]. Supports per-field filter and sort.
slice.measuresValues to aggregate — { uniqueName, aggregation, format, caption }, or a calculated formula.
slice.reportFiltersFields shown as a filter bar above the grid.
formatsNamed number formats referenced by a measure's format.
conditionsConditional formatting rules (#value dialect + colours).
options.gridLayout (compact / flat / classic), totals, grand totals.
💡 A report is plain JSON — store it, diff it, or send it over the wire. Call 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).

No Vue plugin or registration needed — <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); }
}
The Angular wrapper forwards all events as @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

EventFires when
cellclick / celldoubleclickA cell (or a row/column header) is clicked — payload carries the full row & column tuple, measure and value.
columnresizeA column is drag-resized — { ref, width }.
columnreorderA column is moved between/within zones — { uniqueName, fromZone, toZone }.
columnpropertychangeA column's heading / aggregation / format / filter changes.
reportcomplete / readyThe report finished computing & rendering.
updateThe grid re-rendered after any interaction.
copyA selected cell range was copied — { rows, columns, text } (the TSV written to the clipboard).

Useful methods

MethodPurpose
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):

KeyAction
TabMove focus into / out of the grid (one stop).
Arrow keysMove the focused cell; Home/End jump to row ends, Ctrl+Home/Ctrl+End to the grid corners, PageUp/PageDown by a viewport.
Enter / SpaceActivate the cell — sort a header, expand/collapse a group row, or select a value.
Shift+F10Open the column properties menu, or drill through a value cell.
📱 Column resize, reorder, and the drag-drop field list use Pointer Events, so they work with mouse, pen, and touch (phones / tablets).

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

TypeExample
Membersfilter: { members: ['West', 'East'] }
Top/Bottom-Nfilter: { 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.

💡 Localization keys also include 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