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:
heatmap-gaps

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. :slight_smile:

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