Skip to content

Palette resolver

For which palettes ship and what each looks like, see Palettes. This page covers how to use them.

Resolver API

from dbt_charts.core.compile.resolve.style.palette import palette, color

# Sequential / diverging: returns a list of hex stops.
palette("dbt-seq-blue")                     # 11 default stops
palette("dbt-seq-blue", steps=5)            # 5 evenly spaced stops
palette("dbt-seq-blue", reverse=True)       # reversed

# Diverging auto-skips the midpoint on even N.
palette("dbt-div-blue-red", steps=6)        # flanks, no gray middle

# Categorical and scaffold: fixed slots.
palette("vivid-10")                         # all 10 slots
palette("vivid-10", steps=4)                # first 4 slots

# Named tokens: tone roles and scaffold stops.
color("negative.solid")                     # "#94001e"
color("dbt-grays.ink")                      # "#222222"

YAML shorthand

Dashboard YAML supports a compact shorthand:

style:
  palette: "dbt-seq-blue"        # 11 stops
  palette: "dbt-seq-blue:5"      # 5 stops
  palette: "dbt-seq-blue_r"      # reversed
  palette: "dbt-seq-blue:5_r"    # 5 stops reversed
  palette: "dbt-div-blue-red:6"  # 6 stops, midpoint auto-skipped
  fill: "negative.solid"
  text: "dbt-grays.ink"

The shorthand is parsed by _parse_palette_reference(); programmatic callers should pass kwargs (steps=, reverse=) instead.

Theme-portable references vs. absolute pins

Color tokens in board YAML come in two grammars with different contracts:

# Board-root style block
style:
  charts:
    marks:
      bar:
        # Theme role: resolves through the active theme's
        # `style.palettes` bindings. Switching themes re-resolves the
        # color: on vivid, `category[2]` is vivid-10's cyan; on
        # clarity, editorial-10's sky. A trailing variant segment
        # (`.dark`/`.light`/`.pale`/`.deep`) addresses a literal tier
        # of that same slot, and named aliases like `chrome.ink`
        # resolve through the chrome role.
        border:
          color: "category[2].dark"
      rule:
        stroke:
          # Absolute pin: names a physical palette and slot.
          # Theme-agnostic: the same hex on every theme, by design.
          color: "vivid-10.1"

Role tokens resolve everywhere colors are authored: theme YAML, the board-root style: block, and chart-level style: blocks (the theme's role bindings travel to the resolved boundary, so per-chart overrides like style.color.categorical: ["category[3].dark"] follow a theme switch too).

Default to theme roles when authoring boards. A board written with role references restyles itself completely on a theme switch. Reach for an absolute pin only when the color is a deliberate pick that should survive theme changes (a brand color, a semantic association like "gold means contract revenue"); pinning is a feature there, not a bug.

Indexing is 1-indexed everywhere: role brackets, absolute dot references, and the integer slot aliases inside scaffold/tone palettes all count from 1. Every stop-list family answers the positional form: color("vivid-10.3"), color("dbt-seq-blue.3"), and color("dbt-div-blue-red.3") all resolve the third stop. Tone and scaffold palettes carry named aliases instead, so they take a slot name (color("positive.solid")), not a position. The same physical stop carries the same slot number in every variant of a family too; on the clarity theme, category.gold.light and editorial-10.5.light are the same gold-light color. Prefer the named role when the hue meaning should survive a later slot reorder.

The shipped role bindings: _base binds category to the vivid-10 family (inherited by vivid and neon); clarity and paper (which extends clarity) rebind it to the editorial-10 family. Themes may rebind any role via style.palettes, and boards can too (style.palettes is patchable at the board root).

Every categorical palette derives every tier live (variant(), compile/resolve/style/palette.py) -- there is no companion file to bind or keep in sync. A third token segment addresses one: category[2].dark, category.blue.pale, vivid-10.3.deep. dark is never lighter than its base and light never darker, on every theme -- the tier names are literal, not a contrast direction. Which variant a board or theme reaches for on a dark canvas (so a mark stays legible, or a mark stays deliberately recessive) is an authoring decision, covered in the theme guide, not a rule this resolver enforces.

surface="table": WCAG-AA-safe table fills

For sequential and diverging spines, palette(name, surface="table") returns a WCAG-AA-safe sub-palette intended for table cell backgrounds behind #222 body text:

palette("dbt-seq-blue", surface="table")              # 11 AA-safe stops
palette("dbt-div-blue-red", surface="table", steps=5) # 5 AA-safe stops

The algorithm binary-searches the OKLCH-interpolated spine for the lightness boundary where contrast against #222222 crosses 4.5:1, then generates steps stops evenly from the light end of the spine to that boundary. The boundary is found on the continuous curve, not snapped to a spine index, so the result is dense enough for any steps value without falling off the readable edge.

surface= is not valid for categorical / scaffold / tone palettes; their slots are fixed and there is no spine to carve. Passing surface="table" on those families raises SurfaceUnsupportedError, as does any unrecognized surface= string. surface=None and surface="default" both return the standard downsample.

Smart defaults

When a dashboard doesn't name a palette, the resolver picks based on the data shape (see select_default_palette()):

Data shape Default palette
Continuous numeric, no midpoint dbt-seq-blue
Continuous numeric, signed / midpoint dbt-div-blue-red
Discrete enum, ≤ 10 values, clarity/paper theme editorial-10
Discrete enum, ≤ 10 values, vivid/neon theme vivid-10
Discrete enum, > 10 values Theme default + runtime warning
Status / severity field Tone palette (negative, warning, positive)

Errors

Exception When
UnknownPaletteError Name doesn't match; message suggests the nearest hit
UnknownColorError color(token) slot unresolved
CategoricalOverrequestError steps=N > len(stops) on categorical/scaffold
SurfaceUnsupportedError surface= passed for a family that doesn't carve
ToneAsPaletteError Tone name passed to palette() instead of color()

Anti-Patterns

A handful of famous palettes routinely produce misleading charts. The resolver treats them two ways:

  • Hard fail: the name is not shipped. palette("jet") raises UnknownPaletteError.
  • Warn + alias: the name resolves to the nearest dbt Charts equivalent silently at compile time; a render-stage WARN-PALETTE-UNSUPPORTED diagnostic surfaces the nudge (dct docs warnings --code WARN-PALETTE-UNSUPPORTED). Migration paths still work either way.

Hard-fail list

Name Why rejected
jet Non-monotonic luminance creates illusory discontinuities and reverses ordering perception. Broadly criticized in the data-vis corpus (Cleveland, Cairo, Kosara). No defensible use remains.
rainbow Same problem: hue steps masquerade as ordered magnitude. Luminance wobbles.
hsv Not perceptually uniform; conflates hue and saturation. Produces banding.

There is no dbt Charts substitute for these because their structure (rainbow, maximal hue excursion) is the anti-pattern itself. If a chart really needs distinct hues, use vivid-10 (unordered) or dbt-seq-blue (ordered).

Warn-and-alias list

Name Aliased to Reason
RdYlGn dbt-div-crimson-green The classic red-yellow-green diverging pair is CVD-hostile; deuteranopes can't reliably distinguish the red and green ends. dbt-div-crimson-green is the only dbt Charts palette engineered to pass CVD ΔE ≥ 11 on R/G endpoints.
parula dbt-seq-blue MATLAB's default; not CVD-safe and superseded by the viridis family in most scientific tooling. dbt-seq-blue is dbt Charts's default sequential.

The diagnostic message names the substitution explicitly so dashboards rendered from third-party tooling don't crash, but the suggestion points at the dbt Charts alternative for new work.

References

  • Borland & Taylor, Rainbow Color Map (Still) Considered Harmful (IEEE CG&A, 2007)
  • Kenneth Moreland, Diverging Color Maps for Scientific Visualization (2009)
  • Cindy Brewer et al., ColorBrewer 2.0: comparative palette library

Palette scoring

The Colorgorical scoring helper scores any palette on four axes, a re-implementation of the Colorgorical methodology (Gramazio, Laidlaw, Schloss, IEEE TVCG 2017) against the public XKCD color-name survey (CC0) and the Schloss-Palmer 2011 pair-preference regression.

Score What it measures
Perceptual Distance CIEDE2000 ΔE between color pairs. Same metric as the Leonardo CVD gate.
Name Difference Whether two colors land under different XKCD names: "can someone say 'the blue one' vs 'the green one'?"
Name Uniqueness Whether a color unambiguously falls under one name, or sits on a boundary (cyan/teal, slate/charcoal). Higher = clearer.
Pair Preference Schloss-Palmer regression (lightness contrast + hue-angle difference). Which pairs look good side-by-side in a legend.

The primary gate remains Leonardo ΔE ≥ 11 for CVD safety. Colorgorical scoring is a secondary quality signal: a palette that passes Leonardo is CVD-safe; passing the scoring functions says it is also preferable and legend-readable.

Known simplifications vs the Colorgorical paper (deliberate; per the Colorgorical paper):

  • Name Difference / Uniqueness use the aggregated XKCD 949-centroid list, not the full 2.8M-response probability distributions from Heer-Stone
  • Colors near naming boundaries score worse than they would under the paper's full model.
  • Pair Preference omits Schloss-Palmer's coolness term (requires a 5MB precomputed LAB→coolness lookup derived from their raw data). We keep the hue + lightness regression with the published coefficients (wh=-46.4222, wl=47.6133).

Scores are therefore mutually comparable across dbt Charts palettes, but not a bit-exact reproduction of Colorgorical's paper numbers.

Versioning

Once a palette spine is committed, its hex values are immutable. Re-tunes ship under a new name (dbt-seq-blue-v2, say) so existing dashboards don't silently shift. Bug fixes (for example, out-of-gamut typos) are the one exception, documented in PALETTE_CHANGELOG.md when that happens.