Custom divergent colormap with unequal sides

I am working with Makie.jl and I wish to create a custom colormap. Taking any divergent colormap provided by Makie, such as :balance for example, I want to make a colormap that has the same colors as :balance or :delta, but the left side spans the range of values 0-1, and the right side spans the range of values 1-4 (instead of what the symmetric 1-2 would dictate).

Is that possible? If yes, how?

1 Like

Canā€™t you use a standard diverging colormap for the range [-3,3], and then apply it to values v - 1 \in [-1, 3] (so that the left side is truncated)?

I am sorry, I did not understand the advice. Iā€™ve set up a MWE which I should have done so already in my first post:

using CairoMakie

x = 1:10
y = 1:10
c = rand(10,10)

cmap = :delta
fig, ax, hm = heatmap(x, y, c; colormap = cmap)
cb = Colorbar(fig[1,2], hm)
fig

what I want to achieve is that the ā€œblueā€ part of the color map covers the range (e.g.,) 0-0.25, and the ā€œgreenā€ part covers the range 0.25-1.0, instead of the currently symmetric case of covering each 50-50. I want to be able to utlize all colors in the color map, not cut off e.g., the bottom deep blue color.

Maybe just pass your data through the map:

distort(x) = x < 0.25 ? -3x : x - 0.25

which maps it to [-0.75, 0.75], and then apply a standard diverging colormap centered at zero.

Of course, this distorts the data, which is something you have to be cautious about for visualization. Effectively, your colormap is no longer a ā€œperceptually uniformā€ representation of your data, which is something that the colormap designers have worked hard to avoid.

1 Like

Yeah, I guess thatā€™s the only way. I wanted to avoid transforming my own data, because this requires me to re-label all ticks in the colorbar. But itā€™s not a big deal so Iā€™ll go ahead with this way of remapping data.

Yes, I understand. Thankfully in my particular use case it is not a big problem!

You can distort the colormap rather than the data. The following code is a bit clumsy, but shows the general idea.

using GLMakie
using ColorSchemes


function get_color_val(data_val, data_min, data_max, data_crit, color_val_crit)

    is_low = data_val ā‰¤ data_crit
    prop_val = is_low ? (data_val-data_min)/(data_crit - data_min) : (data_val-data_crit)/(data_max-data_crit)
    color_val = is_low ? prop_val*color_val_crit : color_val_crit + prop_val*(1-color_val_crit)

end 

X = rand(20,20)
data_min, data_max = 0.0, 1.0
data_crit = 0.25

cmap = [ get(ColorSchemes.RdBu, get_color_val(x, data_min, data_max, data_crit, 0.5)) for x āˆˆ range(0,1;length = 512)]


fig = Figure()
ax = Axis(fig[1,1])
hm = heatmap!(ax, X; colormap = cmap)
Colorbar(fig[1,2], hm)
fig

crit_025
Note that the white midpoint of the colorscheme has been reset to 0.25

1 Like

Makie.jl also has some tools to make divergent colormaps, for example Makie.diverging_palette(0, 230; mid=0.4)
image
Here, 0 and 230 are hues in a color wheel from 0 to 360 and mid is just a fraction to bias the point where the colors diverge.

1 Like

Iā€™ve also seen plots where people use an uneven scale on a symmetrical colormap. So you donā€™t distort the map but the scale. Hereā€™s an example I cooked up a while ago, adjusted to your numbers:

data = rand(10, 10) .* 4

f = Figure()
ax = Axis(f[1, 1])

upper = 4
lower = 0
midpoint = 1
ratio = (upper - midpoint) / (midpoint - lower)

sc = ReversibleScale(
    x -> x > midpoint ? (x - midpoint) / ratio + midpoint : x,
    x -> x > midpoint ? (x - midpoint) * ratio + midpoint : x
)

hm = heatmap!(ax, data, colorscale = sc, colorrange = (lower, upper), colormap = :delta)
Colorbar(f[1, 2], hm, ticks = lower:upper)
f

Hereā€™s a variant with the upper part going to 10, to show the effect even more:

upper = 10
lower = 0
midpoint = 1

data = rand(10, 10) .* (upper - lower) .+ lower

f = Figure()
ax = Axis(f[1, 1])


ratio = (upper - midpoint) / (midpoint - lower)

sc = ReversibleScale(
    x -> x > midpoint ? (x - midpoint) / ratio + midpoint : x,
    x -> x > midpoint ? (x - midpoint) * ratio + midpoint : x
)

hm = heatmap!(ax, data, colorscale = sc, colorrange = (lower, upper), colormap = :RdBu)
Colorbar(f[1, 2], hm, ticks = lower:upper)
f

Might be confusing to some readers though.

Edit: hmm seems like the rendering of the colors in the heatmap is a bit off though with this

Edit2: I just had forgot to scale the input data with the larger limits, I think

2 Likes

Thanks @jules , the differently scaled colorscale is exactly what I was looking for!

Iā€™m glad you got a good solution! I started to write an answer, but was interrupted, so this is now superfluous to requirementsā€¦ :slight_smile: But for the record, my idea was to concatenate colorschemes:

cs = ColorSchemes.delta
cs1 = ColorScheme(get(cs, range(0, 0.5, length=50))) * 
      ColorScheme(get(cs, range(0.5, 1.0, length=200)))

I was going to explore some of the tools in ColorSchemeTools.jl such as weighted colorschemes to see if they helped, but no need to, now! :joy:

2 Likes

This solution always need us to specified the colorrange in advance, is there a way to update this colorrange to the values that might be in our current view? think of zooming in, where we will have new bounds, because we will be looking at a subset of all values.