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
TypeScript ts
// 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();
};
}
Vue vue
<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>
React tsx
// 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;
Angular ts
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 & Config ts
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.
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.
- 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.
- Confusing column subtotals with collapsed-column placeholders. Those are different features.
Continue with Values On Rows or Drill-Down.