Chart Color Channels¶
dbt Charts supports color as the primary data channel at chart root.
color accepts a bare field name (string) to encode a column as series grouping.
Static colors and gradient scales go under style.color.
Discrete, rule-driven styling lives in a separate top-level
conditional_formatting: block on the chart, available on type: table
and type: kpi only (see
Conditional (threshold) below).
Shorthand¶
A plain string binds a column to the color channel:
color: segment
This is the only accepted form at chart root. color does not accept {column: ...}, {value: ...}, or {scale: ...} at chart root; use style.color for static paint or gradient scale.
Literal¶
Fix a channel to a static color via style.color.static:
style: color: static: "#1aff3c"
Gradient scale¶
Map a numeric column to a continuous color range via style.color.gradient:
color: revenue style: color: gradient: palette: ["#ffffff", "#0000ff"] min: 0 max: 1000000
Omit min/max to scale from data: by default the domain is widened to round
bounds and every round tick is labeled on the legend; set nice: false for the
exact data extent instead. palette must be list[str] for color channels.
Conditional (threshold)¶
Available on type: table and type: kpi only.
Discrete, rule-driven styling goes under the chart-level
conditional_formatting: block, indexed by column name. Each column entry
carries a single when: list of rules; each rule has exactly one predicate
and one or more style outputs (background, font):
| Predicate | Shape | Matches |
|---|---|---|
eq: x |
any scalar | values equal to x (bool/int cross-match guarded) |
ne: x |
any scalar | values not equal to x |
lt / lte / gt / gte |
number | one-sided numeric comparison (non-numeric never matches) |
between: [low, high] |
two numbers, low <= high |
closed inclusive range |
in: [v1, v2, ...] |
non-empty list | equality against any list element |
is_null: true |
boolean | null/None values |
is_null: false |
boolean | any non-null value (including 0, "", false) |
default: true |
boolean | catch-all; fires when no other rule matched |
type: table conditional_formatting: arr: when: - gt: 1000000 background: "#166534" font: {color: "#ffffff", weight: bold} - between: [0, 1000000] background: "#dcfce7" - is_null: true background: "#e5e7eb" - default: true background: "#fee2e2" font: {color: "#991b1b"}
Non-default rules are evaluated in list order; later matches override
earlier ones for overlapping style keys. A default: true rule must be the
last entry in its when: list and only fires when no other rule matched
for the current row; it is a true fallback, not a last-match-wins override.
Without a default:, rows that match nothing fall back to the chart's
un-conditional styling.
Channel reference¶
| Channel | Chart types | Value type |
|---|---|---|
color |
All | str (color) |
background |
KPI, table | str (color) |
opacity |
All | float [0, 1] |
stroke.color |
Bar, line, area, arc | str (color) |
stroke.width |
Bar, line, area | float |
Tables¶
On type: table, the table renderer reads conditional_formatting rules
directly from the chart-level block at render time; there is no per-column
when: shape under style.columns. Per-channel color.scale /
background.scale still lower to style.columns[<column>].scale because
scale is a continuous encoding the renderer interpolates per cell.
KPI channels¶
On type: kpi, color controls the value text color and background
controls the card fill; both support literal and gradient modes via the
per-channel shape. Rule-driven overrides (mix of background and font
color/weight per threshold) go under conditional_formatting:. Provide
explicit min/max in gradient scale for KPIs; auto-domain over a
single row always collapses.
Precedence¶
chart.<channel> (data-bound) wins over chart.style.<channel> (literal).
Using a data-bound form under chart.style.<channel> is a validation error.
On type: table/type: kpi, conditional_formatting rules apply on top
of the per-channel encoding for matching rows.
Legend¶
When color is bound to a series column, dbt Charts renders a legend mapping
each distinct value to its swatch. Two style.legend fields tune the legend
itself without touching the color scale:
symbol_limit: caps the number of legend entries shown. Useful for a high-cardinality series that would otherwise overflow the legend area.values: an explicit list pinning which entries appear in the legend and in what order, overriding the default order dbt Charts infers from the color scale's domain. This only reorders/filters the legend itself; it does not filter which rows are plotted on the chart.values: []renders an empty legend; it does not fall back to the default order the way an omittedvalues:does.
Each entry in values can be spelled either the way the legend renders it,
or as the underlying column/measure name: case, -/_/space differences
all resolve to the same entry (net_revenue, Net Revenue, and
net-revenue all select the same series). A dimensionless wide
y: [...] chart's legend text always matches its y-axis title, so
either the raw column name or that display text works there too. When
the chart also authors color: as a dimension the measures cross with,
each entry is a <dimension value> - <measure label> composite instead:
author that composite spelling (the dimension value paired with either
the raw or the display measure name). The bare measure name alone is
ambiguous across dimension values and will not resolve.
An entry that doesn't match anything, or matches more than one domain entry, is dropped and reported with a warning naming what it tried and the real domain. If every authored entry misses, the legend falls back to its full default order instead of rendering empty.
This resolution covers every nominal/ordinal color: field on the
families above, including a wide y: [...] chart and an overlay's
layers: labels. A point_map/bubble_map's legend, a temporal
(date/timestamp) or boolean color: column's legend, and an all-null
color: column (inferred as numeric, the same as any other quantitative
column) are not resolved: values there is used exactly as authored,
with no matching and no diagnostic on a miss. Neither is an overlay
whose base y isn't quantitative: its shared color scale is never
built, so legend.values passes through verbatim there too.
queries:
revenue_by_channel:
columns: [month, channel, revenue]
values:
- ["2026-01", "Organic", 42000]
- ["2026-01", "Paid", 18000]
- ["2026-01", "Referral", 9000]
- ["2026-02", "Organic", 47000]
- ["2026-02", "Paid", 21000]
- ["2026-02", "Referral", 8500]
charts:
channel_revenue:
query: revenue_by_channel
type: bar
title: Revenue by Channel
x: month
y: revenue
color: channel
style:
legend:
values: ["Paid", "Organic", "Referral"] # pin legend order
symbol_limit: 5
rows:
- channel_revenue