A good colorscheme encodes the right information on data. Is inferno a good colorscheme?

There is a thread https://discourse.julialang.org/t/comparison-of-plotting-packages/99860 where users of different plotting/drawing libraries try to convince the readers about the superiority of their tool. No matter what’s your favourite library, it is important to know how to choose colors in data visualization.
Unlike the Python and R communities, here I didn’t notice any particular interest in using good colorschemes in plots, i.e. colorschemes that satisfy the actual requirements, and are not based on 40+ old color models.
Properly selected colors encode and convey accurately the information contained by the underlying data. A bad colorscheme distorts relationships between data values.

What is a perceptually uniform colorscheme (PUC) is discussed here https://discourse.julialang.org/t/complex-colormap-in-julia-plots/84212/8.
But not any PUC is a good one. Among the first four matplotlib PUCs (viridis, plasma, magma, inferno), inferno, the default Plots.jl colorscheme, is the worst one, and I’ll give all details below.
One year later after their creations the authors realized that inferno has drawbacks but couldn’t explain why. In 2019, when the first image of a blackhole has been published, a member of the EHT (Event Horizon Telescope) project developed a python module, ehtplot, that asseses and removes the drawbacks of colorschemes. On that ocasion it was obvious why inferno is not a good PUC.
Even if you know that a colorscheme is perceptually uniform i.e. it has a linear lightness, more tests should be performed to asses its quality.

Below is the ehtcmap a colormap created by o function in ehtplot that illustrates the linear lightness and the results of three tests, i.e. the three heatmaps on the second row. This is an ideal colormap.
Then the ColorScheme.leonardo and inferno:



The first test is the pyramid heatmap which translated from Python to Julia is defined by:
function pyramid(;N=256)
    s = range(-1, 1 ,length=N)
    x =ones(size(s)) .*s'  
    z = 1 .- [maximum([u, v]) for (u,v) in zip(abs.(x), abs.(x'))]
    return  reshape(z, N, N)
end 

The pyramid reveals band colors and sharp transitions created by non-linear lightness and/or non-smooth chroma, as a function of normalized values.

Example of a bad colorscheme revealed by such a heatmap is ColorSchemes.leonardo, and all those with artist names:

n=256
heatmap(0:n-1, 0:n-1, pyramid(;), c=:leonardo)
plot!(size=(350, 300))

The second test for a perceptually uniform colorscheme is the heatmap created by z-order:

function bit_interleave32(m, n)
    ndigits(m, base=2) <= 16 && ndigits(n, base=2) <= 16 ||
        error("This function works with max 16 bits integers")
        
    interleaved = 0x00000000
    bit = 0x00000001
    for k in collect(15:-1: 0)
        mask = bit << k
        m_bit = m & mask
        interleaved = interleaved | m_bit << (k+1)
        n_bit = n & mask
        interleaved = interleaved | n_bit << k
    end    
    interleaved
end

function z_order(;N=256)
    N & (N-1) == 0 || error("N should be a power of 2: N=2^p;here N=$N")
    I = [i for i in 0:N-1, j in 0:N-1]
    z= bit_interleave32.(I, I') 
end

using Plots
n=512
Z= z_order(;N=n)
heatmap(0:n-1, 0:n-1, Z, c=:inferno, yflip=true)
plot!(size=(450, 400))

With a good perceptually uniform colorscheme such a heatmap display 16 distinct, and 48 almost distinct squares, but with inferno, the upper left square and one of its neighbohrs are hardly distinguishable. As a conclusion with inferno, a longer than usual interval, [0, a]⊂[0,1], of normalized data, is mapped to indistinguishable colors, i.e. it doesn’t encode the right information on data. This occurs because the lightness of this colorscheme has a maximal range [0,100].
A good colorscheme should have this range [20, 98] or [20, 100].

The third test is the XOR row-by-column that reveals the same drawback as Z-order, and how the lighter colors interleave the darker ones.

J = [i for i in 0:n-1, j in 0:n-1]
heatmap(0:n-1, 0:n-1, xor.(J', J), c=:inferno, yflip=true)
plot!(size=(400, 350))
3 Likes