Docs / Build Workflow

Visualization — kpi

When to use kpi

KPI is the right choice when the answer is a single number — revenue total, order count, conversion rate, error budget consumed. Add a delta or a secondary value to show direction or comparison alongside the headline. KPI is the typical "headline" of a dashboard: it does not emit clicks (there is no dimension to click on), but it does react to dashboard filters and cross-filter pills like every other viz — its underlying query re-runs with the active params and the headline number recomputes.

If you need a number per category instead of a single number, use bar. If you need a trend over time, use line. If you need many numbers in a tabular layout, use grid.

Type identifiers kpi and number map to the same renderer; either is accepted.

Mapping

The KPI reads the first row of the query result. Mapping fields name which columns of that row become which visual element:

  • mapping.value — required. The field whose value is the headline number.
  • mapping.subtitle — string field for a label / period context line under the value.
  • mapping.delta — numeric field shown as the change indicator. Sign drives the up/down arrow; the polarity setting controls the color.
  • mapping.secondary_value — numeric field for a comparison value (e.g. "previous period") when there is no delta.
  • mapping.secondary_label — caption for secondary_value, written as a literal string (e.g. "Invoices", "Previous quarter"). It also accepts a field name: if the value matches a column in the result row, that column's value is used as the caption. Defaults to "Previous" when omitted.
  • mapping.secondary_plain — plain-text field shown next to the headline when there is no delta or secondary value.
mapping:
  value: revenue
  subtitle: period_label
  delta: revenue_delta_pct

kpi block

KPI-specific options live under the top-level kpi block. KPI does not have a chart block.

  • kpi.comparison_label — caption shown beneath the delta (e.g. "vs. last week"). When omitted, the renderer composes a default like "Up vs previous period" from the delta sign.
  • kpi.delta_good_when"increase" (default) or "decrease". Sets the polarity that colors the delta green vs red. Use "decrease" for metrics where lower is better — refunds, error rate, time-to-resolution.

format

Number formatting is field-keyed. Per-field patterns win over the root pattern.

  • format.value — pattern for the headline number.
  • format.delta — pattern for the delta indicator.
  • format.secondary_value — pattern for the secondary comparison value.
  • format at the root — fallback when a field-specific entry is missing.
# headline as abbreviated currency, delta as percent
format:
  revenue: "$#,##0a"
  revenue_delta_pct: "#,##0.00%"

The full pattern grammar lives on the viz-types overview.

Cross-filter behavior

KPI participates in cross-filtering as a consumer only:

  • Does not emit. Clicking a KPI does not add a pill — there is no dimension to click on.
  • Does react. Pills set elsewhere on the dashboard, and any dashboard-level filter, become parameters on the next run, and the KPI's underlying query re-runs with them. The headline number recomputes accordingly.

If you want a "headline that always shows the dashboard-wide total" regardless of pills, write the underlying Malloy query so its parameter defaults ignore the pill values (e.g. accept the parameter but do not use it in the where-clause), or use two separate KPI items: one bound to a model that ignores the cross-filter parameter, one bound to a model that respects it.

Worked examples

Headline with delta and percent polarity favoring decrease (lower error rate is better):

id: ec_error_rate_kpi
title: Error Rate
query: "models/ec_quality.malloy::error_rate_kpi"
type: kpi
mapping:
  value: error_rate
  delta: error_rate_delta_pct
  subtitle: period_label
format:
  error_rate: "#,##0.00%"
  error_rate_delta_pct: "#,##0.00%"
kpi:
  comparison_label: "vs previous period"
  delta_good_when: decrease
published: true

Headline with a secondary comparison value (no delta):

id: ec_revenue_vs_prev_kpi
title: Revenue
query: "models/ec_revenue.malloy::current_vs_previous"
type: kpi
mapping:
  value: revenue_current
  secondary_value: revenue_previous
  secondary_label: "Previous quarter"
format:
  revenue_current: "$#,##0a"
  revenue_previous: "$#,##0a"
published: true

Headline with a plain-text annotation when the metric does not have a numeric comparison:

id: ec_top_brand_kpi
title: Top Brand
query: "models/ec_revenue.malloy::top_brand"
type: kpi
mapping:
  value: brand_name
  secondary_plain: market_share_label
published: true

Common pitfalls

  • The KPI shows the wrong number. Only the first row of the query result is read. Make sure the underlying query aggregates to one row, or sort it so the headline value is the row you want.
  • The delta polarity is inverted. Set kpi.delta_good_when: decrease for metrics where lower is better (refunds, errors, latency). The default "increase" assumes higher is better.
  • The KPI is reacting to a pill when you wanted a global headline. KPI consumes pills like every other viz. To make a "always-total" KPI, write the underlying model so the parameter is accepted but unused in the where-clause for that view.
  • The headline value is not formatted. Set format.value (or the field-specific entry under format) — without a pattern, the value is rendered with the platform default.
  • Mixing delta and secondary_value in the same KPI. Pick one or the other; mixing makes the chart busy and the renderer prefers delta.