Skip to content

Totals

Totals help users move from detailed branch-level summaries to whole-table summaries. RevoGrid Pivot supports grand totals and subtotals through pivot.totals.

Source code
TypeScriptts
// src/components/pivot/PivotTotals.ts

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

import NumberColumnType from '@revolist/revogrid-column-numeral';
import { currentTheme } from '../composables/useRandomData';
import { PIVOT_TOTALS, PIVOT_TOTALS_PLUGINS, PIVOT_TOTALS_ROWS } from '../sys-data/pivot.totals';

export function load(parentSelector: string, rows: any[] | { isDark?: boolean } = PIVOT_TOTALS_ROWS) {
  const { isDark } = currentTheme();
  // Ensure rows is an array with data, otherwise fallback to default data
  const data = Array.isArray(rows) && rows.length > 0 ? rows : PIVOT_TOTALS_ROWS;
  const container = document.createElement('div');
  container.className = 'grow h-full flex flex-col gap-2';

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

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

  const track = document.createElement('span');
  track.className = 'rv-switch-track';

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

  label.appendChild(checkbox);
  label.appendChild(track);
  label.appendChild(document.createTextNode('Suppress redundant totals'));

  const grid = document.createElement('revo-grid') as any;

  grid.className = 'flex-1 min-h-0 w-full cell-border';
  grid.range = true;
  grid.resize = true;
  grid.filter = true;
  grid.colSize = 120;
  grid.readonly = true;
  grid.hideAttribution = true;
  grid.theme = isDark() ? 'darkCompact' : 'compact';
  grid.columnTypes = {
    currency: new NumberColumnType('$0,0.00'),
  };
  grid.plugins = PIVOT_TOTALS_PLUGINS;
  grid.columns = [];

  const updateConfig = () => {
    Object.assign(grid, {
      pivot: {
        ...PIVOT_TOTALS,
        totals: {
          ...PIVOT_TOTALS.totals,
          suppressSingleChildSubtotals: checkbox.checked,
          suppressGrandTotalWhenSingleLeaf: checkbox.checked,
        },
      },
    })
  };

  checkbox.addEventListener('change', updateConfig);
  updateConfig();

  container.appendChild(label);
  container.appendChild(grid);
  document.querySelector(parentSelector)?.appendChild(container);

  // Set source last after DOM attachment and config
  grid.source = data;

  return () => {
    checkbox.removeEventListener('change', updateConfig);
    container.remove();
  };
}
Vuevue
<template>
  <div class="grow h-full flex flex-col gap-2">
    <label class="rv-switch-label">
      <input v-model="suppressRedundantTotals" class="rv-switch-input" type="checkbox" />
      <span class="rv-switch-track"><span class="rv-switch-thumb" /></span>
      Suppress redundant totals
    </label>
    <RevoGrid
      class="flex-1 min-h-0 cell-border"
      hide-attribution
      range
      resize
      filter
      :colSize="120"
      :source="rows"
      :pivot.prop="pivot"
      :theme="isDark ? 'darkCompact' : 'compact'"
      :plugins="plugins"
      :column-types="columnTypes"
      readonly
    />
  </div>
</template>

<script setup lang="ts">
import { computed, ref } from 'vue';
import { currentThemeVue } from '../composables/useRandomData';
import NumberColumnType from '@revolist/revogrid-column-numeral';
import RevoGrid from '@revolist/vue3-datagrid';
import { PIVOT_TOTALS, PIVOT_TOTALS_PLUGINS, PIVOT_TOTALS_ROWS } from '../sys-data/pivot.totals';

const { isDark } = currentThemeVue();

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

const plugins = PIVOT_TOTALS_PLUGINS;
const rows = ref(PIVOT_TOTALS_ROWS);
const suppressRedundantTotals = ref(false);
const pivot = computed(() => ({
  ...PIVOT_TOTALS,
  totals: {
    ...PIVOT_TOTALS.totals,
    suppressSingleChildSubtotals: suppressRedundantTotals.value,
    suppressGrandTotalWhenSingleLeaf: suppressRedundantTotals.value,
  },
}));
</script>
Reacttsx
// src/components/pivot/PivotTotals.tsx

import { useMemo, useState } from 'react';
import { RevoGrid } from '@revolist/react-datagrid';
import NumberColumnType from '@revolist/revogrid-column-numeral';
import { currentTheme } from '../composables/useRandomData';
import { PIVOT_TOTALS, PIVOT_TOTALS_PLUGINS, PIVOT_TOTALS_ROWS } from '../sys-data/pivot.totals';

interface PivotTotalsProps {
  rows?: any[];
}

function PivotTotals({ rows }: PivotTotalsProps) {
  const { isDark } = currentTheme();
  const data = useMemo(() => (Array.isArray(rows) && rows.length > 0 ? rows : PIVOT_TOTALS_ROWS), [rows]);
  const [suppressRedundantTotals, setSuppressRedundantTotals] = useState(false);

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

  const plugins = useMemo(() => PIVOT_TOTALS_PLUGINS, []);
  const pivot = useMemo(
    () => ({
      ...PIVOT_TOTALS,
      totals: {
        ...PIVOT_TOTALS.totals,
        suppressSingleChildSubtotals: suppressRedundantTotals,
        suppressGrandTotalWhenSingleLeaf: suppressRedundantTotals,
      },
    }),
    [suppressRedundantTotals],
  );

  return (
    <div className="grow h-full flex flex-col gap-2">
      <label className="rv-switch-label">
        <input
          className="rv-switch-input"
          type="checkbox"
          checked={suppressRedundantTotals}
          onChange={e => setSuppressRedundantTotals(e.target.checked)}
        />
        <span className="rv-switch-track"><span className="rv-switch-thumb" /></span>
        Suppress redundant totals
      </label>
      <RevoGrid
        className="flex-1 min-h-0 cell-border"
        hideAttribution
        range
        resize
        filter
        colSize={120}
        source={data}
        columns={[]}
        pivot={pivot}
        theme={isDark() ? 'darkCompact' : 'compact'}
        plugins={plugins}
        columnTypes={columnTypes}
        readonly
      />
    </div>
  );
}

export default PivotTotals;
Angularts
import { Component, NO_ERRORS_SCHEMA, ViewEncapsulation } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { RevoGrid } from '@revolist/angular-datagrid';
import NumberColumnType from '@revolist/revogrid-column-numeral';
import { currentTheme } from '../composables/useRandomData';
import { PIVOT_TOTALS, PIVOT_TOTALS_PLUGINS, PIVOT_TOTALS_ROWS } from '../sys-data/pivot.totals';

@Component({
  selector: 'pivot-totals-grid',
  standalone: true,
  imports: [RevoGrid, FormsModule],
  // Allows Angular demos to bind RevoGrid plugin props that are not wrapper inputs.
  schemas: [NO_ERRORS_SCHEMA],
  template: `
    <div class="grow h-full flex flex-col gap-2" style="min-height: 640px">
      <label class="rv-switch-label">
        <input class="rv-switch-input" type="checkbox" [(ngModel)]="suppressRedundantTotals" (ngModelChange)="updateConfig()" />
        <span class="rv-switch-track"><span class="rv-switch-thumb"></span></span>
        Suppress redundant totals
      </label>
      <revo-grid
        class="flex-1 min-h-0 cell-border"
        style="min-height: 560px"
        [hideAttribution]="true"
        [range]="true"
        [resize]="true"
        [filter]="true"
        [colSize]="120"
        [source]="rows"
        [pivot]="pivot"
        [theme]="theme"
        [plugins]="plugins"
        [columnTypes]="columnTypes"
        [readonly]="true"
      ></revo-grid>
    </div>
  `,
  encapsulation: ViewEncapsulation.None,
})
export class PivotTotalsGridComponent {
  rows = PIVOT_TOTALS_ROWS;

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

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

  plugins = PIVOT_TOTALS_PLUGINS;
  suppressRedundantTotals = false;

  pivot = {
    ...PIVOT_TOTALS,
    totals: {
      ...PIVOT_TOTALS.totals,
      suppressSingleChildSubtotals: this.suppressRedundantTotals,
      suppressGrandTotalWhenSingleLeaf: this.suppressRedundantTotals,
    },
  };

  updateConfig() {
    this.pivot = {
      ...PIVOT_TOTALS,
      totals: {
        ...PIVOT_TOTALS.totals,
        suppressSingleChildSubtotals: this.suppressRedundantTotals,
        suppressGrandTotalWhenSingleLeaf: this.suppressRedundantTotals,
      },
    };
  }
}
Data & Configts
import type { GridPlugin } from '@revolist/revogrid';
import { PivotPlugin, type PivotConfig } from '@revolist/revogrid-enterprise';
import { AdvanceFilterPlugin, ColumnCollapsePlugin, RowOddPlugin, commonAggregators } from '@revolist/revogrid-pro';
import { PIVOT_TIME_RANGE_ROWS } from './pivot.shared';

export const PIVOT_TOTALS_ROWS = [
  ...PIVOT_TIME_RANGE_ROWS,
  {
    year: 2025,
    quarter: 'Q1',
    region: 'West',
    rep: 'Mia',
    sales: 42,
  },
];

export const PIVOT_TOTALS_PLUGINS: GridPlugin[] = [
  PivotPlugin,
  ColumnCollapsePlugin,
  AdvanceFilterPlugin,
  RowOddPlugin,
] as any[];

export const PIVOT_TOTALS: PivotConfig = {
  dimensions: [
    { prop: 'region', sortable: true },
    { prop: 'rep', sortable: true },
    { prop: 'year', sortable: true },
    { prop: 'quarter', sortable: true },
    {
      prop: 'sales',
      name: 'Sales',
      columnType: 'currency',
      aggregators: {
        sum: commonAggregators.sum,
        avg: commonAggregators.avg,
      },
    },
  ],
  rows: ['region', 'rep'],
  columns: ['year', 'quarter'],
  values: [{ prop: 'sales', aggregator: 'sum' }],
  collapsed: true,
  groupAggregations: true,
  columnCollapse: {
    collapsed: true,
    aggregator: 'sum',
  },
  totals: {
    subtotals: true,
    grandTotal: true,
  },
};
totals: {
grandTotal: true,
subtotals: true,
grandTotalLabel: 'All Sales',
subtotalLabel: 'Subtotal',
}
  • grandTotal: Adds a grand-total row. When column dimensions exist, it also adds grand-total value columns.
  • subtotals: Adds subtotal rows for intermediate row branches and subtotal value columns for intermediate column branches.
  • grandTotalLabel: Overrides the default "Grand Total" label.
  • subtotalLabel: Overrides the default "Subtotal" label.
  • suppressSingleChildSubtotals: Hides subtotal rows for single-source branches where the subtotal duplicates the only leaf.
  • suppressGrandTotalWhenSingleLeaf: Hides the grand-total row when the whole result is one source-backed leaf.

Use suppression when totals are enabled for broad reports, but small filtered results should avoid duplicate summary rows:

totals: {
subtotals: true,
grandTotal: true,
suppressSingleChildSubtotals: true,
suppressGrandTotalWhenSingleLeaf: true,
}

Suppression is conservative. Multiple source rows, multiple meaningful leaves, or disabled subtotals / grandTotal keep the existing total behavior.

When row fields exist:

  • subtotal rows are inserted after the descendants of each branch
  • the grand-total row is rendered separately and pinned at the bottom

This keeps the detailed body rows readable while still making the full-table total visible.

When column fields exist:

  • intermediate column paths can expose subtotal value columns
  • the full column hierarchy can expose grand-total value columns

Pivot does not create a separate total table. Totals become part of the generated analytical structure.

Column totals and column collapse are designed to work together. A collapsed group shows one aggregate carrier. Expanding that group keeps the parent aggregate visible as a subtotal column and reveals only the next configured level.

Financial Pivot with the Year level collapsed into one aggregate column for each year, followed by grand-total measure columns.

Collapsed years use one aggregate carrier per year. Grand-total columns remain independent from the branch collapse state.

Financial Pivot with 2023 expanded to a Year Total subtotal followed by individually collapsed month columns.

Expanding 2023 reveals its configured Year Total first, followed by the next-level month groups. The subtotal spans the otherwise unused lower header depth.

Financial Pivot with 2023 expanded and January expanded into Sales, Profit, and Units Sold while later months remain collapsed.

Drill-down is progressive: expanding January reveals only January’s measures. Sibling months stay collapsed and Year Total remains one Sales subtotal column.

ConfigurationCollapsed groupExpanded group
totals.subtotals: falseUses the regular collapsed aggregate bucket.Shows descendants without a parent subtotal column.
totals.subtotals: true and the level subtotal is enabledUses the level subtotal as the aggregate carrier.Shows the same subtotal plus the next visible descendant level.
columnLevels[level].subtotal: falseUses the regular collapsed aggregate bucket for that level.Omits the subtotal for that level, even when other levels show subtotals.
columnLevels[level].collapsible: falseThe level cannot enter a collapsed state.The level stays expanded; its subtotal can still be shown.
subtotalPosition: 'before'Position is not visible while the branch is compact.Places the parent subtotal before descendant columns. This is the default.
subtotalPosition: 'after'Position is not visible while the branch is compact.Places the parent subtotal after descendant columns.
subtotalValues: ['sales']The compact carrier exposes only Sales for that subtotal.The subtotal contains only Sales, while descendant groups can still expose every configured measure.
totals.grandTotal: trueGrand-total columns and the pinned grand-total row remain visible.Grand totals remain independent from individual branch expansion.

This configuration matches the screenshots:

const pivot: PivotConfig = {
rows: ['country', 'segment'],
columns: ['year', 'month'],
values: [
{ prop: 'sales', aggregator: 'sum' },
{ prop: 'profit', aggregator: 'sum' },
{ prop: 'unitsSold', aggregator: 'sum' },
],
columnCollapse: {
enabled: true,
collapsed: true,
},
columnLevels: {
0: {
collapsible: true,
collapsed: true,
subtotal: true,
subtotalLabel: 'Year Total',
subtotalPosition: 'before',
subtotalValues: ['sales'],
filterable: true,
sortable: true,
},
1: {
collapsible: true,
collapsed: true,
subtotal: false,
filterable: false,
sortable: false,
},
},
totals: {
subtotals: true,
grandTotal: true,
},
};

columnLevels uses the zero-based order of columns: level 0 configures year, level 1 configures month, and so on.

The following rules make mixed configurations predictable:

  1. totals.subtotals is the global subtotal gate. When it is false, no columnLevels[level].subtotal setting can enable a subtotal.
  2. With global subtotals enabled, an explicit columnLevels[level].subtotal overrides totals.disabledSubtotals.columns for that level.
  3. An explicit columnLevels[level].collapsible overrides the global columnCollapse setting for that level. When omitted, the level inherits columnCollapse.enabled.
  4. columnLevels[level].collapsed controls the initial state for that level and falls back to columnCollapse.collapsed.
  5. Persisted collapsedColumns state wins over both initial-state defaults for the matching generated group.
  6. subtotalLabel, subtotalPosition, subtotalValues, filterable, and sortable apply only to their configured level.
  7. Grand-total columns do not belong to a generated hierarchy level, so columnLevels does not change them.

The subtotal or collapsed aggregate represents one analytical group across both visual states. A filter or sort applied to that aggregate therefore stays active when the group expands or collapses:

  • expanding does not broaden the filter to every newly visible child column
  • collapsing does not discard the active filter or sort direction
  • the active control moves between the compact carrier, parent group proxy, and visible subtotal without changing its analytical property
  • filterable: false removes aggregate filter controls for that level
  • sortable: false removes aggregate sorting for that level

The expand/collapse control appears before the group title. Expanding a group reveals one hierarchy level only; it does not expand its descendants or sibling branches.

A direct subtotal such as Year Total fills the unused descendant header rows vertically. It remains one value column; it does not span horizontally across the descendant columns. This keeps the subtotal aligned with deeper headers without covering the parent expand/collapse control.

For:

rows: ['region', 'rep'],
columns: ['year', 'quarter'],
values: [{ prop: 'sales', aggregator: 'sum' }],
totals: { grandTotal: true, subtotals: true },

You get:

  • leaf rows for each region -> rep
  • subtotal rows for each region
  • grand total at the bottom
  • leaf value columns for each year -> quarter
  • subtotal value columns for each year
  • grand-total value columns across all years and quarters
  • Subtotals only appear when a hierarchy has more than one level to summarize.
  • Grand totals use the configured label only for display. The internal total keys stay synthetic.
  • A column subtotal remains the same analytical aggregate through collapse and expansion; only its header presentation changes.
  • Per-level subtotal, collapse, filter, and sort settings are independent.
  • In remote/server mode, totals stay part of the analytical contract and should be computed by the engine, not recomputed from visible UI cells.
  • Expecting subtotals: true to add subtotal rows when there is only one row field.
  • Expecting grand totals to stay inside the body rows. In the current implementation, grand total rows are pinned bottom rows.
  • Assuming subtotal: true can bypass totals.subtotals: false.
  • Assuming expanding a year should expand every month below it.
  • Confusing a regular collapsed bucket with a subtotal carrier. They look similarly compact, but only a level with an enabled subtotal exposes that same subtotal as a visible column after expansion.

Continue with Values On Rows or Drill-Down.