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
// 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();
};
}
<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>
// 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;
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,
},
};
}
}
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,
},
};
The Totals Config
Section titled “The Totals Config”totals: { grandTotal: true, subtotals: true, grandTotalLabel: 'All Sales', subtotalLabel: 'Subtotal',}What Each Option Does
Section titled “What Each Option Does”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.
Smart Suppression
Section titled “Smart Suppression”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.
Row Totals
Section titled “Row Totals”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.
Column Totals
Section titled “Column Totals”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 With Drill-Down
Section titled “Column Totals With Drill-Down”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.

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

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

Drill-down is progressive: expanding January reveals only January’s measures. Sibling months stay collapsed and Year Total remains one Sales subtotal column.
Behavior Matrix
Section titled “Behavior Matrix”| Configuration | Collapsed group | Expanded group |
|---|---|---|
totals.subtotals: false | Uses the regular collapsed aggregate bucket. | Shows descendants without a parent subtotal column. |
totals.subtotals: true and the level subtotal is enabled | Uses the level subtotal as the aggregate carrier. | Shows the same subtotal plus the next visible descendant level. |
columnLevels[level].subtotal: false | Uses the regular collapsed aggregate bucket for that level. | Omits the subtotal for that level, even when other levels show subtotals. |
columnLevels[level].collapsible: false | The 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: true | Grand-total columns and the pinned grand-total row remain visible. | Grand totals remain independent from individual branch expansion. |
Recommended Per-Level Configuration
Section titled “Recommended Per-Level Configuration”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.
Resolution And Precedence
Section titled “Resolution And Precedence”The following rules make mixed configurations predictable:
totals.subtotalsis the global subtotal gate. When it isfalse, nocolumnLevels[level].subtotalsetting can enable a subtotal.- With global subtotals enabled, an explicit
columnLevels[level].subtotaloverridestotals.disabledSubtotals.columnsfor that level. - An explicit
columnLevels[level].collapsibleoverrides the globalcolumnCollapsesetting for that level. When omitted, the level inheritscolumnCollapse.enabled. columnLevels[level].collapsedcontrols the initial state for that level and falls back tocolumnCollapse.collapsed.- Persisted
collapsedColumnsstate wins over both initial-state defaults for the matching generated group. subtotalLabel,subtotalPosition,subtotalValues,filterable, andsortableapply only to their configured level.- Grand-total columns do not belong to a generated hierarchy level, so
columnLevelsdoes not change them.
Filtering And Sorting Through Collapse
Section titled “Filtering And Sorting Through Collapse”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: falseremoves aggregate filter controls for that levelsortable: falseremoves 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.
Header Spanning
Section titled “Header Spanning”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.
Example
Section titled “Example”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
Important Notes
Section titled “Important Notes”- 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.
Common Mistakes
Section titled “Common Mistakes”- Expecting
subtotals: trueto 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: truecan bypasstotals.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.