Palette resolver¶
For which palettes ship and what each looks like, see Palettes. This page covers how to use them.
Resolver API¶
from dataface.core.compile.palette import palette, color
# Sequential / diverging — returns a list of hex stops.
palette("dft-seq-blue") # 11 default stops
palette("dft-seq-blue", steps=5) # 5 evenly spaced stops
palette("dft-seq-blue", reverse=True) # reversed
# Diverging auto-skips the midpoint on even N.
palette("dft-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("dft-grays.gray-90") # "#222222"
YAML shorthand¶
Dashboard YAML supports a compact shorthand:
style: palette: "dft-seq-blue" # 11 stops palette: "dft-seq-blue:5" # 5 stops palette: "dft-seq-blue_r" # reversed palette: "dft-seq-blue:5_r" # 5 stops reversed palette: "dft-div-blue-red:6" # 6 stops, midpoint auto-skipped fill: "negative.solid" text: "dft-grays.gray-90"
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 face YAML come in two grammars with different contracts:
# Face-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 stark, `category[2]` is vivid-10's cyan; on # editorial, editorial-10's sky. `category_dark[2]` / # `category_light[2]` / `category_ghost[2]` address the # positional companions the same way, and named aliases like # `chrome.ink` resolve through the chrome role. border: color: "category_dark[2]" 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
face-root style: block, and chart-level style: blocks (the theme's
role bindings travel to the resolved boundary, so per-chart overrides
like style.palette: ["category_dark[3]"] follow a theme switch too).
Default to theme roles when authoring faces. A face 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. The same physical stop carries the same slot number in both
forms — on the editorial theme, category_light[7] and
editorial-10-light.7 are the same gold-light color (the 7th slot).
The shipped role bindings: _base binds category / category_dark /
category_light / category_ghost to the vivid-10 family (inherited
by stark, dark, light); editorial and cream rebind them to the
editorial-10 family. Themes may rebind any role via style.palettes, and
faces can too (style.palettes is patchable at the face root).
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("dft-seq-blue", surface="table") # 11 AA-safe stops
palette("dft-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 | dft-seq-blue |
| Continuous numeric, signed / midpoint | dft-div-blue-red |
| Discrete enum, ≤ 10 values, editorial theme | editorial-10 |
| Discrete enum, ≤ 10 values, stark 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() |
UnsupportedPaletteWarning |
Anti-pattern names (RdYlGn, parula) — see Anti-Patterns below |
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 DFT equivalent and
the resolver emits an
UnsupportedPaletteWarning. Migration paths still work, but you'll see the nudge in logs.
Hard-fail list¶
| Name | Why rejected |
|---|---|
jet |
Non-monotonic luminance creates illusory discontinuities and reverses ordering perception. Broadly criticised 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 DFT 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 dft-seq-blue (ordered).
Warn-and-alias list¶
| Name | Aliased to | Reason |
|---|---|---|
RdYlGn |
dft-div-crimson-green |
The classic red-yellow-green diverging pair is CVD-hostile — deuteranopes can't reliably distinguish the red and green ends. dft-div-crimson-green is the only DFT palette engineered to pass CVD ΔE ≥ 11 on R/G endpoints. |
parula |
dft-seq-blue |
MATLAB's default; not CVD-safe and superseded by the viridis family in most scientific tooling. dft-seq-blue is DFT's default sequential. |
The warning text names the substitution explicitly so dashboards rendered from third-party tooling don't crash, but the suggestion points at the DFT 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 DFT 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 (dft-seq-blue-v2, say) so existing
dashboards don't silently shift. Bug fixes (e.g., out-of-gamut typos) are
the one exception — documented in PALETTE_CHANGELOG.md when that
happens.