Pivot Table Plugin
The Pivot plugin transforms flat data into a pivot table view.
Installation
Section titled “Installation”import '@toolbox-web/grid/features/pivot';Basic Usage
Section titled “Basic Usage”import '@toolbox-web/grid';import '@toolbox-web/grid/features/pivot';import { queryGrid } from '@toolbox-web/grid';
const salesData = [ { region: 'North', product: 'Widget', quarter: 'Q1', sales: 1200 }, { region: 'North', product: 'Widget', quarter: 'Q2', sales: 1500 }, { region: 'South', product: 'Gadget', quarter: 'Q1', sales: 900 }, { region: 'South', product: 'Gadget', quarter: 'Q2', sales: 1100 },];
const grid = queryGrid('tbw-grid');grid.gridConfig = { columns: [ { field: 'region', header: 'Region' }, { field: 'product', header: 'Product' }, { field: 'quarter', header: 'Quarter' }, { field: 'sales', header: 'Sales', type: 'number' }, ], features: { pivot: { rowGroupFields: ['region', 'product'], columnGroupFields: ['quarter'], valueFields: [{ field: 'sales', aggFunc: 'sum', header: 'Total' }], }, },};grid.rows = salesData;import '@toolbox-web/grid-react/features/pivot';import { DataGrid } from '@toolbox-web/grid-react';import type { GridConfig } from '@toolbox-web/grid-react';
const gridConfig: GridConfig = { columns: [ { field: 'region', header: 'Region' }, { field: 'product', header: 'Product' }, { field: 'quarter', header: 'Quarter' }, { field: 'sales', header: 'Sales', type: 'number' }, ], features: { pivot: { rowGroupFields: ['region', 'product'], columnGroupFields: ['quarter'], valueFields: [{ field: 'sales', aggFunc: 'sum', header: 'Total' }], }, },};
function SalesPivot({ data }) { return <DataGrid rows={data} gridConfig={gridConfig} style={{ height: '400px' }} />;}<script setup>import '@toolbox-web/grid-vue/features/pivot';import { TbwGrid } from '@toolbox-web/grid-vue';import type { GridConfig } from '@toolbox-web/grid-vue';
const data = [ { region: 'North', product: 'Widget', quarter: 'Q1', sales: 1000 }, { region: 'North', product: 'Gadget', quarter: 'Q1', sales: 1500 }, { region: 'South', product: 'Widget', quarter: 'Q2', sales: 1200 },];
const gridConfig: GridConfig = { columns: [ { field: 'region', header: 'Region' }, { field: 'product', header: 'Product' }, { field: 'quarter', header: 'Quarter' }, { field: 'sales', header: 'Sales', type: 'number' }, ], features: { pivot: { rowGroupFields: ['region', 'product'], columnGroupFields: ['quarter'], valueFields: [{ field: 'sales', aggFunc: 'sum', header: 'Total' }], }, },};</script>
<template> <TbwGrid :rows="data" :grid-config="gridConfig" style="height: 400px" /></template>Define everything inside gridConfig.features, exactly like the other frameworks. The side-effect import registers the feature so the adapter can bridge it.
import '@toolbox-web/grid-angular/features/pivot';import { Component } from '@angular/core';import { Grid } from '@toolbox-web/grid-angular';import type { GridConfig } from '@toolbox-web/grid-angular';
@Component({ selector: 'app-sales-pivot', imports: [Grid], template: ` <tbw-grid [rows]="rows" [gridConfig]="gridConfig" style="height: 400px; display: block;"> </tbw-grid> `,})export class SalesPivotComponent { rows = [...]; // Your sales data
gridConfig: GridConfig = { columns: [ { field: 'region', header: 'Region' }, { field: 'product', header: 'Product' }, { field: 'quarter', header: 'Quarter' }, { field: 'sales', header: 'Sales', type: 'number' }, ], features: { pivot: { rowGroupFields: ['region', 'product'], columnGroupFields: ['quarter'], valueFields: [{ field: 'sales', aggFunc: 'sum', header: 'Total' }], }, }, };}Full configuration
Section titled “Full configuration”<tbw-grid style="height: 400px;"></tbw-grid>import '@toolbox-web/grid';import { queryGrid } from '@toolbox-web/grid';import '@toolbox-web/grid/features/pivot';
const salesData = [ { region: 'North', product: 'Widget A', quarter: 'Q1', sales: 1200 }, { region: 'North', product: 'Widget B', quarter: 'Q1', sales: 800 }, { region: 'North', product: 'Widget A', quarter: 'Q2', sales: 1500 }, { region: 'North', product: 'Widget B', quarter: 'Q2', sales: 950 }, { region: 'South', product: 'Widget A', quarter: 'Q1', sales: 900 }, { region: 'South', product: 'Widget B', quarter: 'Q1', sales: 1100 }, { region: 'South', product: 'Widget A', quarter: 'Q2', sales: 1300 }, { region: 'South', product: 'Widget B', quarter: 'Q2', sales: 1400 }, { region: 'East', product: 'Widget A', quarter: 'Q1', sales: 750 }, { region: 'East', product: 'Widget B', quarter: 'Q1', sales: 650 }, { region: 'East', product: 'Widget A', quarter: 'Q2', sales: 880 }, { region: 'East', product: 'Widget B', quarter: 'Q2', sales: 720 },];const columns = [ { field: 'region', header: 'Region' }, { field: 'product', header: 'Product' }, { field: 'quarter', header: 'Quarter' }, { field: 'sales', header: 'Sales', type: 'number' },];
const grid = queryGrid('tbw-grid');
function rebuild(opts: Record<string, unknown>) { const aggFunc = (opts.aggFunc) ?? 'sum'; const headerLabel = aggFunc === 'sum' ? 'Total Sales' : aggFunc === 'avg' ? 'Avg Sales' : aggFunc === 'count' ? 'Count' : aggFunc === 'min' ? 'Min Sales' : 'Max Sales'; const animRaw = (opts.animation) ?? 'slide'; const animation = animRaw === 'false' ? false : animRaw; const valueField: Record<string, unknown> = { field: 'sales', aggFunc, header: headerLabel, format: (v: number) => `$${v.toLocaleString()}`, }; const pivotConfig: Record<string, unknown> = { active: opts.active ?? true, animation, rowGroupFields: ['region', 'product'], columnGroupFields: ['quarter'], valueFields: [valueField], showTotals: opts.showTotals ?? true, showGrandTotal: opts.showGrandTotal ?? true, showToolPanel: opts.showToolPanel ?? true, defaultExpanded: opts.defaultExpanded ?? true, indentWidth: opts.indentWidth ?? 20, }; grid.gridConfig = { columns, features: { pivot: pivotConfig }, }; grid.rows = salesData;}
rebuild({ active: true, showTotals: true, showGrandTotal: true, showToolPanel: true, defaultExpanded: true, indentWidth: 20, animation: 'slide', aggFunc: 'sum' });Use the controls to explore the full pivot configuration — toggle active state, totals, grand total, tool panel, default expansion, indent width, aggregation function, and animation style. For sorting, see the Sorting section below.
Events and programmatic API
Section titled “Events and programmatic API”<tbw-grid style="height: 350px;"></tbw-grid>import '@toolbox-web/grid';import { queryGrid } from '@toolbox-web/grid';import '@toolbox-web/grid/features/pivot';
const salesData = [ { region: 'North', product: 'Widget A', quarter: 'Q1', sales: 1200 }, { region: 'North', product: 'Widget B', quarter: 'Q1', sales: 800 }, { region: 'North', product: 'Widget A', quarter: 'Q2', sales: 1500 }, { region: 'North', product: 'Widget B', quarter: 'Q2', sales: 950 }, { region: 'South', product: 'Widget A', quarter: 'Q1', sales: 900 }, { region: 'South', product: 'Widget B', quarter: 'Q1', sales: 1100 }, { region: 'South', product: 'Widget A', quarter: 'Q2', sales: 1300 }, { region: 'South', product: 'Widget B', quarter: 'Q2', sales: 1400 }, { region: 'East', product: 'Widget A', quarter: 'Q1', sales: 750 }, { region: 'East', product: 'Widget B', quarter: 'Q1', sales: 650 }, { region: 'East', product: 'Widget A', quarter: 'Q2', sales: 880 }, { region: 'East', product: 'Widget B', quarter: 'Q2', sales: 720 },];
const grid = queryGrid('tbw-grid');
grid.gridConfig = { columns: [ { field: 'region', header: 'Region' }, { field: 'product', header: 'Product' }, { field: 'quarter', header: 'Quarter' }, { field: 'sales', header: 'Sales', type: 'number' }, ], features: { pivot: { rowGroupFields: ['region', 'product'], columnGroupFields: ['quarter'], valueFields: [{ field: 'sales', aggFunc: 'sum', header: 'Total Sales' }], showTotals: true, showGrandTotal: true, showToolPanel: true, defaultExpanded: true, }, },};grid.rows = salesData;
// Event logconst entries = document.getElementById('pivot-event-entries');const MAX_LOG = 20;
function log(type: string, detail: string) { const el = document.createElement('div'); el.className = 'pivot-event-entry'; el.innerHTML = `<span class="pivot-event-type">${type}</span> ${detail}`; entries.prepend(el); while (entries.children.length > MAX_LOG) { entries.lastElementChild?.remove(); }}
grid.addEventListener('pivot-toggle', ((e: CustomEvent) => { const d = e.detail; log('toggle', `<strong>${d.label}</strong> ${d.expanded ? 'expanded' : 'collapsed'} (depth ${d.depth})`);}));
grid.addEventListener('pivot-state-change', ((e: CustomEvent) => { log('state', `Pivot is now <strong>${e.detail.active ? 'active' : 'inactive'}</strong>`);}));
grid.addEventListener('pivot-config-change', ((e: CustomEvent) => { const d = e.detail; const parts = [`property: <strong>${d.property}</strong>`]; if (d.field) parts.push(`field: ${d.field}`); if (d.zone) parts.push(`zone: ${d.zone}`); log('config', parts.join(', '));}));
// Programmatic API buttonsconst plugin = grid.getPluginByName('pivot');
document.getElementById('pivot-expand-all')?.addEventListener('click', () => { plugin?.expandAll(); log('api', 'Called <strong>expandAll()</strong>');});
document.getElementById('pivot-collapse-all')?.addEventListener('click', () => { plugin?.collapseAll(); log('api', 'Called <strong>collapseAll()</strong>');});
document.getElementById('pivot-get-expanded')?.addEventListener('click', () => { const groups = plugin?.getExpandedGroups() ?? []; log('api', `<strong>getExpandedGroups()</strong> → [${groups.map((k: string) => `"${k}"`).join(', ')}]`);});Click groups to expand/collapse and watch the event log. Use the buttons to call the programmatic API. Open the tool panel and drag fields between zones to see pivot-config-change events.
Configuration Options
Section titled “Configuration Options”See PivotConfig for the full list of options and defaults.
Key options:
showTotals— Shows/hides the per-row Total column on the right (the sum across all pivot columns for each row). This does not affect the aggregated values shown in group rows.showGrandTotal— Shows/hides the grand total row at the bottom.grandTotalInRowModel— Whentrue, the grand total renders as a regular row instead of a sticky footer (useful for data export and copy/paste).
Aggregation Functions
Section titled “Aggregation Functions”Built-in: sum, avg, count, min, max, first, last
Custom Aggregators
Section titled “Custom Aggregators”You can provide a custom aggregation function instead of a built-in name:
features: { pivot: { rowGroupFields: ['region'], valueFields: [{ field: 'sales', aggFunc: (values) => values.reduce((a, b) => a + b * 2, 0), }], },}Custom aggregators appear as “CUSTOM” in the tool panel and cannot be changed via the UI.
Value Formatting
Section titled “Value Formatting”Format aggregated values with a format function on the value field:
valueFields: [{ field: 'revenue', aggFunc: 'sum', format: (value) => `$${value.toLocaleString()}`,}]If no format is provided, the plugin falls back to the original column’s format function.
Programmatic-Only Usage
Section titled “Programmatic-Only Usage”To use pivot transformation without exposing the tool panel UI to users:
features: { pivot: { showToolPanel: false, rowGroupFields: ['region'], columnGroupFields: ['quarter'], valueFields: [{ field: 'sales', aggFunc: 'sum' }], },},The pivot API methods remain available for programmatic control.
Tool Panel
Section titled “Tool Panel”When showToolPanel: true (default), the pivot panel lets users configure the pivot interactively:
- Drag fields between Row Groups, Column Groups, and Values zones
- Reorder fields within a zone by dragging
- Search fields using the filter input at the top of the Available Fields list
- Change aggregation via the dropdown on each value field (built-in functions only — custom aggregators show “CUSTOM” and cannot be changed via the UI)
The panel fires pivot-config-change events when users modify the configuration.
Accessibility
Section titled “Accessibility”The pivot panel satisfies WCAG 2.2 SC 2.5.7 Dragging Movements: every field move that can be made by dragging a chip can also be made with a single pointer that never drags.
- Click the chip you would have dragged — pressing and releasing a field chip without moving is a tap, not a drag. HTML5 drag-and-drop only fires
dragstarton a real drag, so a plain click means the pointer user could not (or chose not to) drag, and the panel opens a menu of click-only actions instead. - Chips already in a zone — the menu offers Move up, Move down, Move to Column Groups (or Move to Row Groups from the column zone), Move to Values and Remove field. Steps that would run off the end of the zone are rendered disabled rather than hidden. Every entry calls the same panel callback a drop does, so the resulting
pivot-config-changeevent is identical. - Chips in Available Fields — the menu offers Add to Row Groups, Add to Column Groups and Add to Values, matching the three zones a chip can be dropped on.
- No reserved chrome — the alternative reuses the chip itself, so it costs no extra panel width. SC 2.5.7 requires the alternative to exist and be pointer-operable, not to be permanently on screen.
- Chip semantics — the accessible name is “<field> — drag, or activate for move options” so the dual purpose is announced. On a chip that owns a remove button, the name and
role="button"sit on the chip’s label rather than the chip, becauserole="button"would make the remove button presentational. The menu itself is arole="group"of plain buttons; focus moves to the first enabled entry on open, Escape closes it, and moving focus outside dismisses it.
See Pointer Alternatives to Dragging for how this fits the grid-wide a11y.dragAlternatives setting.
Animation
Section titled “Animation”The plugin supports animated expand/collapse transitions. Animation respects the grid-level
animation.mode setting and can be customized per-plugin:
features: { pivot: { rowGroupFields: ['region'], valueFields: [{ field: 'sales', aggFunc: 'sum' }], animation: 'slide', // 'slide' | 'fade' | false },},Animation is automatically disabled when animation.mode is 'off' or when the user prefers
reduced motion ('reduced-motion' mode with system preference).
Programmatic API
Section titled “Programmatic API”const plugin = grid.getPluginByName('pivot');
plugin.expand('North__Widget A'); // expand a specific group by keyplugin.collapse('North'); // collapse a groupplugin.expandAll();plugin.collapseAll();plugin.getExpandedGroups(); // returns string[] of expanded keysSorting
Section titled “Sorting”Pivot columns support interactive sorting — click any column header to cycle through ascending, descending, and unsorted.
Multi-Column Sort
Section titled “Multi-Column Sort”When the Multi-Sort plugin is loaded alongside Pivot, shift-click adds secondary sort columns with numbered priority badges. Pivot translates the multi-sort model into hierarchical sorting that respects the group structure.
Click any column header to sort. Shift-click to sort by multiple columns. Click again to reverse. Click a third time to clear.
Programmatic Sort
Section titled “Programmatic Sort”You can also configure sorting programmatically:
features: { pivot: { rowGroupFields: ['region'], valueFields: [{ field: 'sales', aggFunc: 'sum' }], sortRows: { by: 'label', direction: 'asc' }, // sort row groups alphabetically sortColumns: 'desc', // sort column keys descending },}sortRows accepts { by: 'label' | 'value', direction: 'asc' | 'desc', valueField?: string }.
When by is 'value', rows are sorted by their total (or by a specific valueField).
Default Expanded
Section titled “Default Expanded”Control which groups start expanded:
features: { pivot: { defaultExpanded: true, // all groups (default) defaultExpanded: false, // all collapsed defaultExpanded: 'North', // expand only the 'North' group defaultExpanded: ['North', 'South'], // expand specific groups defaultExpanded: 0, // expand group at index 0 },}Grand Total in Row Model
Section titled “Grand Total in Row Model”By default, the grand total renders as a sticky footer. To include it in the row model (useful for exports and copy/paste):
features: { pivot: { showGrandTotal: true, grandTotalInRowModel: true, },}Events
Section titled “Events”| Event | Detail | Description |
|---|---|---|
pivot-toggle | { key, expanded, label, depth } | Fired when a group is expanded or collapsed |
pivot-state-change | { active: boolean } | Fired when pivot mode is enabled or disabled |
pivot-config-change | { property, field?, zone? } | Fired when pivot configuration changes (fields, agg func, etc.) |
grid.addEventListener('pivot-toggle', (e) => { console.log(`${e.detail.label} ${e.detail.expanded ? 'expanded' : 'collapsed'}`);});
grid.addEventListener('pivot-state-change', (e) => { console.log(`Pivot is now ${e.detail.active ? 'active' : 'inactive'}`);});Styling
Section titled “Styling”The pivot plugin supports CSS custom properties for theming. Override these on tbw-grid or a parent container:
CSS Custom Properties
Section titled “CSS Custom Properties”| Property | Default | Description |
|---|---|---|
--tbw-pivot-group-bg | var(--tbw-color-row-alt) | Group row background |
--tbw-pivot-group-hover | var(--tbw-color-row-hover) | Group row hover |
--tbw-pivot-leaf-bg | var(--tbw-color-bg) | Leaf row background |
--tbw-pivot-grand-total-bg | var(--tbw-color-header-bg) | Grand total row |
--tbw-pivot-toggle-hover-bg | var(--tbw-color-row-hover) | Toggle button hover |
--tbw-pivot-section-bg | var(--tbw-color-panel-bg) | Panel section background |
--tbw-toggle-size | 1.25em | Toggle button size |
--tbw-animation-duration | 200ms | Expand/collapse animation |
Example
Section titled “Example”tbw-grid { /* Custom pivot styling */ --tbw-pivot-group-bg: #fff3e0; --tbw-pivot-grand-total-bg: #ffcc80; --tbw-pivot-toggle-hover-bg: #ffe0b2;}CSS Classes
Section titled “CSS Classes”The pivot plugin uses these class names:
| Class | Element |
|---|---|
.pivot-group-row | Grouped summary row |
.pivot-leaf-row | Detail/leaf row |
.pivot-grand-total-row | Grand total row |
.pivot-grand-total-footer | Sticky footer container |
.pivot-toggle | Expand/collapse button |
.pivot-label | Group name display |
.tbw-pivot-slide-in | Row slide animation |
.tbw-pivot-fade-in | Row fade animation |
Plugin Compatibility
Section titled “Plugin Compatibility”A development-mode warning is shown if incompatible plugins are loaded together.
See Also
Section titled “See Also”- Row Grouping — Group flat data by field values
- Server-Side Plugin — Lazy loading and remote data