Skip to content

Pivot Overview

Pivot turns a flat fact table into an analytical view. You choose which fields define the row hierarchy, which fields define generated columns, and which fields become aggregated measures. RevoGrid stays responsible for rendering and interaction, while Pivot builds the grouped rows, generated headers, totals, and summary cells.

For raw type signatures, see Pivot API and Pivot Config API. Use this guide set for behavior, defaults, and how the parts work together.

Source code
TypeScript ts
// src/components/pivot/Pivot.ts

import { defineCustomElements } from '@revolist/revogrid/loader';
defineCustomElements();

import { PivotPlugin } from '@revolist/revogrid-enterprise';
import { AdvanceFilterPlugin, RowOddPlugin } from '@revolist/revogrid-pro';
import NumberColumnType from '@revolist/revogrid-column-numeral';
import { ECOMMERCE_PIVOT } from '../sys-data/ecommerce.pivot';
import { currentTheme } from '../composables/useRandomData';
import type { DataType } from '@revolist/revogrid';

export function load(parentSelector: string, rows: DataType[]) {
  const { isDark } = currentTheme();
  const grid = document.createElement('revo-grid');
  grid.className = 'grow h-full w-full cell-border';


  grid.range = true;
  grid.resize = true;
  grid.filter = true;
  grid.colSize = 200;
  grid.readonly = true;
  grid.hideAttribution = true;
  grid.theme = isDark() ? 'darkCompact' : 'compact';
  grid.source = rows;

  grid.columnTypes = {
    integer: new NumberColumnType(),
    currency: new NumberColumnType('$0,0.00'),
  };

  grid.plugins = [PivotPlugin, AdvanceFilterPlugin, RowOddPlugin];

  Object.assign(grid, {
    pivot: { ...ECOMMERCE_PIVOT },
  })

  document.querySelector(parentSelector)?.appendChild(grid);
}
Vue vue
// src/components/pivot/Pivot.vue
<template>
    <RevoGrid
    class="grow h-full cell-border"
      hide-attribution
      range
      resize
      filter
      :colSize="200"
      :source="rows"
      :pivot.prop="pivot"
      :theme="isDark ? 'darkCompact' : 'compact'"
      :plugins="plugins"
      :column-types="columnTypes"
      readonly
    />
</template>

<script setup lang="ts">
import { computed, ref } from 'vue';
import { currentThemeVue } from '../composables/useRandomData';
const { isDark } = currentThemeVue();

import NumberColumnType from '@revolist/revogrid-column-numeral';
import RevoGrid, {
  type DataType,
  type GridPlugin,
} from '@revolist/vue3-datagrid';

import {
  PivotPlugin,
} from '@revolist/revogrid-enterprise';
import {
  AdvanceFilterPlugin,
  RowOddPlugin,
} from '@revolist/revogrid-pro';

import { ECOMMERCE_PIVOT } from '../sys-data/ecommerce.pivot';

const props = defineProps<{
  rows: DataType[];
  hasConfigurator?: boolean;
}>();

const columnTypes = ref({
  integer: new NumberColumnType(),
  currency: new NumberColumnType('$0,0.00'),
});

const plugins: GridPlugin[] = [PivotPlugin, AdvanceFilterPlugin, RowOddPlugin];
const pivot = computed(() => {
  return {
    ...ECOMMERCE_PIVOT,
    hasConfigurator: props.hasConfigurator,
  };
});

</script>
React tsx
// src/components/pivot/Pivot.tsx

import { PivotPlugin } from '@revolist/revogrid-enterprise';
import { AdvanceFilterPlugin, RowOddPlugin } from '@revolist/revogrid-pro';
import { useMemo } from 'react';
import { RevoGrid, type DataType } from '@revolist/react-datagrid';
import NumberColumnType from '@revolist/revogrid-column-numeral';
import { ECOMMERCE_PIVOT } from '../sys-data/ecommerce.pivot';
import { currentTheme } from '../composables/useRandomData';

interface PivotProps {
  rows?: DataType[];
}

function Pivot({ rows }: PivotProps) {
  const { isDark } = currentTheme();
  const data = useMemo(() => (Array.isArray(rows) && rows.length > 0 ? rows : []), [rows]);

  const columnTypes = useMemo(
    () => ({
      integer: new NumberColumnType(),
      currency: new NumberColumnType('$0,0.00'),
    }),
    []
  );

  const plugins = useMemo(() => [PivotPlugin, AdvanceFilterPlugin, RowOddPlugin], []);
  const pivot = useMemo(
    () => ({ ...ECOMMERCE_PIVOT }),
    []
  );

  return (
    <RevoGrid
      className="grow h-full cell-border"
      hideAttribution
      range
      resize
      filter
      colSize={200}
      source={data}
      columns={[]}
      pivot={pivot}
      theme={isDark() ? 'darkCompact' : 'compact'}
      plugins={plugins}
      columnTypes={columnTypes}
      readonly
    />
  );
}

export default Pivot;
Angular ts
// src/components/pivot/PivotAngular.ts

import { Component, ViewEncapsulation, Input, NO_ERRORS_SCHEMA } from '@angular/core';
import { RevoGrid, type DataType } from '@revolist/angular-datagrid';
import NumberColumnType from '@revolist/revogrid-column-numeral';
import { PivotPlugin } from '@revolist/revogrid-enterprise';
import { AdvanceFilterPlugin, RowOddPlugin } from '@revolist/revogrid-pro';
import { ECOMMERCE_PIVOT } from '../sys-data/ecommerce.pivot';
import { currentTheme } from '../composables/useRandomData';

@Component({
  selector: 'pivot-grid',
  standalone: true,
  imports: [RevoGrid],
  template: `
    <revo-grid
      class="grow h-full cell-border"
      [hideAttribution]="true"
      [range]="true"
      [resize]="true"
      [filter]="true"
      [colSize]="200"
      [source]="rows"
      [pivot]="pivot"
      [theme]="theme"
      [plugins]="plugins"
      [columnTypes]="columnTypes"
      [readonly]="true"
    ></revo-grid>
  `,
  encapsulation: ViewEncapsulation.None,
  // Allows Angular demos to bind RevoGrid plugin props that are not wrapper inputs.
  schemas: [NO_ERRORS_SCHEMA],
})
export class PivotGridComponent {
  @Input() rows: DataType[] = [];

  theme = currentTheme().isDark() ? 'darkCompact' : 'compact';

  columnTypes = {
    integer: new NumberColumnType(),
    currency: new NumberColumnType('$0,0.00'),
  };

  plugins = [PivotPlugin, AdvanceFilterPlugin, RowOddPlugin];

  pivot = { ...ECOMMERCE_PIVOT };
}
Pivot Config ts
import type { GridPlugin } from '@revolist/revogrid';
import {
  type PivotConfigDimension,
  type PivotConfig,
  PivotPlugin,
} from '@revolist/revogrid-enterprise';
import {
  commonAggregators,
  ratingStarRenderer,
  progressLineWithValueRenderer,
  progressLineRenderer,
  changeRenderer,
  thresholdRenderer,
  mergeCellProperties,
  RowOddPlugin,
  AdvanceFilterPlugin,
  SameValueMergePlugin,
  RowSelectPlugin,
  FilterHeaderPlugin,
  ColumnCollapsePlugin,
} from '@revolist/revogrid-pro';

export const PIVOT_PLUGINS: GridPlugin[] = [FilterHeaderPlugin, RowSelectPlugin, SameValueMergePlugin, PivotPlugin, AdvanceFilterPlugin, RowOddPlugin] as any[]; 
export const PIVOT_SHOWCASE_PLUGINS: GridPlugin[] = [
  FilterHeaderPlugin,
  RowSelectPlugin,
  SameValueMergePlugin,
  PivotPlugin,
  ColumnCollapsePlugin,
  AdvanceFilterPlugin,
  RowOddPlugin,
] as any[];

export const ECOMMERCE_PIVOT: PivotConfig = {
  dimensions: [
    {
      prop: 'Age',
      columnType: 'integer',
      size: 100,
      sortable: true,
      merge: true,
      filterPlaceholder: 'Age?',
    },
    {
      prop: 'Time',
      columnType: 'time',
      size: 100,
      sortable: true,
      merge: true,
      columnProperties: (column) => {
        if (column.children && column.name === '00:00') {
          return {
            highlight: true,
          };
        }
      }
    },
    {
      prop: 'City',
      sortable: true,
      merge: true,
      filter: ['string', 'selection'],
      order: 'asc',
    },
    {
      prop: 'Gender',
      filter: ['string', 'selection'],
      sortable: true,
      merge: true,
    },
    {
      prop: 'Membership Type',
      filter: ['string', 'selection'],
      sortable: true,
      merge: true,
    },
    {
      prop: 'Total Spend',
      sortable: true,
      columnType: 'currency',
      filter: ['number'],
      filterPlaceholder: 'Total Spend?',
      thresholds: [
        { value: 900, className: 'high' },
        { value: 600, className: 'medium' },
      ],
      cellProperties: mergeCellProperties(thresholdRenderer, ({ value }) => ({
        class: {
          highlight: value > 20000,
          'align-right': true,
        },
      })),
      aggregators: {
        sum: commonAggregators.sum,
        avg: commonAggregators.avg,
        min: commonAggregators.min,
        max: commonAggregators.max,
      },
    },
    {
      name: 'Spend Change %',
      prop: 'Spend Change (%)',
      sortable: true,
      filter: ['number'],
      columnType: 'percent',
      cellTemplate: changeRenderer,
    },
    {
      name: 'Avg Rating',
      prop: 'Average Rating',
      filter: ['number', 'slider'],
      filterPlaceholder: 'Avg Rating?',
      sortable: true,
      maxStars: 5,
      maxValue: 5,
      thresholds: [
        { value: 4, className: 'high' },
        { value: 3, className: 'medium' },
        { value: 0, className: 'low' },
      ],
      cellParser: (model, column) => {
        const value = model[column.prop];
        if (Number(value)) {
          return value.toFixed(2);
        }
        return value;
      },
      cellTemplate(...args) {
        const column: PivotConfigDimension = args[1].column;
        if (column.dimension === 'values') {
          switch (column.aggregator) {
            case 'star':
              return ratingStarRenderer(...args);
            case 'prg':
              return progressLineRenderer(...args);
            case 'prgvalue':
              return progressLineWithValueRenderer(...args);
          }
        }
        return args[1].value;
      },
      aggregators: {
        prg: commonAggregators.avg,
        star: commonAggregators.avg,
        prgvalue: commonAggregators.avg,
      },
    },
    {
      prop: 'Discount Applied',
      sortable: true,
      merge: true,
      filter: ['selection'],
    },
  ],
  rows: ['City', 'Age'],
  columns: ['Time'],
  values: [
    {
      prop: 'Total Spend',
      aggregator: 'sum',
    },
    {
      prop: 'Average Rating',
      aggregator: 'prg',
    },
  ],
  hasConfigurator: true,
  flatHeaders: false,
};

export const ECOMMERCE_SHOWCASE_PIVOT: PivotConfig = {
  ...ECOMMERCE_PIVOT,
  rows: ['City', 'Membership Type'],
  columns: ['Time', 'Discount Applied'],
  values: [
    {
      prop: 'Total Spend',
      aggregator: 'sum',
    },
    {
      prop: 'Total Spend',
      aggregator: 'avg',
    },
    {
      prop: 'Average Rating',
      aggregator: 'prgvalue',
    },
  ],
  filters: ['Gender', 'Membership Type', 'Discount Applied'],
  collapsed: true,
  groupAggregations: true,
  columnCollapse: {
    enabled: true,
    collapsed: false,
    aggregator: {
      'Total Spend': 'sum',
      'Average Rating': 'prgvalue',
    },
    placeholder: 'Group Total',
  },
  totals: {
    subtotals: true,
    grandTotal: true,
    subtotalLabel: 'Subtotal',
    grandTotalLabel: 'Grand Total',
  },
};
Data ts
[{
  "Customer ID": "101",
  "Gender": "Female",
  "Age": "29",
  "City": "New York",
  "Membership Type": "Gold",
  "Total Spend": "1120.2",
  "Items Purchased": "14",
  "Average Rating": "4.6",
  "Discount Applied": "True",
  "Days Since Last Purchase": "25",
  "Satisfaction Level": "Satisfied",
  "Spend Change (%)": "12.020000000000005",
  "Savings": "112.02000000000001",
  "Items per Purchase": "0.5384615384615384",
  "Lifetime Value": "1680.3000000000002"
}, ...]

Use Pivot when your source rows represent detailed facts such as orders, transactions, tickets, or events, and users need to answer questions like:

  • “Sales by region and quarter”
  • “Count of incidents by team and priority”
  • “Average margin by category and month”
  • “Grand totals and subtotals across multiple hierarchies”

Use a normal grid when users mainly inspect or edit row-level records. Use Pivot when users need grouped analytical summaries.

  • dimension: A field that can be placed on the row axis, column axis, or used as a value source.
  • row field: A dimension listed in rows. It creates the vertical hierarchy.
  • column field: A dimension listed in columns. It creates generated headers.
  • value: A measure listed in values. It is aggregated into Pivot cells.
  • generated row: A synthetic row produced by Pivot, not a raw input row.
  • generated column: A synthetic column or column group produced by Pivot.
  • subtotal: A summary for one intermediate branch of a hierarchy.
  • grand total: A summary across the full filtered result.

Client-side Pivot follows this shape:

source rows
-> group by rows / columns
-> aggregate values
-> generate row hierarchy
-> generate column hierarchy
-> render with RevoGrid

Server-side Pivot keeps the same public config, but the aggregation step moves behind a remote engine boundary:

source rows
-> analytical engine
-> visible analytical window
-> RevoGrid

That means the same concepts stay in place even when the data becomes too large for full client-side materialization.

If you are new to Pivot tables or Business Intelligence (BI) concepts, we recommend starting with these conceptual guides:

  1. Install the Enterprise package.

    Terminal window
    pnpm add @revolist/revogrid-enterprise
  2. Import the plugin and any aggregators you want to expose.

    import {
    PivotPlugin,
    commonAggregators,
    type PivotConfig,
    } from '@revolist/revogrid-enterprise';
  3. Register PivotPlugin on the grid.

    grid.plugins = [PivotPlugin];
  4. Define a PivotConfig.

    const pivot: PivotConfig = {
    dimensions: [
    { prop: 'region', name: 'Region' },
    { prop: 'quarter', name: 'Quarter' },
    {
    prop: 'sales',
    name: 'Sales',
    aggregators: {
    ...commonAggregators,
    },
    },
    ],
    rows: ['region'],
    columns: ['quarter'],
    values: [{ prop: 'sales', aggregator: 'sum' }],
    };
  5. Apply it through the direct pivot binding.

    grid.pivot = pivot;

    additionalData.pivot is still observed for legacy integrations, but new code should bind pivot directly.

const pivot: PivotConfig = {
dimensions: [...],
rows: ['region', 'rep'],
columns: ['year', 'quarter'],
values: [{ prop: 'sales', aggregator: 'sum' }],
totals: { grandTotal: true, subtotals: true },
collapsed: true,
};
  • dimensions describes the reusable field metadata.
  • rows defines the row hierarchy from left to right.
  • columns defines the generated header hierarchy from left to right.
  • values defines which measures are aggregated in each visible cell.
  • totals enables grand totals and subtotals.
  • collapsed enables grouped row drill-down when the row hierarchy has multiple levels.
  • groupAggregations makes grouped Pivot rows show aggregate values across generated metric columns.

When Pivot is active, PivotPlugin temporarily owns:

  • the rendered source
  • the rendered columns
  • optional pinnedBottomSource for grand totals
  • grouped-row metadata used for drill-down

When Pivot is cleared, the plugin restores the original source, columns, grouping, and pinned totals.

Current Scope

  • Client-side Pivot rows, columns, values, totals, and drill-down are implemented.
  • Client-side grouped row aggregates are implemented through pivot.groupAggregations.
  • valuesOnRows, flat headers, and the drag-and-drop configurator are implemented.
  • Server-side Pivot exposes a first-class remote engine/store contract and plugin integration.
  • The docs below describe the current implementation in packages/enterprise/plugins/pivot and packages/enterprise/plugins/pivot-config.
  1. What Is Pivot?
  2. What is an OLAP Cube?
  3. Data Modeling for Pivot
  4. Dimensions
  5. Aggregations
  6. Configuration Reference
  7. Totals
  8. Values On Rows
  9. Drill-Down
  10. Sorting And Filtering
  11. Plugin Lifecycle
  12. Configurator
  13. Field Panel
  14. Server-Side Pivot
  15. Caching And Cache Keys
  16. OLAP Best Practices