Skip to content

Pivot Use Case

This page is a guided tour of the more advanced combinations available in the current Pivot implementation. Use it after you understand the basic model and want to see several features working together.

Source code
TypeScript ts
// src/components/showcase-pivot/PivotShowcase.ts

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

import { type PivotConfig } from '@revolist/revogrid-enterprise';
import { ECOMMERCE_COLUMNS, ECOMMERCE_COLUMNS_TYPES } from '../sys-data/ecommerce.columns';
import { ECOMMERCE_SHOWCASE_PIVOT, PIVOT_SHOWCASE_PLUGINS } from '../sys-data/ecommerce.pivot';
import { currentTheme } from '../composables/useRandomData';

export function load(parentSelector: string, rows: any[] | { isDark?: boolean } = []) {
  const { isDark } = currentTheme();
  // Ensure rows is an array with data, otherwise fallback to default data
  const data = Array.isArray(rows) && rows.length > 0 ? rows : [];

  const parent = document.querySelector(parentSelector);
  if (!parent) return;

  // State
  let newCfg: PivotConfig = { ...ECOMMERCE_SHOWCASE_PIVOT };
  let pivot: PivotConfig | null = { ...ECOMMERCE_SHOWCASE_PIVOT };
  let rowGroupingEnabled = false;

  function getConfiguredPivot(config: PivotConfig | null): PivotConfig | undefined {
    if (!config) return undefined;
    const rows = config.rows || [];
    const nextConfig: PivotConfig = {
      ...config,
      rows,
    };

    if (!rowGroupingEnabled) {
      delete nextConfig.collapsed;
      delete nextConfig.expanded;
    } else if (!nextConfig.expanded) {
      nextConfig.expanded = getInitialExpandedGroups(data, rows);
    }

    return nextConfig;
  }

  // Container
  const container = document.createElement('div');
  container.className = 'grow flex flex-col gap-2 h-full';

  // Top bar
  const topBar = document.createElement('div');
  topBar.className = 'flex justify-between mb-3 pt-2 relative';

  const leftDiv = document.createElement('div');
  leftDiv.className = 'flex gap-2 flex-wrap';

  const rowGroupingSpan = document.createElement('span');
  rowGroupingSpan.className = 'flex ml-2 gap-1 items-center';

  const rowGroupingLabel = document.createElement('label');
  rowGroupingLabel.className = 'rv-switch-label';

  const rowGroupingToggle = document.createElement('input');
  rowGroupingToggle.type = 'checkbox';
  rowGroupingToggle.className = 'rv-switch-input';
  rowGroupingToggle.checked = false;

  const rowGroupingTrack = document.createElement('span');
  rowGroupingTrack.className = 'rv-switch-track';
  const rowGroupingThumb = document.createElement('span');
  rowGroupingThumb.className = 'rv-switch-thumb';
  rowGroupingTrack.appendChild(rowGroupingThumb);

  rowGroupingLabel.appendChild(rowGroupingToggle);
  rowGroupingLabel.appendChild(rowGroupingTrack);
  rowGroupingLabel.appendChild(document.createTextNode('Row grouping'));
  rowGroupingSpan.appendChild(rowGroupingLabel);
  leftDiv.appendChild(rowGroupingSpan);

  topBar.appendChild(leftDiv);
  container.appendChild(topBar);

  // Grid container
  const gridContainer = document.createElement('div');
  gridContainer.className = 'pivot-grid-container grow overflow-hidden flex flex-col';

  const grid = document.createElement('revo-grid') as any;
  grid.className = 'overflow-hidden skip-style flex-1 min-h-0 cell-border';
  grid.hideAttribution = true;
  grid.range = true;
  grid.resize = true;
  grid.filter = true;
  grid.colSize = 200;
  grid.readonly = true;
  grid.theme = isDark() ? 'darkMaterial' : 'material';
  grid.columns = ECOMMERCE_COLUMNS;
  grid.columnTypes = ECOMMERCE_COLUMNS_TYPES;
  grid.plugins = PIVOT_SHOWCASE_PLUGINS;
  Object.assign(grid, { pivot: getConfiguredPivot(pivot) })

  const onPivotConfigUpdate = (e: any) => {
    newCfg = e.detail || { ...ECOMMERCE_SHOWCASE_PIVOT };
    pivot = newCfg;
    Object.assign(grid, { pivot: getConfiguredPivot(pivot) })
  };

  grid.addEventListener('pivot-config-update', onPivotConfigUpdate);

  const onRowGroupingChange = (e: any) => {
    rowGroupingEnabled = (e.target as HTMLInputElement).checked;
    applyPivotOptions();
  };

  function applyPivotOptions() {
    Object.assign(grid, { pivot: getConfiguredPivot(pivot) })
  }

  rowGroupingToggle.addEventListener('change', onRowGroupingChange);

  grid.source = data;

  gridContainer.appendChild(grid);
  container.appendChild(gridContainer);
  parent.appendChild(container);

  return () => {
    grid.removeEventListener('pivot-config-update', onPivotConfigUpdate);
    rowGroupingToggle.removeEventListener('change', onRowGroupingChange);
    container.remove();
  };
}

function getInitialExpandedGroups(data: any[], rows: Array<string | number>) {
  const expanded: Record<string, boolean> = {};
  const groupingDepth = Math.max(0, rows.length - 1);

  data.forEach((row) => {
    const path: Array<unknown> = [];
    for (let index = 0; index < groupingDepth; index += 1) {
      path.push(row[rows[index]] ?? null);
      expanded[path.join(',')] = true;
    }
  });

  return expanded;
}
React tsx
// src/components/showcase-pivot/PivotShowcase.tsx

import { useState, useMemo, useEffect, useRef } from 'react';
import { RevoGrid, type DataType } from '@revolist/react-datagrid';
import { type PivotConfig } from '@revolist/revogrid-enterprise';
import { currentTheme } from '../composables/useRandomData';
import { ECOMMERCE_COLUMNS, ECOMMERCE_COLUMNS_TYPES } from '../sys-data/ecommerce.columns';
import { ECOMMERCE_SHOWCASE_PIVOT, PIVOT_SHOWCASE_PLUGINS } from '../sys-data/ecommerce.pivot';

interface PivotProps {
  rows: DataType[];
}

function PivotShowcase({ rows }: PivotProps) {
  const { isDark } = currentTheme();

  const [pivotConfig, setPivotConfig] = useState<PivotConfig | null>({ ...ECOMMERCE_SHOWCASE_PIVOT });
  const newCfgRef = useRef<PivotConfig>({ ...ECOMMERCE_SHOWCASE_PIVOT });

  const [rowGroupingEnabled, setRowGroupingEnabled] = useState(false);

  const effectivePivot = useMemo(() => {
    return applyShowcasePivotOptions(pivotConfig, rowGroupingEnabled, rows);
  }, [pivotConfig, rowGroupingEnabled, rows]);

  const pivot = useMemo(() => effectivePivot, [effectivePivot]);
  const plugins = useMemo(() => PIVOT_SHOWCASE_PLUGINS, []);

  const gridRef = useRef<HTMLRevoGridElement>(null);
  useEffect(() => {
    const grid = gridRef.current;
    if (!grid) return;
    const handler = (e: Event) => {
      const detail = (e as CustomEvent<PivotConfig>).detail || { ...ECOMMERCE_SHOWCASE_PIVOT };
      newCfgRef.current = detail;
      setPivotConfig(detail);
    };
    grid.addEventListener('pivot-config-update', handler);
    return () => grid.removeEventListener('pivot-config-update', handler);
  }, []);

  return (
    <div className="grow flex flex-col gap-2 h-full">
      <div className="flex justify-between mb-3 pt-2 relative">
        <div className="flex gap-2 flex-wrap">
          {pivotConfig && (
            <>
              <label className="rv-switch-label ml-2">
                <input
                  className="rv-switch-input"
                  type="checkbox"
                  checked={rowGroupingEnabled}
                  onChange={(e) => setRowGroupingEnabled(e.target.checked)}
                />
                <span className="rv-switch-track"><span className="rv-switch-thumb" /></span>
                Row grouping
              </label>
            </>
          )}
        </div>
      </div>
      <div className="pivot-grid-container grow overflow-hidden">
        <RevoGrid
          ref={gridRef}
          className="overflow-hidden skip-style h-full min-h-0 cell-border"
          hideAttribution
          range
          resize
          filter
          colSize={200}
          source={rows}
          columns={ECOMMERCE_COLUMNS}
          pivot={pivot}
          theme={isDark() ? 'darkMaterial' : 'material'}
          plugins={plugins}
          columnTypes={ECOMMERCE_COLUMNS_TYPES}
          readonly
        />
      </div>
    </div>
  );
}

export default PivotShowcase;

function applyShowcasePivotOptions(
  config: PivotConfig | null,
  rowGroupingEnabled: boolean,
  data: DataType[],
): PivotConfig | undefined {
  if (!config) return undefined;
  const rows = config.rows || [];
  const nextConfig: PivotConfig = {
    ...config,
    rows,
  };

  if (!rowGroupingEnabled) {
    delete nextConfig.collapsed;
    delete nextConfig.expanded;
  } else if (!nextConfig.expanded) {
    nextConfig.expanded = getInitialExpandedGroups(data, rows);
  }

  return nextConfig;
}

function getInitialExpandedGroups(data: DataType[], rows: Array<string | number>) {
  const expanded: Record<string, boolean> = {};
  const groupingDepth = Math.max(0, rows.length - 1);

  data.forEach((row) => {
    const path: Array<unknown> = [];
    for (let index = 0; index < groupingDepth; index += 1) {
      path.push(row[rows[index]] ?? null);
      expanded[path.join(',')] = true;
    }
  });

  return expanded;
}
Vue vue
// src/components/showcase-pivot/PivotShowcase.vue
<template>
  <div class="grow flex flex-col gap-2 h-full">
    <div class="flex justify-between mb-3 pt-2 relative">
      <div class="flex gap-2 flex-wrap">
        <template v-if="pivotMode">
          <label class="rv-switch-label ml-2">
            <input v-model="rowGroupingEnabled" class="rv-switch-input" type="checkbox" />
            <span class="rv-switch-track"><span class="rv-switch-thumb"></span></span>
            Row grouping
          </label>
        </template>
      </div>
    </div>

    <div class="pivot-grid-container grow overflow-hidden">
      <RevoGrid
        class="overflow-hidden skip-style h-full min-h-0 cell-border"
        hide-attribution
        range
        resize
        filter
        :colSize="200"
        :source="rows"
        :columns="ECOMMERCE_COLUMNS"
        :pivot.prop="pivot"
        :theme="isDark ? 'darkMaterial' : 'material'"
        :plugins="PIVOT_SHOWCASE_PLUGINS"
        :column-types="columnTypes"
        readonly
        @pivot-config-update="configUpdate"
      />
    </div>
  </div>
</template>
<script setup lang="ts">
import { computed, ref, shallowRef } from 'vue';
import { currentThemeVue } from '../composables/useRandomData';
const { isDark } = currentThemeVue();

import RevoGrid from '@revolist/vue3-datagrid';
import type { PivotConfig } from '@revolist/revogrid-enterprise';

import { ECOMMERCE_COLUMNS, ECOMMERCE_COLUMNS_TYPES } from '../sys-data/ecommerce.columns';
import { ECOMMERCE_SHOWCASE_PIVOT, PIVOT_SHOWCASE_PLUGINS } from '../sys-data/ecommerce.pivot';

const props = defineProps({
  rows: {
    type: Array<any>,
    default: () => [],
  },
});

/**
 * Pivot config
 */
const pivotConfig = shallowRef<PivotConfig | null>({
  ...ECOMMERCE_SHOWCASE_PIVOT,
});

const configuredPivot = computed(() => {
  return applyShowcasePivotOptions(
    pivotConfig.value,
    rowGroupingEnabled.value,
    props.rows,
  );
});

/**
 * Grid column type properties
 */
const columnTypes = ref(ECOMMERCE_COLUMNS_TYPES);

/**
 * Init plugins
 */
const pivot = computed(() => configuredPivot.value);

/**
 * Non reactive, because it updates itself
 */
let newCfg: PivotConfig = {
  ...ECOMMERCE_SHOWCASE_PIVOT,
};
const configUpdate = (e: CustomEvent<PivotConfig>) => {
  newCfg = e.detail || {
    ...ECOMMERCE_SHOWCASE_PIVOT,
  };
  pivotConfig.value = newCfg;
};


const rowGroupingEnabled = ref(false);

/**
 * Pivot mode disable toggle
 */
const pivotMode = computed({
  get: () => !!pivotConfig.value,
  set: (value) => {
    pivotConfig.value = value ? newCfg : null;
  },
});

function applyShowcasePivotOptions(
  config: PivotConfig | null,
  selectedRowGroupingEnabled: boolean,
  data: any[],
): PivotConfig | undefined {
  if (!config) return undefined;
  const rows = config.rows || [];
  const nextConfig: PivotConfig = {
    ...config,
    rows,
  };

  if (!selectedRowGroupingEnabled) {
    delete nextConfig.collapsed;
    delete nextConfig.expanded;
  } else if (!nextConfig.expanded) {
    nextConfig.expanded = getInitialExpandedGroups(data, rows);
  }

  return nextConfig;
}

function getInitialExpandedGroups(data: any[], rows: Array<string | number>) {
  const expanded: Record<string, boolean> = {};
  const groupingDepth = Math.max(0, rows.length - 1);

  data.forEach((row) => {
    const path: Array<unknown> = [];
    for (let index = 0; index < groupingDepth; index += 1) {
      path.push(row[rows[index]] ?? null);
      expanded[path.join(',')] = true;
    }
  });

  return expanded;
}
</script>

<style lang="scss">
revo-grid.cell-border .rgHeaderCell[highlight] {
  box-shadow: 0 -3px 0 0 #00b997 inset, -1px 0 0 0 var(--revo-grid-cell-border) inset;
}
</style>
Angular ts
// src/components/showcase-pivot/PivotShowcaseAngular.ts
import {
  Component,
  Input,
  ChangeDetectionStrategy,
  signal,
  computed,
  NO_ERRORS_SCHEMA,
} from '@angular/core';
import { CommonModule } from '@angular/common';
import { RevoGrid, type DataType } from '@revolist/angular-datagrid';
import { type PivotConfig } from '@revolist/revogrid-enterprise';
import { ECOMMERCE_COLUMNS, ECOMMERCE_COLUMNS_TYPES } from '../sys-data/ecommerce.columns';
import { ECOMMERCE_SHOWCASE_PIVOT, PIVOT_SHOWCASE_PLUGINS } from '../sys-data/ecommerce.pivot';
import { currentTheme } from '../composables/useRandomData';

@Component({
  selector: 'pivot-showcase-grid',
  standalone: true,
  imports: [RevoGrid, CommonModule],
  changeDetection: ChangeDetectionStrategy.OnPush,
  // Allows Angular demos to bind RevoGrid plugin props that are not wrapper inputs.
  schemas: [NO_ERRORS_SCHEMA],
  template: `
    <div class="grow flex flex-col gap-2 h-full">
      <div class="flex justify-between mb-3 pt-2 relative">
        <div class="flex gap-2 flex-wrap">
          @if (pivotMode()) {
            <label class="rv-switch-label ml-2">
              <input
                class="rv-switch-input"
                type="checkbox"
                [checked]="rowGroupingEnabled()"
                (change)="rowGroupingEnabled.set($any($event.target).checked)"
              />
              <span class="rv-switch-track"><span class="rv-switch-thumb"></span></span>
              Row grouping
            </label>
          }
        </div>
      </div>
      <div class="pivot-grid-container grow overflow-hidden">
        <revo-grid
          class="overflow-hidden skip-style h-full min-h-0 cell-border"
          [hideAttribution]="true"
          [range]="true"
          [resize]="true"
          [filter]="true"
          [colSize]="200"
          [source]="rows"
          [columns]="ECOMMERCE_COLUMNS"
          [pivot]="pivot()"
          [theme]="theme"
          [plugins]="plugins"
          [columnTypes]="columnTypes"
          [readonly]="true"
          (pivot-config-update)="configUpdate($event)"
        ></revo-grid>
      </div>
    </div>
  `,
})
export class PivotShowcaseGridComponent {
  @Input() rows: DataType[] = [];

  readonly ECOMMERCE_COLUMNS = ECOMMERCE_COLUMNS;
  readonly columnTypes = ECOMMERCE_COLUMNS_TYPES;
  readonly plugins = PIVOT_SHOWCASE_PLUGINS;
  readonly theme = currentTheme().isDark() ? 'darkMaterial' : 'material';

  private readonly pivotSignal = signal<PivotConfig | null>({ ...ECOMMERCE_SHOWCASE_PIVOT });
  readonly rowGroupingEnabled = signal(false);

  private readonly configuredPivot = computed(() => {
    return applyShowcasePivotOptions(
      this.pivotSignal(),
      this.rowGroupingEnabled(),
      this.rows,
    );
  });

  readonly pivot = computed(() => this.configuredPivot());

  readonly pivotMode = computed(() => !!this.pivotSignal());

  configUpdate(event: CustomEvent<PivotConfig>) {
    const newCfg = event.detail || { ...ECOMMERCE_SHOWCASE_PIVOT };
    this.pivotSignal.set(newCfg);
  }
}

function applyShowcasePivotOptions(
  config: PivotConfig | null,
  rowGroupingEnabled: boolean,
  data: DataType[],
): PivotConfig | undefined {
  if (!config) return undefined;
  const rows = config.rows || [];
  const nextConfig: PivotConfig = {
    ...config,
    rows,
  };

  if (!rowGroupingEnabled) {
    delete nextConfig.collapsed;
    delete nextConfig.expanded;
  } else if (!nextConfig.expanded) {
    nextConfig.expanded = getInitialExpandedGroups(data, rows);
  }

  return nextConfig;
}

function getInitialExpandedGroups(data: DataType[], rows: Array<string | number>) {
  const expanded: Record<string, boolean> = {};
  const groupingDepth = Math.max(0, rows.length - 1);

  data.forEach((row) => {
    const path: Array<unknown> = [];
    for (let index = 0; index < groupingDepth; index += 1) {
      path.push(row[rows[index]] ?? null);
      expanded[path.join(',')] = true;
    }
  });

  return expanded;
}
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',
  },
};
  • multiple row levels
  • generated column groups
  • multiple value measures
  • totals and subtotals
  • interactive configurator updates
  • pivot-config-update for external state sync

Watch how the layout changes as you:

  • move fields between rows, columns, and values
  • change aggregators
  • expand or collapse grouped rows
  • enable or disable totals

The example is useful because it shows that Pivot is not a separate reporting component. It is still RevoGrid, with generated analytical rows and columns layered on top.