Palettes

scale.palette names a color ramp for a choropleth, a bubble colorScale, or an ordinal marker colorBy. ApexMaps ships 17 registered ramps, and you can add your own with the same static API the built-ins are registered through.

series: [{ name: 'Unemployment rate', joinBy: ['iso_a3', 'code'], data, scale: { palette: 'blues' } }]

The 17 built-in ramps

Each palette belongs to one of three kinds, which determines how it gets sampled and where it's a sensible default.

KindPalettes
Sequentialblues, greens, oranges, reds, purples, greys, viridis, magma, teal
Divergingrdbu, brbg, piyg, spectral, rdylgn
Categoricalapex, tableau, okabeIto

Sequential ramps run light to dark and are for magnitudes: a value is low, medium, or high. Diverging ramps run through a neutral midpoint to two contrasting extremes and are for signed data: a change, a margin, a difference from a baseline. Categorical palettes are unordered swatch sets for distinct groups rather than a range, used by ordinal scales and colorBy.

teal is a palette this library had to build rather than adopt: ColorBrewer has no teal scheme, and its nearest neighbors (BuGn, GnBu) drift out of the teal hue before the dark end, which is exactly where a choropleth puts most of its ink. apex matches the default ApexCharts series colors, so a map dropped beside a chart on the same dashboard is color-consistent with zero configuration.

Most sequential and diverging ramps follow the ColorBrewer families (Brewer and Harrower), the standard reference for thematic cartography. viridis and magma come from the matplotlib colormaps.

Colorblind-safe

Every Palette object carries a colorblindSafe flag. All nine sequential ramps and three of the five diverging ramps (rdbu, brbg, piyg) are colorblind-safe; spectral and rdylgn are not, since both rely on a red/green contrast that collapses under the two most common deficiencies. okabeIto is the one categorical palette built for this from the start, designed specifically to stay distinguishable under all three common color-vision deficiencies.

ApexMaps.palette('rdbu')
// { kind: 'diverging', colorblindSafe: true, stops: ['#67001f', '#b2182b', ..., '#053061'] }

Why ramps are sampled in OkLab

Interpolating colors through sRGB turns the midpoint of most ramps a muddy grey, because sRGB is not a perceptually uniform space: equal numeric steps between two colors do not read as equal visual steps. That's a correctness problem for a choropleth specifically, not just an aesthetic one, since the reader infers magnitude from perceived lightness. A ramp whose middle classes all look like the same washed-out grey has lost the ability to say "medium."

ApexMaps stores only a handful of anchor stops per palette and samples the class colors in OkLab (Ottosson, 2020) at render time, a perceptual space where equal numeric steps do read as equal visual steps. This is also why a 4-class and a 9-class map of the same data stay perceptually consistent: both are sampled from the same anchor stops rather than picked from two unrelated, hand-tuned color tables.

ApexMaps.listPalettes().forEach((name) => {
  const { stops } = ApexMaps.palette(name)
  // stops are the anchors; the map draws OkLab-interpolated samples between them
})

Sequential ramps get an additional adjustment: sampling starts slightly inside the light end (10% in) rather than at the literal lightest anchor stop. A sequential ramp's lightest class would otherwise render nearly white, invisible against a white page with white feature borders, silently losing the reader a whole category. Diverging ramps and explicit color lists are sampled across their full range, since a diverging ramp needs both true extremes and its exact midpoint, and an explicit list is a decision you've already made.

Registering your own palette

ApexMaps.registerPalette(name, { kind, stops, colorblindSafe? }) adds a palette under the global name, available anywhere scale.palette or colorScale.palette accepts a string:

ApexMaps.registerPalette('brand', {
  kind: 'sequential',
  stops: ['#f5f3ff', '#c4b5fd', '#7c3aed', '#3b0764'],
  colorblindSafe: false,
})

series: [{ name: 'Adoption', joinBy: 'name', data, scale: { palette: 'brand' } }]

Only the anchor stops need supplying, four here, the same as most built-ins ship with nine. ApexMaps samples whatever class count the scale needs from them at render time, in OkLab, the same as it does for blues or viridis.

ApexMaps.listPalettes() returns every registered name, built-in and custom, which is what a palette picker UI should read from rather than a hard-coded list:

for (const name of ApexMaps.listPalettes()) {
  const { kind, stops, colorblindSafe } = ApexMaps.palette(name)
  // build a swatch strip from `stops`
}

An explicit color array is also accepted anywhere a palette name is, taken literally with no OkLab resampling, for when you've already chosen your exact per-class colors:

scale: { palette: ['#fee5d9', '#fcae91', '#fb6a4a', '#cb181d'] }

See also