# Pivot Table Plugin

> Transform row data into a cross-tabulation (pivot table) layout.

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

## Installation

```ts
import '@toolbox-web/grid/features/pivot';
```

## Basic Usage

#### TypeScript

```ts
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;
```

#### React

```tsx
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' }}
    />
  );
}
```

#### Vue

```html
<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>
```

#### Angular

```typescript
// Feature import - enables the [pivot] input
import { 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' }],
  };
}
```

## Demo

```ts
// PivotDefaultDemo.astro
import '@toolbox-web/grid';
import { queryGrid } from '@toolbox-web/grid';
import '@toolbox-web/grid/features/pivot';

const container = document.getElementById('pivot-default-demo');
if (container) {
  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', container)!;

  function rebuild(opts: Record<string, unknown>) {
    const aggFunc = (opts.aggFunc as string) ?? 'sum';
    const headerLabel = aggFunc === 'sum' ? 'Total Sales'
      : aggFunc === 'avg' ? 'Avg Sales'
      : aggFunc === 'count' ? 'Count'
      : aggFunc === 'min' ? 'Min Sales'
      : 'Max Sales';
    const animRaw = (opts.animation as string) ?? '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 as boolean ?? true,
      animation,
      rowGroupFields: ['region', 'product'],
      columnGroupFields: ['quarter'],
      valueFields: [valueField],
      showTotals: opts.showTotals as boolean ?? true,
      showGrandTotal: opts.showGrandTotal as boolean ?? true,
      showToolPanel: opts.showToolPanel as boolean ?? true,
      defaultExpanded: opts.defaultExpanded as boolean ?? true,
      indentWidth: opts.indentWidth as number ?? 20,
    };
    grid.gridConfig = {
      columns,
      features: { pivot: pivotConfig as any },
    };
    grid.rows = salesData;
  }

  rebuild({ active: true, showTotals: true, showGrandTotal: true, showToolPanel: true, defaultExpanded: true, indentWidth: 20, animation: 'slide', aggFunc: 'sum' });

  container.addEventListener('control-change', ((e: CustomEvent) => {
    rebuild(e.detail.allValues);
  }) as EventListener);
}
```

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](#sorting) section below.

## Events & Programmatic API

```ts
// PivotEventsDemo.astro
import '@toolbox-web/grid';
import { queryGrid } from '@toolbox-web/grid';
import '@toolbox-web/grid/features/pivot';

const container = document.getElementById('pivot-events-demo');
if (container) {
  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', container)!;

  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,
      } as any,
    },
  };
  grid.rows = salesData;

  // Event log
  const 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})`);
  }) as EventListener);

  grid.addEventListener('pivot-state-change', ((e: CustomEvent) => {
    log('state', `Pivot is now <strong>${e.detail.active ? 'active' : 'inactive'}</strong>`);
  }) as EventListener);

  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(', '));
  }) as EventListener);

  // Programmatic API buttons
  const plugin = grid.getPluginByName('pivot') as any;

  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

See [`PivotConfig`](./Interfaces/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).

## Aggregation Functions

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

### Custom Aggregators

You can provide a custom aggregation function instead of a built-in name:

```ts
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

Format aggregated values with a `format` function on the value field:

```ts
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

To use pivot transformation without exposing the tool panel UI to users:

```ts
features: {
  pivot: {
    showToolPanel: false,
    rowGroupFields: ['region'],
    columnGroupFields: ['quarter'],
    valueFields: [{ field: 'sales', aggFunc: 'sum' }],
  },
},
```

The pivot API methods remain available for programmatic control.

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

The plugin supports animated expand/collapse transitions. Animation respects the grid-level
`animation.mode` setting and can be customized per-plugin:

```ts
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

```ts
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
```

## Sorting

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

### Multi-Column Sort

When the [Multi-Sort plugin](/grid/plugins/multi-sort.md) 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.

### Programmatic Sort

You can also configure sorting programmatically:

```ts
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

Control which groups start expanded:

```ts
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

By default, the grand total renders as a sticky footer. To include it in the row model
(useful for exports and copy/paste):

```ts
features: {
  pivot: {
    showGrandTotal: true,
    grandTotalInRowModel: true,
  },
}
```

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

```ts
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

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

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

```css
tbw-grid {
  /* Custom pivot styling */
  --tbw-pivot-group-bg: #fff3e0;
  --tbw-pivot-grand-total-bg: #ffcc80;
  --tbw-pivot-toggle-hover-bg: #ffe0b2;
}
```

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

:::caution[Incompatible Plugins]
The Pivot plugin **cannot** be used with the following plugins:

- **[Row Grouping](/grid/plugins/grouping-rows.md)** — Pivot creates its own aggregated row and column structure. Row grouping cannot be applied on top of pivot-generated rows.
- **[Tree](/grid/plugins/tree.md)** — Pivot replaces the entire row and column structure with aggregated data. Tree hierarchy cannot coexist with pivot aggregation.
- **[Server-Side](/grid/plugins/server-side.md)** — Pivot requires the full dataset to compute aggregations. Server-side lazy loading provides rows in blocks, so pivot aggregation cannot be performed client-side.
:::

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

## See Also

- [Row Grouping](/grid/plugins/grouping-rows.md) — Group flat data by field values
- [Server-Side Plugin](/grid/plugins/server-side.md) — Lazy loading and remote data
