Aggregation
Aggregation
An aggregation folds many cell values of one column into a single value: a sum, a mean, a count, a min and max pair. Group rows use aggregations to show something for the rows hidden inside them, and a table footer uses one to show a grand total. This page ports TanStack Table's Aggregation (React) Guide.
Aggregation is separate from Grouping. You can put a total in a footer without ever grouping a row, and you can group rows without configuring any aggregation.
The built-in functions live in
Table.AggregationFn and are plain
values you hand to
withAggregationFn. There is no
registry and no string names.
State
Aggregation owns no state slice. It reads State.grouping to know which
columns are grouped, because that decides which rows are group rows and which
of their cells are aggregated:
-- in Table.State
grouping : List String
-- in Table.initialState
grouping = []
Config options
There is no aggregation-specific Config field. The two grouping fields
below decide whether group rows exist at all, and so whether any aggregated
cell exists:
| Option | Type | Default | Description |
|---|---|---|---|
enableGrouping |
Bool |
True |
With it False, no column can be grouped and the grouped stage produces no group rows. |
manualGrouping |
Bool |
False |
Skip the grouped stage. Footer totals through aggregationValue still work. |
TanStack's manualAggregation option, which turns off the local calculation
so a server value is used instead, has no counterpart here. To show a server
total, put it in your own model and render it.
Column options
| Builder | Type | Default | Description |
|---|---|---|---|
withAggregationFn |
AggregationFn -> Column row -> Column row |
none | The aggregation for this column. |
withMaxAggregationDepth |
Int -> Column row -> Column row |
0 |
How far below the rows being aggregated to look for values. |
Leaving withAggregationFn off is TanStack's aggregationFn: 'auto'. The
automatic choice reads the first row of the row model handed in: sum for a
Number column, extent for a Date column, and no aggregation for
anything else.
columns : List (Table.Column Person)
columns =
[ Table.column "department" (.department >> Value.String)
|> Table.withHeader "Department"
, Table.column "salary" (.salary >> Value.Number)
|> Table.withHeader "Salary"
|> Table.withAggregationFn AggregationFn.sum
, Table.column "age" (.age >> toFloat >> Value.Number)
|> Table.withHeader "Age"
, Table.column "active" (.active >> Value.Bool)
|> Table.withHeader "Active"
|> Table.withAggregationFn AggregationFn.count
]
withMaxAggregationDepth picks a frontier below the starting rows. 0, the
default, aggregates the starting rows themselves. 1 aggregates their direct
sub-rows. A branch that ends before the depth is reached contributes its own
deepest row, so nothing is silently dropped.
deepestOnly : Table.Column Person
deepestOnly =
Table.column "salary" (.salary >> Value.Number)
|> Table.withAggregationFn AggregationFn.sum
|> Table.withMaxAggregationDepth 1
Built-in aggregations
| Function | Result | Has a merge |
|---|---|---|
sum |
Sum of Number values; anything else counts as zero. |
yes |
min |
Smallest Number or Date, Null when there is none. |
yes |
max |
Largest Number or Date, Null when there is none. |
yes |
extent |
Value.List [ min, max ], or [ Null, Null ] when there is no comparable value. |
yes |
mean |
Arithmetic mean of numeric values, Null when there is none. |
no |
median |
Median of Number values, Null when there is none. |
no |
unique |
Distinct values in first-seen order, as a Value.List. |
no |
uniqueCount |
How many distinct values there are. | no |
count |
How many rows there are. | yes |
first |
The first row's value, Null for no rows. |
yes |
last |
The last row's value, Null for no rows. |
yes |
Custom aggregations and merge
custom builds an
aggregation from a fold over the values:
tagList : AggregationFn
tagList =
AggregationFn.custom
(\values -> Value.String (String.join ", " (List.map Value.toString values)))
withMerge adds a second
fold, and this is what the "Has a merge" column above is about. When groups
nest, a parent group can combine the values its child groups already computed
instead of re-reading every leaf row. A sum of sums is the same number as a
sum of everything, so sum carries a merge. A mean of means is not the same
number as a mean of everything, so mean does not, and a nested mean folds
the leaf frontier directly.
runningTotal : AggregationFn
runningTotal =
AggregationFn.custom (\values -> Value.Number (sumOf values))
|> AggregationFn.withMerge (\subResults -> Value.Number (sumOf subResults))
The merge runs exactly where TanStack runs it: only when a group has sub-rows, every one of them is a group row of a different column, and the aggregation carries a merge.
To run a fold outside the table entirely, reach for
aggregate (the leaf fold)
or merge (the child fold, as
a Maybe):
salaryTotalOfList : List Value -> Value
salaryTotalOfList =
AggregationFn.aggregate AggregationFn.sum
Transitions
Aggregation has no state transitions. Nothing about it is stored in State;
it is computed from the Config, the rows, and State.grouping every time
you ask.
Queries
| Function | Signature | Description |
|---|---|---|
getAutoAggregationFn |
Config row -> RowModel row -> String -> Maybe AggregationFn |
The automatic choice for a column, read off the first row of the model. |
getAggregationFn |
Config row -> RowModel row -> String -> Maybe AggregationFn |
The column's own function, or the automatic one. |
aggregationValue |
Config row -> RowModel row -> String -> Value |
Aggregate one column over a row model, at the column's own depth. TanStack's column.getAggregationValue(). |
aggregationValueOf |
Config row -> RowModel row -> String -> { maxDepth : Int, rows : List (Row row) } -> Value |
Aggregate one column over a row list and depth you choose. TanStack's column.getAggregationValue({ rows, maxDepth }). |
cellIsAggregated |
Config row -> RowModel row -> State -> Row row -> String -> Bool |
Is this cell an aggregated one? |
rowAggregatedValues |
Row row -> Dict String Value |
The whole aggregated map of a group row, keyed by column id. |
maxSubRowDepth |
RowModel row -> Int |
The deepest structural depth in a row model, for picking a maxDepth. |
Both aggregationValue and aggregationValueOf take a RowModel because an
automatic aggregation function is chosen by sampling a row.
aggregationValueOf also drops duplicate row ids from the list you give it,
since a caller-supplied list can hold a row and one of its own ancestors;
aggregationValue reads a whole row model, so it skips that check.
A footer total
Call aggregationValue with the
pre-grouped row model, which is the filtered one, to get TanStack's default
grand total: filtering counts, grouping and sorting and pagination do not.
preGrouped : Table.State -> Table.RowModel Person
preGrouped state =
Table.preGroupedRowModel config state (Table.coreRowModelFromList config state people)
viewFooterCell : Table.RowModel Person -> Table.Column Person -> Html msg
viewFooterCell model column =
let
columnId : String
columnId =
Table.columnId column
in
case Table.getAggregationFn config model columnId of
Just _ ->
Html.td [] [ Html.text (Value.toString (Table.aggregationValue config model columnId)) ]
Nothing ->
Html.td [] []
Use aggregationValueOf when
you want a different set of rows, such as the selected ones:
selectedTotal : Table.State -> Table.RowModel Person -> Value
selectedTotal state model =
Table.aggregationValueOf config
model
"salary"
{ maxDepth = 0, rows = (Table.selectedRowModel state model).rows }
Aggregated cells
On a group row, getValue already returns
the aggregated value, so an aggregated cell needs no special reader. Use
cellIsAggregated when you want
to render it differently, which is what TanStack's aggregatedCell column
option is for:
viewCell : Table.State -> Table.RowModel Person -> Table.Row Person -> String -> Html msg
viewCell state model row columnId =
if Table.cellIsAggregated config model state row columnId then
Html.td []
[ Html.text (Value.toString (Table.getValue config row columnId) ++ " (total)") ]
else
Html.td [] [ Html.text (Value.toString (Table.getValue config row columnId)) ]
Group rows carry an explicit aggregatedValues entry for every leaf
column, including a Null for the columns with no aggregation. TanStack
replaces getValue on a group row and returns undefined for those columns;
Elm rows have no method to replace, and a group row's datum is a real leaf
row's datum, so writing an explicit Null is what stops a group row from
reporting its first member's value. One consequence:
Dict.size (Table.rowAggregatedValues row) is the leaf column count on every
group row.
Not covered
TanStack lets a column carry an array of aggregations
(aggregationFn: ['count', 'mean', ...]), producing an object of results
keyed by name. An AggregationFn here is one fold plus an optional merge,
and a cell holds one Value, so there is no counterpart. Configure a second
column, or compute the extra numbers yourself with
aggregate.
TanStack's getAggregationValue column option, which lets a column answer an
aggregation request with a server-provided value before any local
calculation, is also not ported, and neither is manualAggregation. Keep
server totals in your own model.
Custom aggregations receive the list of Values, not a context record, so
TanStack's column, columnId, maxDepth, table, groupingRow, and
subRows context members have no counterpart. Worker-backed row models are
out of scope.
Example
Aggregation, ported from TanStack's Aggregation example.