Plots.jl: How can I get discrete colors in the colorbar of a heatmap?

Simple example:

julia> r = rand(0:10,10,10)
10×10 Array{Int64,2}:
  0  4  10   1  10   1   6   3   6  3
  9  7   9   3   8   4   3   1   6  8
  8  7   6   2   8   7   8   8   2  9
  3  7   2   0   0   7   8   0   9  5
  2  6   0   3   4   9   1  10   6  2
  2  7   2   2   3   6   1   1  10  3
 10  9   4   2   0   3  10   6   0  9
  0  0   7   0  10   5   6   6   9  3
  6  3   1  10   4  10   6   7   0  4
  5  5   3   2   0   2   5   3   8  7

julia> heatmap(r)

newplot(36)

I want the colorbar to use the same 11 discrete colors as the heatmap. I also tried this but got the same result:

heatmap(r, c=[cgrad()[x] for x in 0:0.1:1])

Any ideas?

3 Likes

No, this is one of the most long-standing requests. https://github.com/JuliaPlots/PlotUtils.jl/issues/3

2 Likes

Issue number 3, you’re not kidding. :slight_smile: Oh well, no problem. I just need one figure so I’ll do the legend manually. Thanks for the quick reply.

Bump to ask the same question for other plotting packages such as Makie.jl or VegaLite.jl. After some quick tests I got the same continuous colorbar as above using these alternatives with no obvious options to change the behavior in the docs. Are discrete colormaps possible in other packages or does everything fall back on PlotUtils.jl (which doesn’t support this yet)?

I added this feature to GR.jl. This could be transferred to Plots/gr in a similar way.

using GR
r = rand(0:10,10,10)
heatmap(r, levels=10)

6 Likes

Right now, this does only affect the colorbar, not the heatmap.

It can be done with GMT.jl

using GMT
r = rand(0:10,10,10);
C=makecpt(range=(0,10,1));
imshow(r, cmap=C, colorbar=true)

and one gets the fig bellow. Note that the fact that the border pixels are cut in half is not wrong. The issue is that there are 2 ways to referencing grids (this is a grid plot since data is not uint8). They are called grid and pixel registration in GMT vocabulary or AREA_OR_POINT=Area and AREA_OR_POINT=Point in GeoTiff. Some more info at

But we can get a non-clipped version with

G = mat2grid(r, 1);       # Create a pixel registered grid
imshow(G, cmap=C, colorbar=true)

5 Likes

Very nice, and thanks very much for the lightspeed development! Two small problems though: in this particular example there were 11 levels, not 10. When I run heatmap(r, levels=11), the colorbar labels are slightly misaligned:

qq2
For discrete data the position of the label should probably be at the center of each color patch instead.

Second problem: when I ran savefig() to make this figure, the figure changed to a continuous colorbar and the x & y axes changed a bit too. Had to take a screenshot to post this.

Also, thanks for reminding me how incredibly fast vanilla GR is in terms of time to first plot!

1 Like

Got this error when I tried to run the GMT example:

julia> imshow(G, cmap=C, colorbar=true)
ERROR: Expected a CPT structure for input
Stacktrace:
 [1] error(::String) at .\error.jl:33
 [2] palette_init(::Ptr{Nothing}, ::Bool, ::GMT.GMTgrid, ::UInt32) at C:\Users\niclas\.julia\packages\GMT\Ba3R6\src\gmt_main.jl:1166
 [3] GMTJL_Set_Object(::Ptr{Nothing}, ::GMT.GMT_RESOURCE, ::GMT.GMTgrid) at C:\Users\niclas\.julia\packages\GMT\Ba3R6\src\gmt_main.jl:795
 [4] gmt(::String, ::GMT.GMTgrid, ::Vararg{Any,N} where N) at C:\Users\niclas\.julia\packages\GMT\Ba3R6\src\gmt_main.jl:232
 [5] finish_PS_module(::Dict{Symbol,Any}, ::Array{String,1}, ::String, ::String, ::String, ::String, ::Bool, ::Bool, ::Bool, ::GMT.GMTgrid, ::Vararg{Any,N} where N) at C:\Users\niclas\.julia\packages\GMT\Ba3R6\src\common_options.jl:2186
 [6] #grdimage#95(::Bool, ::Base.Iterators.Pairs{Symbol,Any,Tuple{Symbol,Symbol,Symbol},NamedTuple{(:show, :cmap, :colorbar),Tuple{Bool,GMT.GMTcpt,Bool}}}, ::typeof(grdimage), ::String, ::GMT.GMTgrid, ::Nothing, ::Nothing) at C:\Users\niclas\.julia\packages\GMT\Ba3R6\src\grdimage.jl:88
 [7] #grdimage at .\none:0 [inlined] (repeats 2 times)
 [8] #imshow#125 at C:\Users\niclas\.julia\packages\GMT\Ba3R6\src\imshow.jl:59 [inlined]
 [9] (::getfield(GMT, Symbol("#kw##imshow")))(::NamedTuple{(:cmap, :colorbar),Tuple{GMT.GMTcpt,Bool}}, ::typeof(imshow), ::GMT.GMTgrid) at .\none:0
 [10] top-level scope at REPL[5]:1

The problem could be on my end since I just used my existing GMT installation which is maybe 6 months to a year old, but I don’t have any more time to reinstall or diagnose this problem today. Just thought I’d let you know.

Since you are on Windows, updating GMT is completely painless (just use the installer in the GMT Github page). But you should not need to do it. It was my fault that didn’t repeat the C=makecpt(range=(0,10,1)); step or the seconds case. That’s why it’s complaining about a missing CPT struct

using GMT
r = rand(0:10,10,10);
C=makecpt(range=(0,10,1));
G = mat2grid(r, 1);       # Create a pixel registered grid
imshow(G, cmap=C, colorbar=true)

Here is the VegaLite.jl version of this:

using DataFrames, VegaLite

df = DataFrame(x=repeat(1:10, 10), y=repeat(1:10, inner=10), z=rand(0:10, 100));

df |> @vlplot(:rect, x="x:n", y="y:n", color="z:n")

And we get:
example2

The trick here is to configure the scale for the color encoding as “nominal” via the :n shortcut.

Here is an alternative that looks maybe nicer? The weird thing is that this is not documented in the original vega-lite docs, and I just stumbled across this by accident, so not sure how “official” this actually is:

df |> @vlplot(:rect, x="x:n", y="y:n", color={"z:n", legend={type=:discrete}})

And we get:
example1

4 Likes

The colorbar cells are now centered. For the savefig() problem, I don’t have an explanation right now … Will check it later this week …

1 Like

The VegaLite example seems like it’s something else - a nominal color scale rather than an ordinal one as was requested, no?

It’d be very easy to add this in PlotUtils AFAICS. One way would be to add a levels field to ColorGradient, then use that to round the number value being passed to getindex. So you could do heatmap(A, color = cgrad(:Spectral, levels = 9))

1 Like

Ah, yes, you are right! Here is the version with an ordinal scale, and some other small adjustments (no legend title and color scheme that matches the original post):

df |> @vlplot(
  :rect,
  x="x:n",
  y="y:n",
  color={
    "z:o",
    scale={scheme="inferno"},
    legend={title=nothing, type=:discrete}
  }
)

We then get:
example4

So now I use the :o shortcut to declare the scale for the color as ordinal.

I’m also configuring the use of the inferno color scheme for the scale (you can see all the pre-defined schemes here). The options you have for configuring the scale are very broad in vega-lite, a full description is here.

And finally I’m removing the legend title, and then configure the type of the legend as :discrete (although I can’t find that option documented anywhere, it seems to work…).

5 Likes

With Plots.jl for me it works something like this:

using Plots
r = rand(0:10,10,10)
heatmap(r, color=palette(:hot, 11))

hot_palette

4 Likes