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 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';
function SalesPivot({ data }) { return ( <DataGrid rows={data} columns={[ { field: 'region', header: 'Region' }, { field: 'product', header: 'Product' }, { field: 'quarter', header: 'Quarter' }, { field: 'sales', header: 'Sales', type: 'number' }, ]} pivot={{ rowGroupFields: ['region', 'product'], columnGroupFields: ['quarter'], valueFields: [{ field: 'sales', aggFunc: 'sum', header: 'Total' }], }} style={{ height: '400px' }} /> );}<script setup>import '@toolbox-web/grid-vue/features/pivot';import { TbwGrid, TbwGridColumn } 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 pivotConfig = { rowGroupFields: ['region', 'product'], columnGroupFields: ['quarter'], valueFields: [{ field: 'sales', aggFunc: 'sum', header: 'Total' }],};</script>
<template> <TbwGrid :rows="data" :pivot="pivotConfig" style="height: 400px"> <TbwGridColumn field="region" header="Region" /> <TbwGridColumn field="product" header="Product" /> <TbwGridColumn field="quarter" header="Quarter" /> <TbwGridColumn field="sales" header="Sales" type="number" /> </TbwGrid></template>// Feature import - enables the [pivot] inputimport { GridPivotDirective } from '@toolbox-web/grid-angular/features/pivot';import { Component } from '@angular/core';import { Grid } from '@toolbox-web/grid-angular';import type { ColumnConfig } from '@toolbox-web/grid';
@Component({ selector: 'app-sales-pivot', imports: [Grid, GridPivotDirective], template: ` <tbw-grid [rows]="rows" [columns]="columns" [pivot]="pivotConfig" style="height: 400px; display: block;"> </tbw-grid> `,})export class SalesPivotComponent { rows = [...]; // Your sales data
columns: ColumnConfig[] = [ { field: 'region', header: 'Region' }, { field: 'product', header: 'Product' }, { field: 'quarter', header: 'Quarter' }, { field: 'sales', header: 'Sales', type: 'number' }, ];
pivotConfig = { rowGroupFields: ['region', 'product'], columnGroupFields: ['quarter'], valueFields: [{ field: 'sales', aggFunc: 'sum', header: 'Total' }], };}<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 & Programmatic API
Section titled “Events & 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.
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