Lattice Grid Buy a licence

developer guide

Data Grid Aggregate Functions by Column Type

Each column brings the aggregates its type can honestly support, so a percentage is not summed and a ratio is weighted rather than averaged. Your own total function covers the rest, and a reader can switch aggregate from the column itself.

Developer guideSorting, filtering and find › Data Grid Aggregate Functions by Column Type

Totals that a type can refuse

Some values do not add up the way plain numbers do. A data type can say which aggregates are meaningful for it, and supply its own arithmetic where the built-in one would be wrong.

The failure this prevents is a confident wrong number. You cannot add decibels: 90 dB and 90 dB make 93 dB, not 180. The mean of a column of rates is not the mean, a 100% conversion on two visits and a 1% conversion on ten thousand average to 1.02%, not 50.5%. Both mistakes produce a plausible figure rather than an error, and a footer nobody can check gets used. A missing total gets asked about; a wrong one does not.

A type declaring what it supports

{
  base: 'number',
  totals: {
    // Anything else is refused when a column is configured, not at render.
    supported: ['sum', 'avg', 'min', 'max', 'count', 'countValues'],
    // And where the built-in arithmetic is wrong, replace it.
    implement: { sum: (values) => logDomainSum(values) },
  },
}

A type that declares no totals supports everything, so nothing that shipped before this behaves differently. An implement function receives the values index-aligned with their rows and a context carrying column and valueAt(colId, i), which is how a weighted mean reaches the denominators in another column.

The types that use it

TypeWhat it does differently
decibelSums and averages in the linear domain and converts back. Power scale, factor 10.
decibelAmplitudeThe same, on the field scale: factor 20, for voltage and current.
ratioAverages by weight, using the column named in typeOptions.weight. Refuses sum, since two rates do not add to a rate.
percentRateAs ratio, displayed with a percent sign.

A conversion rate averaged properly

{ field: 'conversion', type: 'percentRate', total: 'avg',
  typeOptions: { weight: 'visits' } }

Without a weight column the average returns nothing rather than falling back to the unweighted mean: falling back would be the exact mistake the type exists to prevent, arrived at silently. Rows with no rate, or no weight, are left out rather than counted as zero.

Choosing an aggregate at runtime

With aggregateChooser on, the column menu's totalling entry becomes an Aggregate submenu. It offers only the reductions the column's type says are meaningful (see above) - sum, avg, min, max and the counts on a plain number, but never sum on a category or a rate - with the current one ticked and a None to stop totalling. It is the same keyboard-operable menu as everywhere else: arrows move, Enter or Space picks, Escape closes and returns focus, and the ticked item reads as aria-checked to a screen reader.

Off by default; turn it on

createGrid(element, { aggregateChooser: true });

It reuses the totals model, it does not fork it. Every choice drives grid.columns.setTotal(id, name), the same public call the old toggle used, so the footer, the group rows, the pivot cells and the grand total all move together and no aggregation is recomputed here. grid.columns.aggregates(id) returns the list the submenu offers, so a host building its own chooser reads the same answer.

Safety holds on both routes. The submenu only lists meaningful aggregates, and setTotal refuses an unmeaningful named total whether it comes from the menu or from an API caller - the wrong footer cannot be reached from either.

Off by default and non-breaking. Left off, the menu keeps its plain Total this column toggle, so an existing grid is unchanged.

Setting an aggregate at runtime, executed

const { createHeadlessGrid } = await import('../packages/core/src/index.js');

const grid = createHeadlessGrid({
  aggregateChooser: true,
  columns: [{ field: 'amount', type: 'number', total: 'sum' }, { field: 'region' }],
  rows: [{ id: '1', amount: 2, region: 'N' }, { id: '2', amount: 4, region: 'S' }],
  rowKey: 'id',
  grandTotalRow: 'inline',
});
grid.rows.count();

// A number column offers every built-in; a text column offers only what makes
// sense - count and the extremes, never sum. This is the list the chooser shows.
const offered = grid.columns.aggregates('amount');   // ['sum','avg','min',...]

// Switch the footer from Sum (6) to Average (3) at runtime.
grid.columns.setTotal('amount', 'avg');
const total = grid.rows.get(grid.rows.count() - 1).totals.amount;

grid.destroy();
return offered.includes('sum') ? 6 : 0;   // sum is offered on a number column

The group subtotals and the grand total can reduce differently. By default one total drives both, and that is unchanged. When a column needs, say, an average per group under a sum of everything, set the two independently with the scope option: setTotal(id, 'avg', { scope: 'group' }) and setTotal(id, 'sum', { scope: 'grand' }). A scope with no override falls back to total, and passing no scope sets the shared total and clears both overrides - so the one-property behaviour is exactly what it was. The same split is declarable on a column as groupTotal / grandTotal, and it persists in saved views alongside total.

Group average under a grand sum, executed

const { createHeadlessGrid } = await import('../packages/core/src/index.js');

const grid = createHeadlessGrid({
  columns: [{ field: 'amount', type: 'number', total: 'sum' }, { field: 'region' }],
  rows: [
    { id: '1', amount: 2, region: 'N' }, { id: '2', amount: 6, region: 'N' },
    { id: '3', amount: 4, region: 'S' }, { id: '4', amount: 8, region: 'S' },
  ],
  rowKey: 'id',
  grandTotalRow: true,
});
grid.columns.group(['region']);

// Group subtotals average within each region; the grand total sums everything.
grid.columns.setTotal('amount', 'avg', { scope: 'group' });
grid.columns.setTotal('amount', 'sum', { scope: 'grand' });

// The first group subtotal (region N: (2+6)/2 = 4) and the grand row (sum = 20).
let groupSubtotal = null;
for (let i = 0; i < grid.rows.count(); i++) {
  const r = grid.rows.get(i);
  if (r && r.group && !r.grandTotal && groupSubtotal === null) groupSubtotal = r.totals.amount;
}
const grand = grid.rows.get(grid.rows.count() - 1).totals.amount;

grid.destroy();
return (groupSubtotal === 4 && grand === 20)
  ? 'avg groups under sum grand' : `group ${groupSubtotal}, grand ${grand}`;