metric="...". The definition owns the aggregation SQL, filter, fmt, and label; components inherit them automatically.
Where they live
metrics/*.yaml files at the project root. Each file is a metrics view: a base table plus the dimensions and metrics defined against it. New projects get an example scaffold; existing projects get the folder on first metric creation.
Example
Metrics view reference
View-level (top of file):
Important:
dimensions and metrics are maps, not arrays. The name is the key.
Metric-level (inside metrics: map):
Metric names are flat and globally unique across all views in a project. Defining the same name in more than one view is flagged as a validation error in the editor and in
list_metrics. At runtime the first-loaded file wins, so a reference isn’t broken — but rename one of them to clear the error and keep the resolution deterministic.
Calculated metrics
Reference another metric in the same view with{name} — it expands inline at compile time. Any SQL is fair game around it:
- Refs must resolve to a metric in the same view (cross-view refs are v2)
- A calculated metric cannot also carry
filter— filter the referenced metrics instead - Cycles are rejected at parse time
{name} is deliberate — it can’t collide with Evidence’s double-brace {{ variable }} syntax. Note: {{ … }} inside a metric’s sql or filter is not interpolated today (v1); write literal SQL.
Using metrics in components
Every data component acceptsmetric="name":
What auto-inherits from the metric:
- Aggregation SQL — the metric’s
sqlbecomes the component’s aggregate - Format — the metric’s
fmtbecomes the component’sfmt/value_fmt - Label — the metric’s
label(or humanized name) becomes the component’s title/series name - Filter — the metric’s
filteris folded into the aggregate (compose with the component’s ownfilters=/where=/date_range=)
metric= is XOR with the raw data/value/y path — use one or the other, not both.
How filter: composes with the rest of the query
The metric’s filter: is compiled into the aggregate (SUM(x) FILTER (WHERE …)), not as a query-level WHERE. This lets divergent-filter metrics coexist in one query — critical for multi-metric charts and tables — but the composition rule is worth knowing:
us_revenue sums only US rows. order_count counts all orders — the US filter is scoped to the metric that declared it, not the whole query. Page filters and the component’s where= DO scope the whole query and stack on top: where="region = 'EU'" here means us_revenue returns 0 (US ∩ EU = ∅) and order_count restricts to EU. Use filter: for per-metric constraints that are part of the metric’s definition; use where= / page filters for query-wide restrictions.
Multi-metric charts
line_chart / bar_chart / area_chart accept an array of metric names. Each becomes one series:
y attribute — a single string for one metric, an array for many. Comma-separated strings are not supported (they’d collide with metric names containing commas); the editor validator surfaces the fix.
Every metric in the array plots on the same y-axis and inherits the chart’s y_fmt. All metrics must share the same base (v1 limitation — cross-base fan-out needs joins/align, coming later). Series legend uses each metric’s label.
For metrics on separate axes — or metrics from different bases — use combo_chart (below); it renders each series with its own query and supports axis="y2" per child.
Dual-axis and cross-base: combo_chart
Every combo_chart series child — {% line %} / {% bar %} / {% area %} / {% scatter %} / {% bubble %} — accepts metric="...". Each child runs its own query, so metrics from different bases just work; per-child axis="y2" gives you dual-axis without any new syntax.
combo_chart’sdata=andx=both become optional when every child series is metric-driven — each child inherits its base from the metric view and its x from the view’s time column. Set them explicitly only when at least one child uses rawy=, or when you want to override the metric’s default axis.- Mixing a metric child with a raw child on
combo_chartrequires the metric’s base to match the parent’sdata=. The editor flags a mismatch and tells you to either dropdata=(so every child resolves its own base) or split into two combo_charts.
Non-additive metrics
Metrics usingcount(distinct …) or a ratio ({a} / {b}, avg(…)) don’t add up across slices. Two situations to know about:
- Count-distinct on a line-item grain —
orders = count(distinct order_id)sliced bycategorycounts an order in every category it touches. The per-category bars are individually correct, but their sum exceeds the topline order count. - Ratios and averages —
aov = revenue / ordersis correct per slice, but per-slice AOVs cannot be averaged to get the overall AOV.
GROUP BY GROUPING SETS, not sum-of-rows), so a table showing “AOV by category” produces the true blended AOV in the total row. Bar/pie charts show raw per-slice values — worth calling out in the metric’s description: when the metric will be sliced by dimensions that aren’t 1:1 with its distinct key.
Editor experience
- Schema autocomplete as you type YAML — dimensions, metrics, all the keys.
- Live validation for schema mismatches, missing
base, invalid{name}refs, cycles, and duplicate metric names. The classicfmt: #,##0.0footgun (unquoted#= comment → null) has a “quote it” hint. - Autocomplete inside
metric="..."— every metric in every view. - Autocomplete inside
{name}refs — every metric in the same file. x=/series=autocomplete in metric mode — the view’s named dimensions first, then the base-table columns.
AI assistant tools
The Evidence editor AI has three metric-specific tools:
Prefer these over hand-writing SQL when a metric matches the question. Broken metric YAML surfaces in
invalidFiles on every tool response — feed the errors back through debug_code to fix.
