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")raisesUnknownPaletteError. - Warn + alias: the name resolves to the nearest dbt Charts equivalent
silently at compile time; a render-stage
WARN-PALETTE-UNSUPPORTEDdiagnostic 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.