---
title: "Totals — quario"
description: "Totalling a quario report with aggregates, running values and inline reducers, and presenting an empty one honestly."
url: "https://getquario.com/docs/diving-deeper/totals"
---

# Totals

## Intent

Three different things get called a total. An **aggregate** folds the rows in
scope into one value. An **inline reducer** folds an array a single row
carries. A **running value** holds a different answer on every row, according
to the rows before it. Choosing the wrong one is the most common way a report
comes out plausible and wrong.

## Design decisions

**An aggregate is a declaration, and an inline reducer is a call.** The same
eight names serve both, which is deliberate, but they sit in different places. A
declaration is `"subtotal": "sum:=@.qty * @.unitPrice"` under `aggregates`. A
call is `sum(@.lines, l => l.total)` inside an expression. Writing
`"total": "round:=@.amt"` is a definition error, because `round` folds nothing.

**Numeric coercion is quiet, and that’s the point.** `sum` and `avg` coerce
each per-row value, and anything that doesn’t come out a finite number
contributes `0`. A report keeps rendering when a data feed degrades rather than
throwing over one bad cell. Where absence has to be visible, test for it in the
report or validate the feed before it arrives.

**An empty scope gives `0` for the counting reducers and `null` for the rest.**
`sum`, `count` and `countDistinct` return `0`, which is a measured result worth
preserving. `avg`, `min`, `max`, `first` and `last` return `null`, because there is no
honest value to give. Neither `first` nor `last` skips a null, so a scope whose
first row holds `null` reports that `null` rather than looking past it.

**`first` and `last` read the order of the rows they fold, and nothing else
does.** Every other reducer gives the same answer whatever order the rows
arrive in. These two therefore depend on what each scope gives them, and the
scopes differ. A report folds after `where`, its `sort`, and its `take`, and
before it splits the rows into groups. It therefore reports the ends of the
whole run. A group that declares a `sort` and no `take` folds its partition
before it orders that partition. Its aggregate therefore holds the partition’s
ends rather than the ends as rendered. Declare `first` under a group’s `run`
where a detail row needs the row that group opens on as drawn.

**Aggregates fold the rows, never each other.** Each aggregate reads the rows in
its scope and nothing else, so the order you declare them in can’t change a
total. A group aggregate that reads a sibling through the group’s handle is a
render error, with or without a `take`. To share a calculation between two
aggregates, write it in each.

**`min` and `max` compare without coercing.** Mixed types therefore compare by
JavaScript ordering. Keep such a column one type where the extremes matter.

**`countDistinct` counts what a `by` key would partition into.** It folds on
the same key identity a group’s `by` uses, so `countDistinct:=@.region` is the
number of instances `by: "=@.region"` would open over those rows. Two
equal-looking objects are two distinct values for the same reason they would be
two instances.

**There is no SQL-style null skipping.** `null` is an ordinary value here, and
since every absent read is `null`, a missing field and an explicit `null` share
one bucket. `COUNT(DISTINCT x)` in SQL skips nulls, and `countDistinct` counts
them as one of the distinct values.

**`count`’s lambda is a predicate. Every other reducer’s is a projection.**
`count(@.lines, l => l.active)` counts the active lines.
`countDistinct(@.lines, l => l.active)` counts how many distinct answers that
test gave, which is at most three.

**A reducer folds an array and says so when it gets anything else.** The quiet
coercion covers the values folded, never the collection they came from. A feed
delivering `lines` as a string fails loudly. An absent array isn’t a mistake,
so `sum(@.lines)` on a row with no lines folds the empty array and gives `0`.

**Running values reset by scope.** A `run` block on the report persists across
the document. A `run` block on a group starts again at each instance. Where a
runner resets is the only thing that decides what it means.

## API walkthrough

### Declaring an aggregate

Declare under `aggregates` on the report or on a group. The reducer runs over
the rows in scope, evaluating the expression per row with `@` bound to that
row.

```json title="report and group aggregates"
{
  "aggregates": { "grandTotal": "sum:=@.qty * @.unitPrice" },
  "groups": [
    {
      "name": "region",
      "by": "=@.region",
      "aggregates": { "subtotal": "sum:=@.qty * @.unitPrice", "orders": "count" }
    }
  ]
}
```

The eight reducers are `sum`, `count`, `countDistinct`, `avg`, `min`, `max`,
`first` and `last`. `count` takes no expression, and every other reducer takes
the form `name:=expression`.

You read a report aggregate as `$.<name>` and a group aggregate as
`<groupName>.<name>`. The engine computes both once and injects them as scope
values, so they compose in later expressions like any other variable.

```json title="composing two aggregates"
{ "value": "{{ round(region.subtotal / $.grandTotal * 100, 1) }}%" }
```

Names stay reserved where they would collide. A report aggregate can’t
take the name `input` or `params`, and a group aggregate can’t take
`key`.

Groups have no zero-row instance. Empty input opens no handle and renders no
group band at all, which is why the report’s `empty` band presents report
aggregates through `$`.

### Presenting an aggregate that has nothing to say

```json title="an honest empty state"
{ "type": "text", "value": "{{ $.rows }} rows, average {{ $.average == null ? '—' : $.average }}",
  "style": { "size": 10 } }
```

Test with `== null`. Don’t use `||`, which also replaces a valid `0` and
turns a measured zero into a dash.

### Folding a row’s own array

The same eight names are functions in the default registry, each taking an array
and an optional per-item lambda. Use them where a row carries its own lines and
you don’t want a group.

```json title="inline reducers"
{ "value": "{{ sum(@.lines, l => l.qty * l.price) }}", "style": { "format": "currency" } }
```

Lambdas take a single bare parameter and no parentheses. The reducer owns the
iteration, so the lambda stays a pure per-item read.

A registered function of the same name overrides an inline reducer. Declared
aggregates always use the built-in ones.

### Accumulating down the rows

Declare under `run`, and read as `run.<name>` on a detail row. The reducers are
the six above plus `prev`, which holds the previous row’s value and is `null`
on the first.

```json title="a running balance"
{
  "run": { "balance": "sum:=@.debit - @.credit" },
  "detail": {
    "columns": [
      { "header": { "value": "Balance" }, "value": "{{ round(run.balance, 2) }}",
        "style": { "align": "right", "format": "currency" } }
    ]
  }
}
```

Running values follow render order, which is the order rows appear after
grouping and sorting. The engine computes them during the banded walk rather
than before it. They update for every selected row before visibility decides
whether to emit it.

Run names can’t repeat across the report and its nested groups, because every
run value shares one `run` object. When no `run` block is in scope at all,
`run` itself is unbound and `run.<name>` throws.

### Placing a total where it belongs

A `detail.total` block draws at the foot of the table, which under a group band
means once per group. Totals for the whole document belong in the report
`footer`, which draws once. [Bands](https://getquario.com/docs/diving-deeper/bands.md) covers the
distinction.

A total cell carries `span` to reach across columns. A total row then reads as
a label and an amount rather than a line of empty spacers.

### Settling the arithmetic

Wrap arithmetic a cell does for itself in `round`. The HTML and PDF targets
present through `format`, so `round` never changes what they show. The XLSX
and CSV targets keep the number the engine hands them, so an unrounded sum
shows there in full. [Formatting](https://getquario.com/docs/diving-deeper/formatting.md) covers the
gap between counting presented places and counting stored ones.

```json
{"@context":"https://schema.org","@type":"BreadcrumbList","itemListElement":[{"@type":"ListItem","position":1,"name":"Home","item":"https://getquario.com/"},{"@type":"ListItem","position":2,"name":"Docs","item":"https://getquario.com/docs"},{"@type":"ListItem","position":3,"name":"Totals","item":"https://getquario.com/docs/diving-deeper/totals"}]}
```
