# Choosing colors for lines over heatmap

**URL:** <https://discourse.julialang.org/t/choosing-colors-for-lines-over-heatmap/117652>\
**Category:** Visualization\
**Tags:** colors, makie\
**Created:** [July 30, 2024, 8:31pm UTC](https://discourse.julialang.org/t/choosing-colors-for-lines-over-heatmap/117652 "2024-07-30T20:31:03Z")\
**Posts on this page:** 10\
**Page:** 1

<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:** [July 30, 2024, 8:31pm UTC](https://discourse.julialang.org/t/choosing-colors-for-lines-over-heatmap/117652/1 "2024-07-30T20:31:03Z")

</div>

I have a heatmap in viridis or inferno and I want to draw grid lines on it but I don’t know what color to make them so they’re visible. Is there a good automatic or manual way to pick colors for this?

```julia
let n = 500
f, ax, plt = heatmap(1:n, 1:n, rand(n,n))
vlines!(ax, 0:50:n; color=:orange)
hlines!(ax, 0:50:n; color=:blue)
f
end

```

 ![pic](https://global.discourse-cdn.com/julialang/original/3X/a/d/ad706de4ee480f5e1a42272fa530646964b28679.jpeg)

---

<div class="post-metadata">

**Author:** ![empet](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/empet/32/221303_2.png) [@empet](https://discourse.julialang.org/u/empet)\
**Post date:** [July 31, 2024, 7:41pm UTC](https://discourse.julialang.org/t/choosing-colors-for-lines-over-heatmap/117652/2 "2024-07-31T19:41:40Z")

</div>

Does the line width, `lw=2`, give a graph as expected?

```julia
let n = 500
plt = heatmap(1:n, 1:n, rand(n,n), aspect_ratio=:equal, xlims=(0,n), ylims=(0,n))
vline!(plt, 0:50:n; color=:yellow, lw=2, label=false)
hline!(plt, 0:50:n; color=:orange, lw=2, label=false)
end

```

---

<div class="post-metadata">

**Author:** ![nsajko](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/nsajko/32/221187_2.png) [@nsajko](https://discourse.julialang.org/u/nsajko)\
**Post date:** [July 31, 2024, 7:59pm UTC](https://discourse.julialang.org/t/choosing-colors-for-lines-over-heatmap/117652/3 "2024-07-31T19:59:35Z")

</div>

I believe what jariji is asking for, is an algorithm that would, given a colormap, return a color that’s _distinct_ enough from all the colors in the colormap?

I guess [`distinguishable_colors`](https://juliagraphics.github.io/Colors.jl/stable/colormapsandcolorscales/#Generating-distinguishable-colors) from Colors.jl is a solution.

Another approach perhaps might be to generate the color scheme together with the single distinguishable color, in which case ColorSchemes.jl and maybe ColorSchemeTools.jl might be useful.

At the lowest level, `Oklch` from ColorTypes.jl might also be worth looking into?

Hoping the ping is OK, I suppose @cormullion might have something to say.

---

<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:** [July 31, 2024, 9:24pm UTC](https://discourse.julialang.org/t/choosing-colors-for-lines-over-heatmap/117652/4 "2024-07-31T21:24:51Z")

</div>

Here’s what I’ve figured out.

I can make a nice collection of colors

```julia
using Colors, ColorSchemes, RectiGrids, AxisKeys, GLMakie, Makie, Statistics, ImageShow

GLMakie.activate!(;scalefactor=2)

MYCOLORS = map(grid(a=range(-100,100; length=8),b=range(-100,100; length=8), L=round.(Int, range(0,100; length=6)))) do (;L,a,b)
    Lab(L,a,b)
end

let fig = Figure(), colors = MYCOLORS
    Label(fig[1,1]; text="Colors", tellwidth=false)
    box = (fig[2,1])
    foreach(pairs(axiskeys(colors,3))) do (iL,L)
        ax = Axis(box[fldmod1(iL, 3)...]; title="L=$L", xlabel="a", ylabel="b")
        heatmap!(ax, axiskeys(colors, 2), axiskeys(colors, 3), colors[:,:, iL])
    end
    fig
end

```

 ![image](https://global.discourse-cdn.com/julialang/original/3X/d/8/d883ebf092fb033087ebc430452d5d517b26276a.png)

And I can measure the mean relative luminance of each one against all of the colors in viridis.

```julia
luminance(c) = XYZ(c).y

map(MYCOLORS) do c
    mean(ColorSchemes.viridis.colors) do v
        r = luminance(c)/luminance(v)
        max(r,inv(r))
    end
end
3-dimensional KeyedArray(NamedDimsArray(...)) with keys:
↓ a ∈ 8-element StepRangeLen{Float64,...}
→ b ∈ 8-element StepRangeLen{Float64,...}
◪ L ∈ 6-element Vector{Int64}
And data, 8×8×6 Array{Float64, 3}:
[:, :, 1] ~ (:, :, 0):
              (-100.0) (-71.4286) (-42.8571) (-14.2857) (14.2857) (42.8571) (71.4286) (100.0)
 (-100.0) Inf Inf Inf Inf Inf Inf Inf Inf
  (-71.4286) Inf Inf Inf Inf Inf Inf Inf Inf
  (-42.8571) Inf Inf Inf Inf Inf Inf Inf Inf
  (-14.2857) Inf Inf Inf Inf Inf Inf Inf Inf
   (14.2857) Inf Inf Inf Inf Inf Inf Inf Inf
   (42.8571) Inf Inf Inf Inf Inf Inf Inf Inf
   (71.4286) Inf Inf Inf Inf Inf Inf Inf Inf
  (100.0) Inf Inf Inf Inf Inf Inf Inf Inf

[:, :, 2] ~ (:, :, 20):
              (-100.0) (-71.4286) (-42.8571) (-14.2857) (14.2857) (42.8571) (71.4286) (100.0)
 (-100.0) 9.48118 9.48118 9.48118 9.48118 9.48118 9.48118 9.48118 9.48118
  (-71.4286) 9.48118 9.48118 9.48118 9.48118 9.48118 9.48118 9.48118 9.48118
  (-42.8571) 9.48118 9.48118 9.48118 9.48118 9.48118 9.48118 9.48118 9.48118
  (-14.2857) 9.48118 9.48118 9.48118 9.48118 9.48118 9.48118 9.48118 9.48118
   (14.2857) 9.48118 9.48118 9.48118 9.48118 9.48118 9.48118 9.48118 9.48118
   (42.8571) 9.48118 9.48118 9.48118 9.48118 9.48118 9.48118 9.48118 9.48118
   (71.4286) 9.48118 9.48118 9.48118 9.48118 9.48118 9.48118 9.48118 9.48118
  (100.0) 9.48118 9.48118 9.48118 9.48118 9.48118 9.48118 9.48118 9.48118

[:, :, 3] ~ (:, :, 40):
              (-100.0) (-71.4286) (-42.8571) (-14.2857) (14.2857) (42.8571) (71.4286) (100.0)
 (-100.0) 3.12448 3.12448 3.12448 3.12448 3.12448 3.12448 3.12448 3.12448
  (-71.4286) 3.12448 3.12448 3.12448 3.12448 3.12448 3.12448 3.12448 3.12448
  (-42.8571) 3.12448 3.12448 3.12448 3.12448 3.12448 3.12448 3.12448 3.12448
  (-14.2857) 3.12448 3.12448 3.12448 3.12448 3.12448 3.12448 3.12448 3.12448
   (14.2857) 3.12448 3.12448 3.12448 3.12448 3.12448 3.12448 3.12448 3.12448
   (42.8571) 3.12448 3.12448 3.12448 3.12448 3.12448 3.12448 3.12448 3.12448
   (71.4286) 3.12448 3.12448 3.12448 3.12448 3.12448 3.12448 3.12448 3.12448
  (100.0) 3.12448 3.12448 3.12448 3.12448 3.12448 3.12448 3.12448 3.12448

[:, :, 4] ~ (:, :, 60):
              (-100.0) (-71.4286) (-42.8571) (-14.2857) (14.2857) (42.8571) (71.4286) (100.0)
 (-100.0) 3.12086 3.12086 3.12086 3.12086 3.12086 3.12086 3.12086 3.12086
  (-71.4286) 3.12086 3.12086 3.12086 3.12086 3.12086 3.12086 3.12086 3.12086
  (-42.8571) 3.12086 3.12086 3.12086 3.12086 3.12086 3.12086 3.12086 3.12086
  (-14.2857) 3.12086 3.12086 3.12086 3.12086 3.12086 3.12086 3.12086 3.12086
   (14.2857) 3.12086 3.12086 3.12086 3.12086 3.12086 3.12086 3.12086 3.12086
   (42.8571) 3.12086 3.12086 3.12086 3.12086 3.12086 3.12086 3.12086 3.12086
   (71.4286) 3.12086 3.12086 3.12086 3.12086 3.12086 3.12086 3.12086 3.12086
  (100.0) 3.12086 3.12086 3.12086 3.12086 3.12086 3.12086 3.12086 3.12086

[:, :, 5] ~ (:, :, 80):
              (-100.0) (-71.4286) (-42.8571) (-14.2857) (14.2857) (42.8571) (71.4286) (100.0)
 (-100.0) 5.32289 5.32289 5.32289 5.32289 5.32289 5.32289 5.32289 5.32289
  (-71.4286) 5.32289 5.32289 5.32289 5.32289 5.32289 5.32289 5.32289 5.32289
  (-42.8571) 5.32289 5.32289 5.32289 5.32289 5.32289 5.32289 5.32289 5.32289
  (-14.2857) 5.32289 5.32289 5.32289 5.32289 5.32289 5.32289 5.32289 5.32289
   (14.2857) 5.32289 5.32289 5.32289 5.32289 5.32289 5.32289 5.32289 5.32289
   (42.8571) 5.32289 5.32289 5.32289 5.32289 5.32289 5.32289 5.32289 5.32289
   (71.4286) 5.32289 5.32289 5.32289 5.32289 5.32289 5.32289 5.32289 5.32289
  (100.0) 5.32289 5.32289 5.32289 5.32289 5.32289 5.32289 5.32289 5.32289

[:, :, 6] ~ (:, :, 100):
              (-100.0) (-71.4286) (-42.8571) (-14.2857) (14.2857) (42.8571) (71.4286) (100.0)
 (-100.0) 9.30165 9.30165 9.30165 9.30165 9.30165 9.30165 9.30165 9.30165
  (-71.4286) 9.30165 9.30165 9.30165 9.30165 9.30165 9.30165 9.30165 9.30165
  (-42.8571) 9.30165 9.30165 9.30165 9.30165 9.30165 9.30165 9.30165 9.30165
  (-14.2857) 9.30165 9.30165 9.30165 9.30165 9.30165 9.30165 9.30165 9.30165
   (14.2857) 9.30165 9.30165 9.30165 9.30165 9.30165 9.30165 9.30165 9.30165
   (42.8571) 9.30165 9.30165 9.30165 9.30165 9.30165 9.30165 9.30165 9.30165
   (71.4286) 9.30165 9.30165 9.30165 9.30165 9.30165 9.30165 9.30165 9.30165
  (100.0) 9.30165 9.30165 9.30165 9.30165 9.30165 9.30165 9.30165 9.30165

```

So I guess the midrange Lab L values produce low mean relative luminance against viridis colors.

Visualizing seems consistent with that, the middle L values have some hard-to-see points. Or maybe I’m just fooling myself – those top-right and bottom-left dots are pretty hard to make out. Maybe @cormullion can say how off-base I am.

```julia
let fig = Figure()
    viridis = ColorSchemes.viridis.colors
    vgrid = repeat(viridis, 1, length(viridis))'
    map(enumerate(axiskeys(MYCOLORS, 3))) do (i, L )
        ax = Axis(fig[1,i]; title="L=$L", xlabel="a", ylabel="b")
        heatmap!(ax, 1.1axiskeys(MYCOLORS, 1), 1.1axiskeys(MYCOLORS, 2), vgrid)
        foreach(axiskeys(MYCOLORS, 1)) do a
            c = vec(MYCOLORS(a, :, L))
            scatter!(ax, fill(a, length(axiskeys(MYCOLORS, 2))), axiskeys(MYCOLORS, 2), color=c)
        end
    end
    fig
end

```

 ![image](https://global.discourse-cdn.com/julialang/original/3X/5/6/56be9761feaee4eec3cafc1d6802c1011139db8d.png)

`distinguishable_colors` makes some colors too:

```julia
distinguished_colors(n, cs; kw...) = distinguishable_colors(length(cs)+n, cs; kw...)[length(cs)+1:end]
let fig = Figure()
    viridis = ColorSchemes.viridis.colors
    vgrid = repeat(viridis, 1, length(viridis))'
    ax = Axis(fig[1,1,])
    heatmap!(ax, vgrid)
    n = 10
    cs = distinguished_colors(n, viridis)
    foreach(1:n) do k
        scatter!(ax, fill(length(viridis)÷n*k, length(n)), range(10, .95length(viridis); length=n), color=circshift(cs, k))
    end
    fig
end

```

 ![image](https://global.discourse-cdn.com/julialang/original/3X/e/a/ea4e8576e58cabb62e76a25ccfe37f6f706f42b9.png)

On the heatmap some `L=100` colors

```julia
let n = 500, k = 25
    fig, ax, plt = heatmap(rand(n,n))
    colors = map(rand(-100:100, k), rand(-100:100, k)) do a,b
        Lab(100, a,b)
    end
    vlines!(ax, range(0,n; length=k); color=colors)
    fig
end

```

 ![L=100](https://global.discourse-cdn.com/julialang/original/3X/3/a/3a0e422cbdab9ca40592b05ca564d8c791b046e1.jpeg)

and the `distinguishable_colors`

```julia
let n = 500, k = 20
    fig, ax, plt = heatmap(rand(n,n))
    vlines!(ax, range(0,n; length=k); color=distinguished_colors(k, ColorSchemes.viridis.colors))
    fig
end

```

 ![dist](https://global.discourse-cdn.com/julialang/original/3X/6/1/615290f8d56b00399c4240f89d64c2b6d367fe94.jpeg)

---

<div class="post-metadata">

**Author:** ![empet](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/empet/32/221303_2.png) [@empet](https://discourse.julialang.org/u/empet)\
**Post date:** [July 31, 2024, 10:10pm UTC](https://discourse.julialang.org/t/choosing-colors-for-lines-over-heatmap/117652/5 "2024-07-31T22:10:12Z")

</div>

A possible algorithm should work as follows:

- evaluate the colorscheme, `cmap` at n values in [0,1]
- define m=10 random colors, of type RGB{Float{64}}
- compute the `colordiff` ([https://juliagraphics.github.io/Colors.jl/stable/colordifferences/](https://juliagraphics.github.io/Colors.jl/stable/colordifferences/))  
between each random color and the colors in the colorscheme, and keep the maximum value
- the maximum among these maximum values corresponds to a distinct color:

```julia
using Colors, ColorSchemes, Plots
function difcolor(cmap::ColorScheme, dcolors::Vector{RGB{Float64}};n=128, metric=DE_2000())
   cmapcolors = get(cmap, range(0, 1, n))
   ic = Float64[]
   for d in dcolors
       push!(ic, maximum(colordiff.(Ref(d), cmapcolors; metric=metric)))
   end 
   _, idx = findmax(ic)
   dcolors[idx]    
end 

cmap = ColorSchemes.plasma
dcolors=rand( RGB{Float64}, 10)
col = difcolor(cmap, dcolors)

let n = 500
plt = heatmap(1:n, 1:n, rand(n,n), aspect_ratio=:equal, xlims=(0,n), ylims=(0,n), colorscheme=cmap)
vline!(plt, 0:50:n; color=col, lw=2, label=false)
hline!(plt, 0:50:n; color=col, lw=2, label=false)
end 

```

---

<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:** [July 31, 2024, 10:48pm UTC](https://discourse.julialang.org/t/choosing-colors-for-lines-over-heatmap/117652/6 "2024-07-31T22:48:51Z")

</div>

FWIW, Edward Tufte’s controversial take on grid lines:

“_One of the more sedate graphical elements, the grid should usually be muted or completely suppressed so that its presence is only implicit - lest it compete with the data. Grids are mostly for the initial plotting of data … rather than for putting into print. Dark grid lines are chartjunk. They carry no information, clutter up the graphic, and generate graphic activity unrelated to data information._  
_…_  
_When a graphic serves as a look-up table, then a grid may help in reading and interpolating. But even in this case the grids should be muted relative to the data._”

---

<div class="post-metadata">

**Author:** ![lmiq](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/lmiq/32/18314_2.png) [@lmiq](https://discourse.julialang.org/u/lmiq)\
**Post date:** [August 1, 2024, 12:46am UTC](https://discourse.julialang.org/t/choosing-colors-for-lines-over-heatmap/117652/7 "2024-08-01T00:46:49Z")

</div>

FWIW, white grid lines are quite visible there:

```julia-repl
julia> using Plots

julia> let n = 500
       plt = heatmap(1:n, 1:n, rand(n,n), aspect_ratio=:equal, xlims=(0,n), ylims=(0,n), color=Plots.cgrad(:viridis))
       vline!(plt, 0:50:n; color=:white, lw=1, label=false)
       hline!(plt, 0:50:n; color=:white, lw=1, label=false)
       plot!(plt, framestyle=:box)
       end

```

 ![image](https://global.discourse-cdn.com/julialang/original/3X/1/b/1b70c86053cb99b8b61e831b932e40937c72a7a7.jpeg)

---

<div class="post-metadata">

**Author:** ![empet](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/empet/32/221303_2.png) [@empet](https://discourse.julialang.org/u/empet)\
**Post date:** [August 1, 2024, 2:09am UTC](https://discourse.julialang.org/t/choosing-colors-for-lines-over-heatmap/117652/8 "2024-08-01T02:09:59Z")

</div>

Tufte refers to background gridlines. As a rule, grid lines are not added to heatmaps, but PlotlyJS, for example, allows for gaps between bricks:  
 ![heatmap-gaps](https://global.discourse-cdn.com/julialang/original/3X/3/0/30848ff8279faea11363b97e6d51cd950802351c.png)

---

<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:** [August 1, 2024, 9:48am UTC](https://discourse.julialang.org/t/choosing-colors-for-lines-over-heatmap/117652/9 "2024-08-01T09:48:59Z")

</div>

One thing you can do if you have a difficult background to annotate is to draw twice, in contrasting colors, thick and thin - that way, every combination of background and foreground has a chance of being visible to every viewer.

 ![Screenshot 2024-08-01 at 10.38.38](https://global.discourse-cdn.com/julialang/original/3X/2/2/228374528cbdbb89b55b335ab1a0d5cd1a6b4476.png)

Markers too:

 ![Screenshot 2024-08-01 at 10.43.31](https://global.discourse-cdn.com/julialang/original/3X/6/7/67eaf6fbec1757176de566614171262861462223.png)

Text is tricky though. 🙂

---

<div class="post-metadata">

**Author:** ![aplavin](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/aplavin/32/222056_2.png) [@aplavin](https://discourse.julialang.org/u/aplavin)\
**Post date:** [August 1, 2024, 10:43pm UTC](https://discourse.julialang.org/t/choosing-colors-for-lines-over-heatmap/117652/10 "2024-08-01T22:43:09Z")

</div>

> [@cormullion](#):
>
> Text is tricky though. 🙂

Makie has “text glow” that can help (:  
Although, by default it looks weird, one should use MakieExtra’s `textglow()` until text glow is fixed in Makie itself.  
MakieExtra.jl also provides `linesglow()` to make drawing lines with glow/stroke convenient – see examples at [https://aplavin.github.io/MakieExtra.jl/test/examples.html](https://aplavin.github.io/MakieExtra.jl/test/examples.html).
