# In search of categorical color scheme families: dark, same perceptual brightness

**URL:** <https://discourse.julialang.org/t/in-search-of-categorical-color-scheme-families-dark-same-perceptual-brightness/109560>\
**Category:** Visualization\
**Tags:** question, colors\
**Created:** [February 1, 2024, 9:52am UTC](https://discourse.julialang.org/t/in-search-of-categorical-color-scheme-families-dark-same-perceptual-brightness/109560 "2024-02-01T09:52:58Z")\
**Posts on this page:** 20\
**Page:** 1

<div class="post-metadata">

**Author:** ![Tamas\_Papp](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/tamas_papp/32/25949_2.png) [@Tamas\_Papp](https://discourse.julialang.org/u/Tamas_Papp)\
**Post date:** [February 1, 2024, 9:52am UTC](https://discourse.julialang.org/t/in-search-of-categorical-color-scheme-families-dark-same-perceptual-brightness/109560/1 "2024-02-01T09:52:58Z")

</div>

I frequently want to plot categorical data on a white background (eg a scatterplot, with various colors for each kind). My ideal color scheme would be

1. reasonably _dark_, as I am plotting on a white background,
2. have more or less the same perceived brightness
3. support at least 3–5 colors, 6–7 would be nice if possible, but more become confusing
4. bonus: have 2–3 non-overlapping palettes

[Dark2 from ColorBrewer](https://docs.juliaplots.org/latest/generated/colorschemes/#Pre-defined-ColorSchemes) comes close, but the ochre gets lost on white and the two greens are easy to confuse.

I will explain (4) above: suppose in a paper I want to use one color palette for some categorical variable in all plots, and another for another palette for another categorical variable for all relevant plots. The two palettes would not show up in the same plot, but I want to avoid confusion.  
families

I guess I could try a rotation around the white point in CIELUV or similar, since Colors.jl supports it, but I am hoping that someone has done this work and I could just use the results.

---

<div class="post-metadata">

**Author:** ![Tamas\_Papp](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/tamas_papp/32/25949_2.png) [@Tamas\_Papp](https://discourse.julialang.org/u/Tamas_Papp)\
**Post date:** [February 1, 2024, 10:41am UTC](https://discourse.julialang.org/t/in-search-of-categorical-color-scheme-families-dark-same-perceptual-brightness/109560/2 "2024-02-01T10:41:26Z")

</div>

A naive experiment:

```julia
using Colors

"`n` colors with evenly distributed hues, luminance `l`"
function my_palette(n, l)
    hues = range(0, 360; length = n + 1)[begin:(end-1)]
    function _f(h)
        c = MSC(h, l)
        LCHuv(l, c, h)
    end
    _f.(hues)
end

```

But `my_palette(5, 50)` gives

![x](https://global.discourse-cdn.com/julialang/original/3X/b/e/bed770b47508490bc16ff1f1747c84778fc21800.png)

which has greenish colors that are again too close.

Maybe take a bunch of initial colors and maximize pairwise CIE \Delta E in some space? Colors.jl supports those calculations, but maybe maxing saturation out is not such a good idea.

---

<div class="post-metadata">

**Author:** ![Tamas\_Papp](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/tamas_papp/32/25949_2.png) [@Tamas\_Papp](https://discourse.julialang.org/u/Tamas_Papp)\
**Post date:** [February 1, 2024, 10:57am UTC](https://discourse.julialang.org/t/in-search-of-categorical-color-scheme-families-dark-same-perceptual-brightness/109560/3 "2024-02-01T10:57:01Z")

</div>

When in doubt, compute stuff.

```julia
const WHITE = convert(LCHuv, colorant"white")
const BLACK = convert(LCHuv, colorant"black")

function pairwise_ΔE(colors; black_correction = 1.0)
    _lch = convert.(LCHuv, colors)
    min_ΔE = Inf
    function _update!(c1, c2, correction = 1.0)
        min_ΔE = min(min_ΔE, colordiff(c1, c2))
    end
    for (i, c1) in enumerate(_lch)
        _update!(WHITE, c1)
        _update!(BLACK, c1, black_correction) # OK to get closer to black
        for c2 in @view(_lch[(i+1):end])
            _update!(c1, c2)
        end
    end
    min_ΔE
end

random_colors(n) = [RGB(rand(), rand(), rand()) for _ in 1:5]

r = sort!([(p = random_colors(5); p => pairwise_ΔE(p)) for _ in 1:100000],
          by = last, rev = true)
permutedims(mapreduce(first, hcat, r[1:10]))

```

 ![r](https://global.discourse-cdn.com/julialang/original/3X/e/7/e79359fc5551f507179be7f85b608b2320cda04a.png)

I guess I could pick manually, some of them are not bad at all. But notice that the first one is like the first attempt, calculated from hues.

---

<div class="post-metadata">

**Author:** ![rafael.guerra](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/rafael.guerra/32/216610_2.png) [@rafael.guerra](https://discourse.julialang.org/u/rafael.guerra)\
**Post date:** [February 1, 2024, 11:35am UTC](https://discourse.julialang.org/t/in-search-of-categorical-color-scheme-families-dark-same-perceptual-brightness/109560/4 "2024-02-01T11:35:04Z")

</div>

Take a [look here](https://discourse.julialang.org/t/color-blind-friendly-color-sequence-palette-for-line-graphs/92551/6) for more color inspiration.

---

<div class="post-metadata">

**Author:** ![Tamas\_Papp](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/tamas_papp/32/25949_2.png) [@Tamas\_Papp](https://discourse.julialang.org/u/Tamas_Papp)\
**Post date:** [February 1, 2024, 12:28pm UTC](https://discourse.julialang.org/t/in-search-of-categorical-color-scheme-families-dark-same-perceptual-brightness/109560/5 "2024-02-01T12:28:31Z")

</div>

Thanks, but those are not either too dark or not dark enough.

Second attempt, using the `DE_CMC()` metric (because it looks best to my eye), forcing a kind of dark yellow to be there (otherwise yellowish colors are pretty rare, why?), and results sorted by hue:

 ![xx](https://global.discourse-cdn.com/julialang/original/3X/7/9/79e05be16d838f4e0594afa2b874e8ac6fbf1fbd.png)

The first one looks kind of OK, so if anyone wants it, here are the values:

```julia
LCHuv{Float64}(100.0,0.0,0.0)
LCHuv{Float64}(0.0,0.0,0.0)
LCHuv{Float64}(51.863153619748246,93.77645820051907,23.238375666328537)
LCHuv{Float64}(70.5,84.0,70.5)
LCHuv{Float64}(43.99159466563735,61.591721169405666,131.19968337189033)
LCHuv{Float64}(49.53323427719795,62.386038925970134,250.83883848314323)
LCHuv{Float64}(37.2494937150584,131.4158387710182,270.28598024082146)
LCHuv{Float64}(29.91785388310216,25.124316742516655,310.2475589723785)
LCHuv{Float64}(10.898674004674053,21.026130868830556,335.6789198210797)
LCHuv{Float64}(52.5930045850733,129.7328214196894,352.8318079349615)

```

> **Code**
>
> ```julia
> using Colors
> 
> function pairwise_ΔE(colors,
> adjusted_constants = [WHITE => 0.5, BLACK => 2.0];
> metric = DE_2000())
> _lch = convert.(LCHuv, colors)
> min_ΔE = Inf
> function _update!(c1, c2, correction = 1.0)
> min_ΔE = min(min_ΔE, colordiff(c1, c2; metric) * correction)
> end
> for (i, c1) in enumerate(_lch)
> for (c, correction) in adjusted_constants
> _update!(c, c1, correction)
> end
> for c2 in @view(_lch[(i+1):end])
> _update!(c1, c2)
> end
> end
> sort!(vcat(_lch, first.(adjusted_constants)), by = hue) => min_ΔE
> end
> 
> pre_list = [LCHuv(100.0, 0.0, 0.0) => 0.7, # white plot background, want extra distance
> LCHuv(0.0, 0.0, 0.0) => 1.2, # black lines, no direct overlap, OK to be closer
> LCHuv(70.5, 84, 70.5) => 1.0] # want to include manually
> 
> random_colors(n) = [RGB(rand(), rand(), rand()) for _ in 1:n]
> 
> r = sort!([pairwise_ΔE(random_colors(7), pre_list; metric = DE_CMC()) for _ in 1:100000],
> by = last, rev = true)
> permutedims(mapreduce(first, hcat, r[1:10]))
> 
> ```

---

<div class="post-metadata">

**Author:** ![BeastyBlacksmith](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/beastyblacksmith/32/4741_2.png) [@BeastyBlacksmith](https://discourse.julialang.org/u/BeastyBlacksmith)\
**Post date:** [February 1, 2024, 12:52pm UTC](https://discourse.julialang.org/t/in-search-of-categorical-color-scheme-families-dark-same-perceptual-brightness/109560/6 "2024-02-01T12:52:05Z")

</div>

> [@Tamas\_Papp](#):
>
> otherwise yellowish colors are pretty rare, why?

I’d say this is a feature, since yellow on white is a terrible look in my opinion.

---

<div class="post-metadata">

**Author:** ![rafael.guerra](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/rafael.guerra/32/216610_2.png) [@rafael.guerra](https://discourse.julialang.org/u/rafael.guerra)\
**Post date:** [February 1, 2024, 1:18pm UTC](https://discourse.julialang.org/t/in-search-of-categorical-color-scheme-families-dark-same-perceptual-brightness/109560/7 "2024-02-01T13:18:09Z")

</div>

> [@BeastyBlacksmith](#):
>
> yellow on white is a terrible look

 ![Screenshot_20240201-141123~2](https://global.discourse-cdn.com/julialang/original/3X/6/2/6298e59622a351ad77e624bb99d7043e7a8321c5.png)

---

<div class="post-metadata">

**Author:** ![Tamas\_Papp](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/tamas_papp/32/25949_2.png) [@Tamas\_Papp](https://discourse.julialang.org/u/Tamas_Papp)\
**Post date:** [February 1, 2024, 1:55pm UTC](https://discourse.julialang.org/t/in-search-of-categorical-color-scheme-families-dark-same-perceptual-brightness/109560/8 "2024-02-01T13:55:32Z")

</div>

> [@BeastyBlacksmith](#):
>
> yellow on white is a terrible look in my opinion

Depends on brightness/saturation I guess. I think that a darker yellow is as good as any other color.

I think the real reason is that what we call “yellow” is a rather narrow slice on the [xy color diagram](https://en.wikipedia.org/wiki/CIE_1931_color_space), compared to eg green and red. So it has a lower chance of showing up.

---

<div class="post-metadata">

**Author:** ![Jeff\_Emanuel](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/jeff_emanuel/32/15440_2.png) [@Jeff\_Emanuel](https://discourse.julialang.org/u/Jeff_Emanuel)\
**Post date:** [February 1, 2024, 3:42pm UTC](https://discourse.julialang.org/t/in-search-of-categorical-color-scheme-families-dark-same-perceptual-brightness/109560/9 "2024-02-01T15:42:00Z")

</div>

I’ve pursued distinct colors for this purpose several times. I especially appreciate you posting your work here because I have tritanopia. It’s genetic–my mother had it, most of my siblings and children also have it, and we’ve participated as test subjects in a published study. It manifests itself as inability to distinguish colors that differ in a small degree of yellow content. For example, I can’t distinguish blue and green when they get relatively close, such as in post #2. My wife has to put white-out on green board game pieces so that I can distinguish them from the blue pieces. I also have difficulty with brown vs. purple, orange vs. pink, and especially pale yellow on white. I’ve bookmarked your posts and will return to them the next time I need to generate distinct colors.

PS I can’t see the boundary between the yellow and white in my avatar.

---

<div class="post-metadata">

**Author:** ![savq](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/savq/32/22063_2.png) [@savq](https://discourse.julialang.org/u/savq)\
**Post date:** [February 1, 2024, 9:37pm UTC](https://discourse.julialang.org/t/in-search-of-categorical-color-scheme-families-dark-same-perceptual-brightness/109560/10 "2024-02-01T21:37:46Z")

</div>

Running a similar script using okhsl I get a better green/blue contrast IMO.

![okhsl](https://global.discourse-cdn.com/julialang/original/3X/2/6/262758fdce8b6cd4896bfebd0853a59eb3a80d87.png)

Oklab has a better \Delta E too so your other scripts could also give better results.

(Note that the PR for oklab in Colors.jl has been stalled for a while, I used my own implementation here.)

---

<div class="post-metadata">

**Author:** ![cormullion](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/cormullion/32/49131_2.png) [@cormullion](https://discourse.julialang.org/u/cormullion)\
**Post date:** [February 2, 2024, 8:19am UTC](https://discourse.julialang.org/t/in-search-of-categorical-color-scheme-families-dark-same-perceptual-brightness/109560/11 "2024-02-02T08:19:07Z")

</div>

> [@savq](#):
>
> Note that the PR for oklab in [Colors.jl](https://juliahub.com/ui/Packages/Colors) has been stalled for a while

Yes, unfortunately Colors.jl has been without maintainers for a couple of years; kimikage stepped away, and timholy and johnnychen are too busy elsewhere… It needs someone with the time,authority, and expertise to take up the challenge. But hopefully one day your PRs will be merged!

---

<div class="post-metadata">

**Author:** ![liuyxpp](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/liuyxpp/32/9870_2.png) [@liuyxpp](https://discourse.julialang.org/u/liuyxpp)\
**Post date:** [February 2, 2024, 8:55am UTC](https://discourse.julialang.org/t/in-search-of-categorical-color-scheme-families-dark-same-perceptual-brightness/109560/12 "2024-02-02T08:55:16Z")

</div>

Can you post the RGB for these colors? They look great and I want to try them out. Thanks!

---

<div class="post-metadata">

**Author:** ![savq](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/savq/32/22063_2.png) [@savq](https://discourse.julialang.org/u/savq)\
**Post date:** [February 2, 2024, 5:55pm UTC](https://discourse.julialang.org/t/in-search-of-categorical-color-scheme-families-dark-same-perceptual-brightness/109560/13 "2024-02-02T17:55:45Z")

</div>

Yeah, I don’t blame them. Colors requires a lot of technical knowledge, but compared to other projects in the ecosystem it seems inconsequential.

The PRs for oklab are not mine, altho hopefully I’ll have time in the future to clean up what I’ve written and make a separate PR for okhsv and okhsl.

---

<div class="post-metadata">

**Author:** ![savq](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/savq/32/22063_2.png) [@savq](https://discourse.julialang.org/u/savq)\
**Post date:** [February 2, 2024, 5:59pm UTC](https://discourse.julialang.org/t/in-search-of-categorical-color-scheme-families-dark-same-perceptual-brightness/109560/14 "2024-02-02T17:59:33Z")

</div>

The colors are:

```julia
#d70071
#a26900
#00911c
#00859d
#7947ff

```

And a similar 8 color palette:

```julia
#d70071
#c14f00
#927300
#478b00
#008a79
#0083a7
#465fff
#b101e4

```

---

<div class="post-metadata">

**Author:** ![Tamas\_Papp](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/tamas_papp/32/25949_2.png) [@Tamas\_Papp](https://discourse.julialang.org/u/Tamas_Papp)\
**Post date:** [February 5, 2024, 10:24am UTC](https://discourse.julialang.org/t/in-search-of-categorical-color-scheme-families-dark-same-perceptual-brightness/109560/15 "2024-02-05T10:24:48Z")

</div>

> [@savq](#):
>
> using okhsl

Is this code public anywhere?

> [@cormullion](#):
>
> unfortunately [Colors.jl](https://juliahub.com/ui/Packages/Colors) has been without maintainers for a couple of years

Yes, this is unfortunate, but since ColorTypes.jl exposes core types, one can implement a lot of stuff outside Colors.jl and still hook into the ecosystem.

---

<div class="post-metadata">

**Author:** ![jar1](https://avatars.discourse-cdn.com/v4/letter/j/c0e974/32.png) [@jar1](https://discourse.julialang.org/u/jar1)\
**Post date:** [February 5, 2024, 10:47am UTC](https://discourse.julialang.org/t/in-search-of-categorical-color-scheme-families-dark-same-perceptual-brightness/109560/16 "2024-02-05T10:47:12Z")

</div>

“[OKLCH in CSS: why we moved from RGB and HSL—Martian Chronicles, Evil Martians’ team blog](https://evilmartians.com/chronicles/oklch-in-css-why-quit-rgb-hsl)” argues for OKLCH over HSL for generating palettes. You can try making some colors in [https://oklch.com](https://oklch.com) and palettes at [https://huetone.ardov.me/](https://huetone.ardov.me/). OKLCH is now available for [CSS in browsers](https://www.w3.org/TR/css-color-4/#ref-for-valdef-oklch-oklch).

---

<div class="post-metadata">

**Author:** ![nealmckee](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/nealmckee/32/206797_2.png) [@nealmckee](https://discourse.julialang.org/u/nealmckee)\
**Post date:** [February 5, 2024, 10:34pm UTC](https://discourse.julialang.org/t/in-search-of-categorical-color-scheme-families-dark-same-perceptual-brightness/109560/17 "2024-02-05T22:34:52Z")

</div>

This thread was mentioned in my (now superceded) PR for implementing OKLab and OKLch. I’m actually still using Colors.jl with a simple (90 lines), rudimentarily grafted on implementation of those for my personal color science projects with great success.

Regarding your original question, I’ve actually probably solved exactly what you are looking for in the past (with even identical chroma, not just luminance), though in the context of syntax highlighting. Check out the non-background colors (six, seven or eight) as well as their construction here: [Penumbra](https://github.com/nealmckee/penumbra)

The hexagonal (similarly pentagonal, heptagonal etc.) vertices of equal luminance and maximum chroma in sRGB (which I would still target, P3 is not common enough by far) are actually around a luminance of about 0.75, so your request for relatively dark colors is tricky since their chroma will decrease with the luminance. A luminance of 0.65 just barely meets the least stringent W3C contrast criteria, which is what I chose for the basic version of the color scheme above.

Huge caveat on all of this though! I would strongly advise **against** using such a color scheme for data visualisation for any purpose that will include sharing with others as it is **not accessible** for people with color vision deficiency, it’s actually especially bad as luminance helps this group with differentiation.

I’ve just last week developed the (to my knowledge) first CVD-accessible discrete palette that is truly optimised from first principles, but haven’t gotten around to re-verify my assumptions (especially the implementation of color blindness simulation in Colors.jl), write everything up and publish it yet. There will definitely also be a repository, so by following my GitHub it should show up once I make it public. Might be a couple of weeks (or even months) since I might turn it into an actual paper, but definitely sometime this year.  
To allow for differentiability for the color blind, there is relatively large variance in luminance, going against your original request, but I think in this case accessibility should come first.  
In the meantime, the Wong palette available in Makie.jl (actually Okabe-Ito) and used as the default in AlgebraOfGraphics.jl is somewhat serviceable (I suspect it has only been verified against anomalous trichromacy though, not full dichromacy, or might have used outdated math) and there’s also the [“Bright” Palette by Paul Tol](https://personal.sron.nl/~pault/), which uses a less precise color blindness simulation and disregards the rarest form entirely, but is somewhat more principled for the other two.

---

<div class="post-metadata">

**Author:** ![savq](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/savq/32/22063_2.png) [@savq](https://discourse.julialang.org/u/savq)\
**Post date:** [February 6, 2024, 1:12am UTC](https://discourse.julialang.org/t/in-search-of-categorical-color-scheme-families-dark-same-perceptual-brightness/109560/18 "2024-02-06T01:12:31Z")

</div>

> Is this code public anywhere?

Yep. My draft is [here](https://github.com/savq/ColorUtils.jl/blob/ok/src/okcolor.jl). It’s a translation of Ottosson’s original C++ code, and the js version used in his [color picker](https://github.com/bottosson/bottosson.github.io/blob/master/misc/colorpicker/colorconversion.js). The original code is written in a very… game-dev way. so there’s _a lot_ that could be improved there.

---

<div class="post-metadata">

**Author:** ![Tamas\_Papp](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/tamas_papp/32/25949_2.png) [@Tamas\_Papp](https://discourse.julialang.org/u/Tamas_Papp)\
**Post date:** [February 6, 2024, 8:57am UTC](https://discourse.julialang.org/t/in-search-of-categorical-color-scheme-families-dark-same-perceptual-brightness/109560/19 "2024-02-06T08:57:57Z")

</div>

Thanks for your detailed reply. I now think that my problem is near impossible: it is very challenging to find even 5 colors that

1. work when viewed on a screen by a color-blind person,
2. survive printing on an unsophisticated garden variety color laser printer,
3. are distinguishable when I present on a projector that has some problem (the lamp burning out etc)

Maybe the “high contrast” scheme of Paul Tol works for 3 colors, but that is pretty much the limit I guess. Perhaps I should look for other ways to present the data that has categories, eg subplots.

---

<div class="post-metadata">

**Author:** ![rafael.guerra](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/rafael.guerra/32/216610_2.png) [@rafael.guerra](https://discourse.julialang.org/u/rafael.guerra)\
**Post date:** [February 6, 2024, 11:46am UTC](https://discourse.julialang.org/t/in-search-of-categorical-color-scheme-families-dark-same-perceptual-brightness/109560/20 "2024-02-06T11:46:45Z")

</div>

For comparison, from the site I linked above, the following 5 colors should be 99.99% accessible:

 ![99.9_accessibility_colors_sashamaps](https://global.discourse-cdn.com/julialang/original/3X/c/b/cb7cc47795762d49a981ffdaccf5ded0c39aeb80.png)

[Next page](https://discourse.julialang.org/t/in-search-of-categorical-color-scheme-families-dark-same-perceptual-brightness/109560.md?page=2)
