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.

The type identifier is kpi — there is no number type; validation rejects it.

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. Map one or the other: when both are present the renderer shows the 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.comparison — numeric field driving the comparison block below (period-over-period readings).
mapping:
  value: revenue
  subtitle: period_label
  delta: revenue_delta_pct

The one-row rule means the Malloy side of a KPI is an aggregate:-only view. Producing the delta column is part of the query, not the viz — one working shape compares the window with the previous window of equal length:

# models/ec_performance.malloy (excerpt)
##! experimental.parameters

source: ec_performance() is ecommerce.sql("""
  WITH windows AS (
    SELECT
      SUM(IF(created_at >= TIMESTAMP_SUB(CURRENT_TIMESTAMP(), INTERVAL 30 DAY),
             sale_price, 0)) AS revenue,
      SUM(IF(created_at <  TIMESTAMP_SUB(CURRENT_TIMESTAMP(), INTERVAL 30 DAY)
         AND created_at >= TIMESTAMP_SUB(CURRENT_TIMESTAMP(), INTERVAL 60 DAY),
             sale_price, 0)) AS revenue_prev
    FROM `bigquery-public-data.thelook_ecommerce.order_items`
  )
  SELECT
    revenue,
    'Last 30 days' AS period_label,
    SAFE_DIVIDE(revenue - revenue_prev, revenue_prev) AS revenue_delta_pct
  FROM windows
""") extend {
  view: kpi is { select: * }
}

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.

comparison block

The top-level comparison block renders a period-over-period reading from the field named in mapping.comparison:

  • comparison.enabled — boolean.
  • comparison.direction"auto" derives the up/down arrow from the value's sign.
  • comparison.period_label — caption for the comparison (e.g. "vs budget to date").
mapping:
  value: attainment_pct
  comparison: attainment_pct
format:
  value: "#,##0.00%"
  comparison: "#,##0.00%"
comparison:
  enabled: true
  direction: auto
  period_label: "vs budget to date"

Note the format entries above are slot-keyed (value, comparison) rather than field-keyed — both forms work, and slot keys read better when the same field fills two slots.

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