Chris Almaguer Data visualization design

Case study - 02 / Tooling & accessibility

Auditing the auditor

Color Safety Lens audits palettes for contrast and color-vision deficiency. The first working version gave confident, authoritative, wrong answers. Off by more than ΔE 14 on some hues, where 1.0 is a visible difference.

Heatmap Live JavaScript
Color Safety Lens - running live Open full screen

Loading tool…

The actual tool, not a screenshot. Load a palette, ramp the tints, and run the color-vision simulation on the same linear-RGB pipeline described below.

01 - Context

Color never ships at 100%

Every brand guideline specifies color at full strength. In a dashboard that color spends its life at 60%, 30%, 10%. Fills, ramps, banding, disabled states. The guideline says nothing about those. That is where accessibility quietly breaks.

I was checking tints one at a time on webaim.org, dozens per dashboard, across fifty. Slow, and worse, it was sampling. I only caught the failures I thought to look for.

Tint ramp, audited both ways

Each step checked against a white label and a black one

Live · WCAG 2.1 relative luminance · 4.5:1 threshold for body text

Watch the label color flip. At full strength only a white label passes. One step down that reverses and only black passes. Every base color here does the same thing. There is no single label color that is safe across a tint ramp, so any dashboard that sets one label color for a whole chart family is failing somewhere on that ramp. That is not a judgment call, it is arithmetic, and it is the sort of thing worth having a machine check.

02 - Process

The first version computed the simulation on the wrong values

I used the Machado, Oliveira & Fernandes (2009) model, the standard CVD matrices. Read the values in, multiply through the matrix, write them back out. It rendered. It looked plausible. I shipped it.

The matrices operate on linear RGB, light as a physical quantity. Hex values in a design file are gamma-encoded sRGB, pre-distorted for how displays and eyes work. I was running a physics model on perceptual numbers.

The same simulation, computed both ways

Deuteranomaly at full severity

Wrong, gamma sRGB v0.1
Correct, linear RGB v1.0

Max ΔE2000

-

Mean ΔE2000

-

Reference

ΔE 1.0 is the threshold of a just-noticeable difference. Anything above about 2.3 is plainly visible to an ordinary observer.

Live · Machado et al. 2009 matrices · CIEDE2000 difference

Both strips claim to show the same thing. One is a simulation. The other is a picture of an arithmetic error. The tool gave no indication which was which, and that is what made it dangerous instead of merely wrong.

03 - Design

Rebuilt on linear values, then checked against published figures

Fixing the code took an afternoon: linearize in, apply, re-encode out. Trusting it again took much longer.

  • Rebuilt in linear RGB. Every operation, simulation, tinting, mixing, happens in linear space and converts back at the boundary.
  • Validated ΔE against Sharma's test suite. CIEDE2000 has known discontinuities that naive implementations get wrong. The published test data exists to catch them.
  • Ran Okabe and Ito through as a control. It is the palette designed by colorblind scientists to stay distinguishable. If my tool flagged it unsafe, my tool was still broken.

Five audits later, it passed clean.

The corrected simulator

Okabe and Ito, the control palette

100

Live · severity interpolated toward the published severity-1.0 matrices; the production tool uses Machado's full per-severity tables

Switch to the naive palette and drag severity up. Two hues that look clearly distinct at zero converge well before full severity, a failure affecting roughly 1 in 12 men. Okabe and Ito holds separation throughout. That is the point of it.

04 - Impact

Impact

The tool tests every color pair in a palette at once. Run against the dashboard palette in use at the time, it returned combinations that failed WCAG AA that hand-checking had missed.

Verified tints export as SVG back into Figma and into the design system instead of into a screenshot folder, which moved the audit from a final check into the definition of done. The source is public on GitHub.

Source on GitHub

03

115 laps at Thunderhill

Next case study. A year of my own telemetry, and what I found reviewing my own chart a year later.

Next