Annotations and line widths in Plots.jl heatmaps

One of the nicest features of heatmaps in the Python’s Seaborn module is the ability to annotate heatmaps with the values in the cells, the axes labels, and the line style separating the cells as you can see in these examples: seaborn.heatmap — seaborn 0.12.2 documentation

Given the following heatmap in Plots.jl:

using Plots

A = rand(10,10)
heatmap(A)

Is there an option to annotate the cells? What about the line styles?

1 Like

You can add the elements manually - check: https://juliaplots.github.io/examples/pyplot/#annotations for the text annotations (use font to set the style), and you can overlay lines and use all the normal attributes to set the style https://juliaplots.github.io/attributes/ .

A nice trick is to use the userplot macro to create a recipe https://juliaplots.github.io/recipes/ - you’d need to creat two series (of type heatmap and path, using the series macro), and specify the annotations using the := syntax. You’d need Plots (not just RecipesBase) to define the recipe to have access to the font type.

EDIT: passing the annotations in a recipe will require some fiddling.

1 Like

An example speaks louder than a thousand words.

using Plots
@userplot RecipesAreEasy
@recipe function f(x::RecipesAreEasy; annotationargs = ())
    y = x.args[1]              #Get the input arguments, stored in x.args - in this case there's only one
    typeof(y) <: AbstractMatrix || error("Pass a Matrix as the arg to heatmap")

    grid := false                      # turn off the background grid
    
    @series begin                      # the main series, showing the heatmap
        seriestype := :heatmap
        y
    end

    rows, cols = size(y)

    #horizontal lines
    for i in 0:cols         # each line is added as its own series, for clearer code
        @series begin
            seriestype := :path
            primary := false          # to avoid showing the lines in a legend
            linecolor --> :white
            [i, i] .+ 0.5, [0, rows] .+ 0.5  # x and y values of lines
        end
    end

    for i in 0:rows
        @series begin
            seriestype := :path
            primary := false
            linecolor --> :white
            [0, cols] .+ 0.5, [i,i] .+ 0.5
        end
    end

    @series begin
        seriestype := :scatter
        markerstrokecolor := RGBA(0,0,0,0.)  # make the points transparent - setting marker (or seriestype) to :none doesn't currently work right
        seriescolor := RGBA(0,0,0,0.)        # do
        series_annotations := text.(1:(cols*rows), annotationargs...)
        primary := false
        repeat(1:cols, inner = rows), repeat(1:rows, outer = cols)
    end
end

Then

using Plots; pyplot() # currently broken in gr :-(
recipesareeasy(reshape(1:40, 8,5), annotationargs = (10, "Arial", :lightgrey)) #you can also use linecolor , linestyle, linewidth etc to modify the lines

8 Likes

Thanks @mkborregaard, this is really helpful! I appreciate the comments as well, they helped me a lot in understanding the machinery of plot recipes.

Should this recipe be part of PlotRecipes.jl? It may be useful to other users in the future, plus there is more customization that can be taken into account like the color of the annotations adapting to the heatmap (increase contrast), passing DataFrames, etc.

I think it’d be nicer to allow seriesannotations and linewidth / -style / -color to have this functionality inside Plots. It’s intuitive and would give some keywords that don’t apply to heatmap a sensible use. That’s a lot more complex to adjust but a better long-term solution. Nice chance for a new contributor to make a high-value PR :slight_smile:

1 Like

Thank for the content. In order to place the values of the matrix in the output figure I have modified this line from ```
series_annotations := text.(1:(cols*rows), annotationargs…)

to ```
series_annotations := text.(vec(round.(y,digits=3), annotationargs...)

Thanks again for sharing!!!

1 Like