Colour Palettes
When code needs more than one colour (chart series, icon plates, avatars, heat maps, status
ramps), it asks CuspPalette in MeshWeaver.Layout.Palettes for them. Don't hand-pick hex
lists, and don't compute HSL. The engine is pure and deterministic, has no state and needs no
hub, so the same call gives the same colours on every process and every deploy.
Why not HSL or a hand-picked list
For each hue, the colours a screen can show form a rough triangle: black, white, and the cusp, the most saturated colour of that hue. The cusp sits in very different places: yellow's is almost white (OKLab L 0.97), blue's is dark (L 0.45). HSL ignores this. At the same HSL "lightness", yellow is about six times brighter than blue, and a fixed saturation leaves some hues washed out and pushes others out of gamut. Hand-picked lists inherit the same unevenness from whoever picked them.
CuspPalette works in OKLCH (Björn Ottosson's OKLab in polar form), where equal lightness
steps look equal. It draws each colour from a smooth curve inside that hue's triangle. The method
is Wijffelaars et al., Generating Color Palettes using Intuitive Parameters (EuroVis 2008), as
reformulated for OKLCH by David Aerne's CuspHanger (MIT,
© David Aerne). The curve construction and the default parameters follow CuspHanger.
The guarantees, each pinned by CuspPaletteTest:
- Every colour is inside sRGB. The curve stays inside the triangle, and the final chroma is clamped to the exact gamut boundary.
- Ramps are strictly monotone in lightness, so neighbouring steps never swap brightness.
- Categorical colours share one lightness, so no series shouts louder than another. That is
perceived lightness, not WCAG contrast: relative luminance still differs by hue, so a text colour
that passes on one entry can fail on another. A fill that carries text goes through
EnsureContrast, the way the icon backplates do.
The API
using MeshWeaver.Layout.Palettes;
// The primitive: the colour on a hue's curve at a given lightness.
OklchColor c = CuspPalette.ColorAt(hue: 255, lightness: 0.6, saturation: 0.8);
// Sequential: one hue, dark to light. Brightness and contrast place the lightness range.
var ramp = CuspPalette.Sequential(7, hue: 255);
var ranged = CuspPalette.SequentialRange(5, hue: 145, minLightness: 0.3, maxLightness: 0.95);
// Diverging: two arms meeting at a light centre (odd counts get a mixed centre colour).
var div = CuspPalette.Diverging(7, hueStart: 25, hueEnd: 255);
// Categorical: distinct hues at ONE lightness; optionally inside an arc of the wheel.
var series = CuspPalette.Categorical(8); // L 0.62, starts at blue
var earth = CuspPalette.Categorical(6, 0.52, 0.45, startHue: 40, hueSpan: 110);
// Contrast: walk a colour's lightness until text of another colour reaches a WCAG ratio.
var plate = CuspPalette.EnsureContrast(c, OklchColor.FromHex("#ffffff"), 4.5);
string hex = c.ToHex(); // "#rrggbb", clipped to sRGB
string css = c.ToCss(); // "oklch(60% 0.18 255)", culture-invariant
string[] fills = series.ToHex();
The parameters:
| Parameter | Range | Meaning |
|---|---|---|
saturation |
0–1 | 0 is a grey ramp, 1 runs the curve through the cusp itself. Default 0.6. |
brightness |
0–1 | Where a ramp's lightness range sits. Default 0.75. |
contrast |
0–1 | How wide a ramp's lightness range is. The default grows with the step count: min(0.88, 0.34 + 0.06·n). |
lightness |
0–1 | OKLab L. 0.62 suits series marks on both themes; about 0.5 or below carries white text. |
OklabGamut.Cusp(hue) and OklabGamut.MaxChroma(lightness, hue) expose the gamut itself.
OklchColor converts to and from hex and linear sRGB, and reports RelativeLuminance and
ContrastRatio as WCAG defines them.
Where the platform uses it
ChartPalettes(MeshWeaver.Layout.Chart):Default,Pastel,BoldandEarthare generated, andCategorical(n),Sequential(n, hue)andDiverging(n, low, high)size a palette to the data rather than cutting a fixed list.IconBackplate.Palette(MeshWeaver.Graph): the ten plate hues a generated icon backplate draws from. Each is darkened only as far as needed for white detail to reach WCAG AA (4.5:1), whichIconBackplateTestpins.
When you add a new surface that needs several colours, call the engine. Don't copy its output into a literal list: a copied list stops following the engine, and the next agent tunes one copy and not the other.
One numeric detail
OKLab's sRGB boundary is not perfectly straight near blue. Along the edge from black to #0000ff,
the red channel dips to about −0.0007 (linear). A strict gamut test therefore cuts a sliver off
blue's cusp, reporting a chroma of 0.264 instead of 0.313. OklchColor.GamutTolerance (0.001,
linear) accepts that sliver. It is far below one 8-bit step, and ToHex clips it away.