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
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,
  },
};
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.

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.
  • 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.
  • Confusing column subtotals with collapsed-column placeholders. Those are different features.

Continue with Values On Rows or Drill-Down.