Core pivot table utilities extracted and modernized from plotly/react-pivottable.
This library provides powerful data aggregation and pivot table functionality with support for custom aggregators, formatters, and derived attributes. It's framework-agnostic at its core, with optional Lit integration for rendering.
- 🔢 20+ Built-in Aggregators: Count, Sum, Average, Median, Min, Max, Variance, Standard Deviation, and more
- 📊 Flexible Data Processing: Support for derived attributes, custom filters, and sorters
- 🎨 Customizable Formatting: Built-in number formatters with internationalization support
- 🧩 Framework Integration: Optional Lit templates for easy rendering
- 🔧 Extensible: Create custom aggregators using aggregator templates
- 📦 Modern ES Modules: Tree-shakeable exports for optimal bundle size
npm install pivot-utilsor
yarn add pivot-utils// Import the main utilities
import { PivotData } from 'pivot-utils'
// or import multiple items
// import { PivotData, aggregatorTemplates, numberFormat } from 'pivot-utils'
// Sample data (can be array of objects or array of arrays)
const data = [
['name', 'gender', 'colour', 'birthday', 'trials', 'successes'],
['Nick', 'male', 'blue', '1982-11-07', 103, 12],
['Jane', 'female', 'red', '1982-11-08', 95, 25],
['John', 'male', 'blue', '1982-12-08', 112, 30],
['Carol', 'female', 'yellow', '1983-12-08', 102, 12],
]
// Create pivot data
const pivotData = new PivotData({
data: data,
rows: ['colour'],
cols: ['gender'],
aggregatorName: 'Count',
})
// Get aggregated values
console.log(pivotData.getAggregator([], []).value()) // Total count: 4
console.log(pivotData.getAggregator(['blue'], []).value()) // Blue count: 2
console.log(pivotData.getAggregator(['blue'], ['male']).value()) // Blue males: 2Counts the number of records in each cell.
const pivotData = new PivotData({
data: data,
aggregatorName: 'Count',
rows: ['colour'],
cols: ['gender'],
})
pivotData.getAggregator([], []).value() // 4 (total records)
pivotData.getAggregator(['blue'], []).value() // 2 (blue items)
pivotData.getAggregator(['blue'], ['male']).value() // 2 (blue males)Counts the number of unique values in a specified field.
const pivotData = new PivotData({
data: data,
aggregatorName: 'Count Unique Values',
vals: ['successes'],
rows: ['colour'],
cols: ['gender'],
})
pivotData.getAggregator([], []).value() // 3 (unique success values: 12, 25, 30)
pivotData.getAggregator(['blue'], []).value() // 2 (unique: 12, 30)Lists all unique values in a specified field as a comma-separated string.
const pivotData = new PivotData({
data: data,
aggregatorName: 'List Unique Values',
vals: ['successes'],
rows: ['colour'],
cols: ['gender'],
})
pivotData.getAggregator([], []).value() // "12, 25, 30"
pivotData.getAggregator(['blue'], ['male']).value() // "12, 30"Sums the values of a numeric field.
const pivotData = new PivotData({
data: data,
aggregatorName: 'Sum',
vals: ['successes'],
rows: ['colour'],
cols: ['gender'],
})
pivotData.getAggregator([], []).value() // 79 (12+25+30+12)
pivotData.getAggregator(['blue'], []).value() // 42 (12+30)
pivotData.getAggregator(['yellow'], ['female']).value() // 12Same as Sum but formats the output as an integer (no decimal places).
const pivotData = new PivotData({
data: data,
aggregatorName: 'Integer Sum',
vals: ['trials'],
rows: ['colour'],
cols: ['gender'],
})
pivotData.getAggregator([], []).value() // 412Calculates the mean (average) of values in a numeric field.
const pivotData = new PivotData({
data: data,
aggregatorName: 'Average',
vals: ['successes'],
rows: ['colour'],
cols: ['gender'],
})
pivotData.getAggregator([], []).value() // 19.75 (79/4)
pivotData.getAggregator(['blue'], []).value() // 21 (42/2)Calculates the median value of a numeric field.
const pivotData = new PivotData({
data: data,
aggregatorName: 'Median',
vals: ['successes'],
rows: ['colour'],
cols: ['gender'],
})
pivotData.getAggregator([], []).value() // 18.5 (median of 12,12,25,30)Finds the minimum value in a numeric field.
const pivotData = new PivotData({
data: data,
aggregatorName: 'Minimum',
vals: ['successes'],
rows: ['colour'],
cols: ['gender'],
})
pivotData.getAggregator([], []).value() // 12
pivotData.getAggregator(['blue'], []).value() // 12Finds the maximum value in a numeric field.
const pivotData = new PivotData({
data: data,
aggregatorName: 'Maximum',
vals: ['successes'],
rows: ['colour'],
cols: ['gender'],
})
pivotData.getAggregator([], []).value() // 30
pivotData.getAggregator(['blue'], []).value() // 30Returns the first value in a field according to the sort order.
const pivotData = new PivotData({
data: data,
aggregatorName: 'First',
vals: ['name'],
rows: ['colour'],
cols: ['gender'],
})
// Returns the first name encountered in each groupingReturns the last value in a field according to the sort order.
const pivotData = new PivotData({
data: data,
aggregatorName: 'Last',
vals: ['name'],
rows: ['colour'],
cols: ['gender'],
})
// Returns the last name encountered in each groupingCalculates the sample variance of values in a numeric field.
const pivotData = new PivotData({
data: data,
aggregatorName: 'Sample Variance',
vals: ['successes'],
rows: ['colour'],
cols: ['gender'],
})
// Calculates variance with N-1 degrees of freedom (sample variance)Calculates the sample standard deviation of values in a numeric field.
const pivotData = new PivotData({
data: data,
aggregatorName: 'Sample Standard Deviation',
vals: ['successes'],
rows: ['colour'],
cols: ['gender'],
})
// Calculates standard deviation with N-1 degrees of freedomCalculates the ratio of the sum of two numeric fields.
const pivotData = new PivotData({
data: data,
aggregatorName: 'Sum over Sum',
vals: ['successes', 'trials'],
rows: ['colour'],
cols: ['gender'],
})
// Returns: sum(successes) / sum(trials)
pivotData.getAggregator([], []).value() // 0.1917... (79/412)Expresses the sum in each cell as a fraction of the grand total.
const pivotData = new PivotData({
data: data,
aggregatorName: 'Sum as Fraction of Total',
vals: ['successes'],
rows: ['colour'],
cols: ['gender'],
})
pivotData.getAggregator(['blue'], []).value() // 0.5316... (42/79)
// Formatted as: "53.2%" by defaultExpresses the sum in each cell as a fraction of its row total.
const pivotData = new PivotData({
data: data,
aggregatorName: 'Sum as Fraction of Rows',
vals: ['successes'],
rows: ['colour'],
cols: ['gender'],
})
pivotData.getAggregator(['blue'], ['male']).value() // 1.0 (all blue males)
// Shows what percentage of the row total each cell representsExpresses the sum in each cell as a fraction of its column total.
const pivotData = new PivotData({
data: data,
aggregatorName: 'Sum as Fraction of Columns',
vals: ['successes'],
rows: ['colour'],
cols: ['gender'],
})
// Shows what percentage of the column total each cell representsExpresses the count in each cell as a fraction of the grand total count.
const pivotData = new PivotData({
data: data,
aggregatorName: 'Count as Fraction of Total',
rows: ['colour'],
cols: ['gender'],
})
pivotData.getAggregator(['blue'], []).value() // 0.5 (2/4)
pivotData.getAggregator(['yellow'], ['female']).value() // 0.25 (1/4)Expresses the count in each cell as a fraction of its row total count.
const pivotData = new PivotData({
data: data,
aggregatorName: 'Count as Fraction of Rows',
rows: ['colour'],
cols: ['gender'],
})
pivotData.getAggregator(['blue'], ['male']).value() // 1.0 (all blue are male)Expresses the count in each cell as a fraction of its column total count.
const pivotData = new PivotData({
data: data,
aggregatorName: 'Count as Fraction of Columns',
rows: ['colour'],
cols: ['gender'],
})
pivotData.getAggregator(['yellow'], ['female']).value() // 0.5 (1 of 2 females)You can create custom aggregators using aggregator templates:
import { aggregatorTemplates, numberFormat } from 'pivot-utils'
// Create a custom formatter
const customFormat = numberFormat({
digitsAfterDecimal: 3,
scaler: 1,
thousandsSep: ' ',
decimalSep: ',',
prefix: '$',
suffix: ' USD',
})
// Create custom aggregators using templates with custom formatters
const customAggregators = {
'Custom Sum': aggregatorTemplates.sum(customFormat),
'Custom Average': aggregatorTemplates.average(customFormat),
// Or create a completely custom aggregator from scratch
'Weighted Average': ([valueAttr, weightAttr]) => {
return function () {
return {
sumProduct: 0,
sumWeight: 0,
push(record) {
const value = parseFloat(record[valueAttr])
const weight = parseFloat(record[weightAttr])
if (!isNaN(value) && !isNaN(weight)) {
this.sumProduct += value * weight
this.sumWeight += weight
}
},
value() {
return this.sumWeight === 0 ? 0 : this.sumProduct / this.sumWeight
},
format: customFormat,
numInputs: 2,
}
}
},
}
const pivotData = new PivotData({
data: data,
aggregators: customAggregators,
aggregatorName: 'Custom Sum',
vals: ['successes'],
rows: ['colour'],
cols: ['gender'],
})You can add computed fields to your data:
import { derivers } from 'pivot-utils'
const pivotData = new PivotData({
data: data,
rows: ['age_group'],
cols: ['gender'],
aggregatorName: 'Count',
derivedAttributes: {
// Bin numeric values
age_group: derivers.bin('age', 10), // Groups ages in bins of 10
// Format dates
birth_year: derivers.dateFormat('birthday', '%y'),
birth_month: derivers.dateFormat('birthday', '%n'),
},
})Filter data before aggregation:
const pivotData = new PivotData({
data: data,
rows: ['colour'],
cols: ['gender'],
aggregatorName: 'Count',
valueFilter: {
gender: ['male'], // Only include males
colour: ['blue', 'red'], // Only include blue and red
},
})Control the sort order of rows and columns:
import { sortAs } from 'pivot-utils'
const pivotData = new PivotData({
data: data,
rows: ['colour'],
cols: ['gender'],
aggregatorName: 'Count',
// Sort by aggregated values
rowOrder: 'value_a_to_z', // or 'value_z_to_a'
colOrder: 'key_a_to_z', // or 'key_z_to_a'
// Or use custom sorters
sorters: {
colour: sortAs(['red', 'blue', 'yellow']), // Custom order
gender: (a, b) => a.localeCompare(b), // Custom function
},
})Customize number display:
import { numberFormat, aggregatorTemplates } from 'pivot-utils'
const euroFormat = numberFormat({
digitsAfterDecimal: 2,
scaler: 1,
thousandsSep: '.',
decimalSep: ',',
prefix: '€',
suffix: '',
})
const percentFormat = numberFormat({
digitsAfterDecimal: 1,
scaler: 100,
suffix: '%',
})
// Use with custom aggregators
const customAggregators = {
'Sum (EUR)': aggregatorTemplates.sum(euroFormat),
'Percentage': aggregatorTemplates.average(percentFormat),
}The library provides Lit templates for rendering pivot tables.
The package includes CSS for styling pivot tables at styles/pivottable.css. You can import it in your HTML:
<link rel="stylesheet" href="node_modules/pivot-utils/styles/pivottable.css">Or in your JavaScript bundler configuration (Vite, Webpack, etc.):
import 'pivot-utils/styles/pivottable.css'import { html, LitElement } from 'lit'
import { PivotData } from 'pivot-utils'
// Import Lit templates - both paths work:
import { pivotTable, pivotTableLegacy, pivotHead, pivotRow } from 'pivot-utils/lit'
// or
// import { pivotTable, pivotTableLegacy, pivotHead, pivotRow } from 'pivot-utils/lit.js'
// Note: You'll need to import the CSS separately in your HTML or build config
// The styles are available at: node_modules/pivot-utils/styles/pivottable.css
class MyElement extends LitElement {
render() {
const pivotData = new PivotData({
data: this.data,
rows: ['colour'],
cols: ['gender'],
aggregatorName: 'Count',
})
return html`
<!-- Full pivot table -->
${pivotTable(pivotData, {
keyFormatters: { gender: (v) => v.toUpperCase() },
onValueClick: (e) => console.log('Clicked:', e.target.textContent),
})}
<!-- Or use individual components -->
<table class="pvtTable">
<thead>
${pivotHead(pivotData, pivotData.getColKeys())}
</thead>
<tbody>
${pivotData.getRowKeys().map((rowKey) =>
pivotRow(pivotData, rowKey, pivotData.getColKeys())
)}
</tbody>
</table>
`
}
}data(required): Array of objects or array of arrays (with headers in first row)rows: Array of attribute names to use as row headerscols: Array of attribute names to use as column headersvals: Array of attribute names to pass to the aggregatoraggregatorName: Name of the aggregator to use (default: 'Count')aggregators: Object mapping aggregator names to aggregator functionsvalueFilter: Object mapping attribute names to arrays of allowed valuessorters: Object mapping attribute names to sort functions, or function returning sortersrowOrder: Sort order for rows ('key_a_to_z', 'key_z_to_a', 'value_a_to_z', 'value_z_to_a')colOrder: Sort order for columns (same options as rowOrder)derivedAttributes: Object mapping new attribute names to deriver functions
getAggregator(rowKey, colKey): Get the aggregator for a specific cellgetRowKeys(): Get array of all row keys (sorted)getColKeys(): Get array of all column keys (sorted)forEachMatchingRecord(criteria, callback): Iterate over filtered records
Each aggregator has these methods:
value(): Returns the computed valueformat(x): Returns the formatted string representationpush(record): Adds a record to the aggregation (internal use)
Natural sorting algorithm that handles numbers in strings correctly:
import { naturalSort } from 'pivot-utils'
const items = ['item 1', 'item 10', 'item 2', 'item 20']
items.sort(naturalSort)
// Result: ['item 1', 'item 2', 'item 10', 'item 20']Create a custom sort order:
import { sortAs } from 'pivot-utils'
const customSort = sortAs(['high', 'medium', 'low'])
const priorities = ['low', 'high', 'medium', 'high']
priorities.sort(customSort)
// Result: ['high', 'high', 'medium', 'low']Get the appropriate sort function for an attribute:
import { getSort } from 'pivot-utils'
const sorters = {
priority: sortAs(['high', 'medium', 'low']),
}
const sortFn = getSort(sorters, 'priority')This project is a modernized extraction of the core pivot table utilities from plotly/react-pivottable, originally created by Nicolas Kruchten. The code has been updated to use modern JavaScript (ES modules, etc.) and made framework-agnostic while maintaining the powerful aggregation capabilities.
MIT
Contributions are welcome! Please feel free to submit a Pull Request.