Skip to content

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 omitted values: 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
Jan2026Feb020,00040,000PaidOrganicReferralRevenue by Channel Data as of 14:24 UTC on 6 Oct 2026 made withdbt Charts
▶