# Choosing colors for lines over heatmap

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?

``````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
``````

1 Like

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

``````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
``````

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` 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.

3 Likes

Here’s what I’ve figured out.

I can make a nice collection of colors

``````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
``````

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

``````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.

``````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
``````

`distinguishable_colors` makes some colors too:

``````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
``````

On the heatmap some `L=100` colors

``````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
``````

and the `distinguishable_colors`

``````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
``````

2 Likes

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/)
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:
``````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
``````
3 Likes

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.

5 Likes

FWIW, white grid lines are quite visible there:

``````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
``````

4 Likes

Tufte refers to background gridlines. As a rule, grid lines are not added to heatmaps, but PlotlyJS, for example, allows for gaps between bricks:

2 Likes

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.

Markers too:

Text is tricky though.

2 Likes

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.

1 Like