One colorbar for multiple axes

I have the following single figure with multiple heatmaps:

The limits of the colorbar are manually set to the minimum and maximum of the values plotted. This is bound to give an incorrect mapping if one of the heatmaps only “uses” e.g. half of the range of the limits, as the heatmap uses the full color range on whatever is plotted.

How do I make a colorbar that represents all heatmaps?

You have to set colorrange = (min, max) for each heatmap

1 Like

Precision: limits have to be set for both the heatmaps and the colorbar.
Here is a more detailed example:

using CairoMakie
# define the maps
n_rows = 2
n_cols = 5
maps = [rand(5,5) .* (i_row * 10 + i_col) for i_row in 1:n_rows, i_col in 1:n_cols]
# get global extrema
extremas = map(extrema, maps)
global_min = minimum(t->first(t), extremas)
global_max = maximum(t->last(t), extremas)
# these limits have to be shared by the maps and the colorbar
clims = (global_min, global_max)

let
	fig = Figure()
	for i_row in 1:n_rows, i_col in 1:n_cols
		ax =  Axis(fig[i_row, i_col])
		heatmap!(ax, maps[i_row, i_col]; colorrange=clims)
	end
	cb = Colorbar(fig[:, n_cols + 1]; limits=clims)
	fig
end

It would be nice to just pass an iterable of heatmaps to colorbar to automate this process ?

2 Likes

That’s actually a good idea, equalizing the color ranges when passing a couple plot objects to the colorbar.

3 Likes

Huge +1 on that.

Would it be possible to pass a figure, and have the colorbar fit all plot objects on all axes of the figure? This would alleviate the need to define variables for each plot-objects.

pass a figure, and have the colorbar fit all plot objects on all axes of the figure

That would be nice if all plots are to share the same colorbar indeed.

But maybe it could be possible to do the reverse:
define the cb=Colorbar(...) first
and pass a colorbar=cb argument to all heatmaps that should share this colorbar ?
That would suppress the need to define a reference for each heatmap,
while allowing to have separate groups of heatmaps with their own colorbar if needed.

Slightly related, this issue discusses adding linkcolors!, analogous to linkaxes!, to link color ranges across plots. (Mentioning it here because I imagine something like that would be needed to implement the “shared colorbar for many plots” functionality.)

1 Like

It turns out that you can pass any of the heatmaps (since they have the same colorranges and colormaps, if you have done anything right) to the Colorbar command. I found this simpler than adding a kwarg to the call to Colorbar with explicit limits.

This is now also documented, if Colorbar example for heatmap by KronosTheLate · Pull Request #1736 · JuliaPlots/Makie.jl (github.com) gets merged.

A pretty cool convenience would be a sync_colorranges! function, which would take a variable number of plotobject’s, check that they have colors (so heatmap and contour, right?), and set the colorrange of them all to the extrema of all z-values in all the plots.

sync_colorranges! could perhaps also take a figure, and automatically call itself on the axes within the figure.

If implemented, sync_colorranges! would make explicit limit-setting (no fun) redundant, and replace it with a simple-to-understand and easy-to-remember function.

Finally (and possibly too far down the rabbit-hole of convenience), one could implement a method Colorbar(fig::Figure, kwargs...) that would call sync_colorranges! on the figure, and then add a colorbar. I am thinking that kwargs could specify vertical (default) or horizontal, and location (:right (default), :left, :above, :below).

I took a stab at it. Something like

"""
    sync_colorranges!(plotobjects::MakieCore.ScenePlot...)

Set the colorrange of all `plotobjects` to the same value,
namely the extrema of all z-values of all plotobjects in `plotobjects`.
"""
function sync_colorranges!(plotobjects::MakieCore.ScenePlot...)
    for plt in plotobjects 
        haskey(plt.attributes.attributes, :colorrange) || error("This function syncronizes the color range of the given plotobjects. One of the plotobjects passed has no color range.")
    end
    possible_extremas = [extrema(to_value(plt[3])) for plt in plotobjects]
    global_extremas = extrema(vcat(collect.(possible_extremas)...))
    for plt in plotobjects
        plt.attributes.colorrange[] = global_extremas
    end
    return nothing
end

This should be the right approach (checking the existence of colorrange, and changing it) if https://github.com/JuliaPlots/Makie.jl/issues/1737 gets a satisfying solution.