# How to implement Value Suppressing Colormaps in Makie

**URL:** <https://discourse.julialang.org/t/how-to-implement-value-suppressing-colormaps-in-makie/129140>\
**Category:** Visualization\
**Tags:** makie, colorbar, colormaps\
**Created:** [May 19, 2025, 10:37am UTC](https://discourse.julialang.org/t/how-to-implement-value-suppressing-colormaps-in-makie/129140 "2025-05-19T10:37:31Z")\
**Posts on this page:** 12\
**Page:** 1

<div class="post-metadata">

**Author:** ![vladdez](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/vladdez/32/45806_2.png) [@vladdez](https://discourse.julialang.org/u/vladdez)\
**Post date:** [May 19, 2025, 10:37am UTC](https://discourse.julialang.org/t/how-to-implement-value-suppressing-colormaps-in-makie/129140/1 "2025-05-19T10:37:31Z")

</div>

I would like to implement Value Suppressing Colormap described in this paper [https://dl.acm.org/doi/abs/10.1145/3173574.3174216](https://dl.acm.org/doi/abs/10.1145/3173574.3174216)

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

1. triangular colorbar
2. bivariate colormap, where I can manipulate color hue be changing saturation and lightness

At least how I understand it 🙂  
Any ideas? Is it even possible in Makie?

---

<div class="post-metadata">

**Author:** ![juliohm](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/juliohm/32/215266_2.png) [@juliohm](https://discourse.julialang.org/u/juliohm)\
**Post date:** [May 19, 2025, 10:47am UTC](https://discourse.julialang.org/t/how-to-implement-value-suppressing-colormaps-in-makie/129140/2 "2025-05-19T10:47:06Z")

</div>

Very interesting. Thanks for sharing the triangular map.

We implemented the bivariate map in our GeoStats.viewer with Makie.jl, and it would be awesome to replace it with the triangular one.

If you can work on a package that takes a vector of Distributions.jl objects and uses this triangular map to show location and spread statistics, that would be neat.

---

<div class="post-metadata">

**Author:** ![juliohm](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/juliohm/32/215266_2.png) [@juliohm](https://discourse.julialang.org/u/juliohm)\
**Post date:** [May 19, 2025, 12:13pm UTC](https://discourse.julialang.org/t/how-to-implement-value-suppressing-colormaps-in-makie/129140/3 "2025-05-19T12:13:59Z")

</div>

To be more precise, we managed to abstract this concept in the Colorfy.jl package, which takes a vector of objects and returns a vector of colors for plotting. The exact code that handles Distributions.jl objects is stored in an extension:

> <https://github.com/JuliaGraphics/Colorfy.jl/blob/main/ext/ColorfyDistributionsExt.jl>

---

<div class="post-metadata">

**Author:** ![vladdez](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/vladdez/32/45806_2.png) [@vladdez](https://discourse.julialang.org/u/vladdez)\
**Post date:** [May 19, 2025, 4:50pm UTC](https://discourse.julialang.org/t/how-to-implement-value-suppressing-colormaps-in-makie/129140/4 "2025-05-19T16:50:59Z")

</div>

@juliohm great tool! Thank you!  
But is it possible to send there a vector of different transparency values for each heatmap value to represent uncertainty variations?

---

<div class="post-metadata">

**Author:** ![juliohm](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/juliohm/32/215266_2.png) [@juliohm](https://discourse.julialang.org/u/juliohm)\
**Post date:** [May 19, 2025, 5:41pm UTC](https://discourse.julialang.org/t/how-to-implement-value-suppressing-colormaps-in-makie/129140/5 "2025-05-19T17:41:55Z")

</div>

You can pass a vector of `alphas` to the `colorfy` function and it is demonstrated in the README. If you can submit a PR to improve our Distributions.jl extension with the triangular map above, we can happily review and merge. That way users could simply `colorfy(distributions)` to get the final colors for plotting with Makie.jl

---

<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:** [May 19, 2025, 6:30pm UTC](https://discourse.julialang.org/t/how-to-implement-value-suppressing-colormaps-in-makie/129140/6 "2025-05-19T18:30:29Z")

</div>

I shared this recipe some time ago, maybe this capability should be added to Makie.jl directly?..  
Basically, `image()` but `alpha` can be an array of values:

 ![image](https://global.discourse-cdn.com/julialang/original/3X/0/2/02b50bf2f8ba822ea46ae7a3c33472bf30b37bff.png)

```julia
@recipe ImageAlpha (
		x::Makie.EndPoints,
		y::Makie.EndPoints,
		image::AbstractMatrix{<:Number}) begin
	Makie.MakieCore.documented_attributes(Makie.Image)...
	alpha = nothing
	alpharange = 0..1
end

function Makie.plot!(p::ImageAlpha)
	alpha = p.alpha[]
	p.alpha = 1
	Makie.color_and_colormap!(p, p.image)
	imcolors = @lift Makie.to_color($(p.calculated_colors))
	alphas_interval = @lift @something(extrema($(p.alpharange)), extrema(alpha))
	alpha = @lift clamp.((alpha .- $alphas_interval[1]) ./ ($alphas_interval[2] - $alphas_interval[1]), 0, 1)
	imcolors_a = @lift Makie.coloralpha.($imcolors, $alpha)
	image!(p, Makie.shared_attributes(p, Image), imcolors_a)
	return p
end

Makie.convert_arguments(::Type{<:ImageAlpha}, args...) = Makie.convert_arguments(ImageLike(), args...)

```

---

<div class="post-metadata">

**Author:** ![juliohm](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/juliohm/32/215266_2.png) [@juliohm](https://discourse.julialang.org/u/juliohm)\
**Post date:** [May 19, 2025, 6:56pm UTC](https://discourse.julialang.org/t/how-to-implement-value-suppressing-colormaps-in-makie/129140/7 "2025-05-19T18:56:59Z")

</div>

> [@aplavin](#):
>
> maybe this capability should be added to [Makie.jl](https://juliaregistries.github.io/General/packages/redirect_to_repo/Makie) directly?

Colorfy.jl is a lightweight dependency that handles missing values, unitful values, etc. through package extensions. We created it to facilitate the definition of colorful representations for any Julia type. The API is quite stable at this point. Makie.jl could leverage this API to display Julia objects as colors in various plots.

---

<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:** [May 19, 2025, 7:09pm UTC](https://discourse.julialang.org/t/how-to-implement-value-suppressing-colormaps-in-makie/129140/8 "2025-05-19T19:09:43Z")

</div>

There are _a lot_ of reasonable useful ways to turn a Julia object (even just a Normal distribution) to a color + alpha, meaning that there isn’t any “fundamentally correct” way to do that.  
That’s why it would make sense to have a simpler straightforward API in a plotting package itself. Plotting “image with alpha” is well-defined, there’s just one way to do that.

---

<div class="post-metadata">

**Author:** ![juliohm](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/juliohm/32/215266_2.png) [@juliohm](https://discourse.julialang.org/u/juliohm)\
**Post date:** [May 19, 2025, 7:48pm UTC](https://discourse.julialang.org/t/how-to-implement-value-suppressing-colormaps-in-makie/129140/9 "2025-05-19T19:48:55Z")

</div>

> [@aplavin](#):
>
> There are _a lot_ of reasonable useful ways to turn a Julia object (even just a Normal distribution) to a color + alpha, meaning that there isn’t any “fundamentally correct” way to do that.

Do we need these other useful ways in practice though? Or just “the most useful” way that is the result of research like the one linked by the OP?

Your request to add support in Makie.jl for “image + alpha” is an (important) orthogonal request. I believe the OP and I are seeking the most useful colorful representation for a vector of objects, which happens to be a vector of Distributions.jl (or uncertainties).

---

<div class="post-metadata">

**Author:** ![vladdez](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/vladdez/32/45806_2.png) [@vladdez](https://discourse.julialang.org/u/vladdez)\
**Post date:** [June 24, 2025, 8:07pm UTC](https://discourse.julialang.org/t/how-to-implement-value-suppressing-colormaps-in-makie/129140/10 "2025-06-24T20:07:08Z")

</div>

Thank you very much! I tried this solution with

```julia
using TopoPlots
TopoPlots.eeg_topoplot(
    rand(64);
    positions = positions,
    label_text = true,
    plotfnc! = (plot, x, y, image; kwargs...) -> begin
        imgval = replace(image[], NaN => 0.0) # unwrap the Observable
        imagealpha!(
            plot,
            Makie.EndPoints(first(x[]), last(x[])),
            Makie.EndPoints(first(y[]), last(y[])),
            imgval;
            alpha = imgval,
           # alpharange = 0.0 .. maximum(imgval),
            colormap = :viridis,
            interpolate = false,
        )
    end,
)

```

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

It kinda works, but as you my notice, the axis here is just a tiny spot on the left bottom angle. There is a mismatch of scales. Instead of 600x600 it should be just 1x1. How can I control the scale of the interpolated image? Do you have any ideas?

upd  
even with simple example of

```julia
imagealpha(
    Makie.EndPoints((0.0, 1.0)),
    Makie.EndPoints((0.0, 1.0)),
    rand(60, 60);
    alpha =rand(60, 60,
    colormap = :viridis,
    interpolate = false,
)

```

.. limits are equal to the size of data and couldn’t be changed. How can I change them?

---

<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:** [June 24, 2025, 10:35pm UTC](https://discourse.julialang.org/t/how-to-implement-value-suppressing-colormaps-in-makie/129140/11 "2025-06-24T22:35:48Z")

</div>

Yes I guess it should explicitly pass all arguments to `image()`, which it doesn’t do…  
Feel free to suggest a fix 🙂 Alternatively, just use an array type that carries both data and arbitrary axis ranges in one object:

```julia
using AxisKeysExtra

A = KeyedArray(rand(60, 60), x=range(0..1, length=60), y=range(0..1, length=60))
imagealpha(
	A;
	alpha=A .^ 1.5,
    colormap = :viridis,
    interpolate = false,
)

```

 ![image](https://global.discourse-cdn.com/julialang/original/3X/3/c/3c42d33f63b8f33e845fe132bb9a2ffdf2e98f69.png)  
I personally didn’t even notice this limitation of `imagealpha`, simply because I tend to use `KeyedArray`s whenever makes sense.

---

<div class="post-metadata">

**Author:** ![tiemvanderdeure](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/tiemvanderdeure/32/211374_2.png) [@tiemvanderdeure](https://discourse.julialang.org/u/tiemvanderdeure)\
**Post date:** [August 7, 2025, 9:15am UTC](https://discourse.julialang.org/t/how-to-implement-value-suppressing-colormaps-in-makie/129140/12 "2025-08-07T09:15:50Z")

</div>

I have also been playing with this and found that just adjusting `alpha` really doesn’t give the same effect as desaturating colors and looked more closely at the different implementations of this to make my own.

I also used a `PolarAxis` from `GeoMakie` to draw the legend in the shape of an arc (I quite like it)

I think this is pretty close, and there are some parameters you can tweak to get the effect you like.

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

> **Code for the plot above**
>
> ```julia-auto
> using GADM, DataFrames, GLMakie, GeoMakie
> import Colors: HSL, LCHuv
> 
> # Helper functions
> function desaturate(col::T, amount)::T where T
> col_lch = LCHuv(col)
> return LCHuv(col_lch.l, col_lch.c * (1-amount), col_lch.h)
> end
> 
> function lighten(col::T, amount)::T where T
> col_hsl = HSL(col)
> return HSL(col_hsl.h, col_hsl.s, 1 - (1 - col_hsl.l) * (1-amount))
> end
> 
> # Generate a color matrix
> function vsup_colormatrix(;
> cmap, n_uncertainty, 
> max_desat, # 0-1, 1 = most desaturated is grey, 0 = no desaturation
> pow_desat, # higher values to desature more slowly
> max_light, # 0-1 1 = most light is white, 0 = no light
> pow_light # higher values to lighten more slowly
> )
> 
> n_levels = [2^(i-1) for i in 1:n_uncertainty]
> max_levels = n_levels[end]
> 
> rel_col = @. (ceil(Int, (1:max_levels)'/(max_levels/n_levels)) - 0.5) / n_levels
> col_shift = @. 1 - ((1:n_uncertainty) - 1)/(n_uncertainty - 1)
> 
> col = getindex.(Ref(cmap), rel_col)
> col_desat = desaturate.(
> col, 
> max_desat.*col_shift.^pow_desat
> )
> return lighten.(
> col_desat,
> max_light.*col_shift.^pow_light
> )
> end
> 
> function val_u_to_color(
> v, u, colormatrix;
> colorrange = extrema(v), uncertaintyrange = extrema(u),
> v_edges = range(colorrange...; length = size(colormatrix, 2)+1),
> u_edges = range(uncertaintyrange...; length = size(colormatrix, 1)+1)
> 
> )
> n_uncertainty, n_values = size(colormatrix)
> # Find which bin each value is in
> v_bins = clamp.(searchsortedfirst.(Ref(v_edges), v).-1, 1, n_values)
> 
> u_bins = clamp.(searchsortedfirst.(Ref(u_edges), u).-1, 1, n_uncertainty)
> 
> # Get colors
> getindex.(Ref(colormatrix), u_bins, v_bins)
> end
> 
> # Polygons to plot
> dk_munic = DataFrame(GADM.get("DNK"; depth = 2))
> 
> # Mockups values for observations and uncertainty
> vals = rand(nrow(dk_munic))
> u = rand(nrow(dk_munic))
> 
> # Generate color matrix
> vsup_cmap = vsup_colormatrix(; 
> cmap = cgrad(:viridis), n_uncertainty = 4, 
> max_desat = 0.8, pow_desat = 1.0, max_light = 0.7, pow_light = 1
> )
> 
> # Get colors
> col_final = val_u_to_color(vals, u, vsup_cmap; colorrange = (0,1), uncertaintyrange = (0,1))
> 
> # Draw the figure
> f, ax, p = poly(dk_munic.geom; color = col_final, strokewidth = 0.5)
> hidespines!(ax)
> hidedecorations!(ax)
> 
> # Legend axis
> ax_legend = PolarAxis(f[1,1];
> thetalimits = (1.5pi/4, 2.5pi/4),
> rlimits = (0,10),
> rticks = (collect(range(0,10; length = 5)), string.(range(0,1; length = 5))),
> thetaticks = ([1.5pi/4, 2pi/4, 2.5pi/4], ["100%", "50%", "0%"]),
> clip = false,
> height= Relative(0.7), width = Relative(0.5), tellwidth = false,tellheight = false,
> halign = 1.0, valign = 1.0 # found these values to be right through trial and error
> )
> # Draw the legend - the meshimage trick is needed because surface plot is bugged
> # See https://github.com/MakieOrg/Makie.jl/issues/5235
> mi = meshimage!(
> ax_legend,
> ax_legend.thetalimits[], 0..10, rotl90(vsup_cmap), shading = NoShading, 
> )
> mi.plots[1].interpolate[] = false
> GLMakie.closeall()
> 
> f
> 
> ```
