Skip to content

Pivot Table Plugin

The Pivot plugin transforms flat data into a pivot table view.

import '@toolbox-web/grid/features/pivot';
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;
ActiveWhether pivot is active
AggregationAggregation function
Show total columnShow the per-row Total column on the right
Show grand totalShow grand total row at the bottom
Show tool panelShow drag-and-drop tool panel
Default expandedGroups expanded by default
Indent widthIndent per depth level (px)
AnimationExpand/collapse animation

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.

Event Log

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.

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 — When true, the grand total renders as a regular row instead of a sticky footer (useful for data export and copy/paste).

Built-in: sum, avg, count, min, max, first, last

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.

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.

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.

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.

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 dragstart on 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-change event 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, because role="button" would make the remove button presentational. The menu itself is a role="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.

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

const plugin = grid.getPluginByName('pivot');
plugin.expand('North__Widget A'); // expand a specific group by key
plugin.collapse('North'); // collapse a group
plugin.expandAll();
plugin.collapseAll();
plugin.getExpandedGroups(); // returns string[] of expanded keys

Pivot columns support interactive sorting — click any column header to cycle through ascending, descending, and unsorted.

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.

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

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
},
}

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,
},
}
EventDetailDescription
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'}`);
});

The pivot plugin supports CSS custom properties for theming. Override these on tbw-grid or a parent container:

PropertyDefaultDescription
--tbw-pivot-group-bgvar(--tbw-color-row-alt)Group row background
--tbw-pivot-group-hovervar(--tbw-color-row-hover)Group row hover
--tbw-pivot-leaf-bgvar(--tbw-color-bg)Leaf row background
--tbw-pivot-grand-total-bgvar(--tbw-color-header-bg)Grand total row
--tbw-pivot-toggle-hover-bgvar(--tbw-color-row-hover)Toggle button hover
--tbw-pivot-section-bgvar(--tbw-color-panel-bg)Panel section background
--tbw-toggle-size1.25emToggle button size
--tbw-animation-duration200msExpand/collapse animation
tbw-grid {
/* Custom pivot styling */
--tbw-pivot-group-bg: #fff3e0;
--tbw-pivot-grand-total-bg: #ffcc80;
--tbw-pivot-toggle-hover-bg: #ffe0b2;
}

The pivot plugin uses these class names:

ClassElement
.pivot-group-rowGrouped summary row
.pivot-leaf-rowDetail/leaf row
.pivot-grand-total-rowGrand total row
.pivot-grand-total-footerSticky footer container
.pivot-toggleExpand/collapse button
.pivot-labelGroup name display
.tbw-pivot-slide-inRow slide animation
.tbw-pivot-fade-inRow fade animation

A development-mode warning is shown if incompatible plugins are loaded together.