Add row and column labels to grid plot

Hi all,

I have a grid of contour plots, each with the same x and y dimensions. The grid is formed by varying two other dimensions. I would like to add labels r1, r2, …, rn to the rows and c1, c2, …, cn to the columns. corrplot from StatsPlots.jl is a similar example, but I could not figure out how the row and column labels are added. How can I do that?

Here is a simple example, replacing contours with scatter plots for simplicity.

using Plots
x = [(rand(10), rand(10)) for _ ∈ 1:9]
scatter(x, leg = false, xlabel = "x", ylabel = "y", layout = (3, 3))

You can use the subplot parameter. Here is an example (with 2x2 plots):

using Plots
x = [(rand(10), rand(10)) for _ ∈ 1:4]
p=plot( layout = (2, 2))
scatter!(p,x[1], leg = false, xlabel = "x1", ylabel = "y1", subplot=1)
scatter!(p,x[2], leg = false, xlabel = "x2", ylabel = "y2", subplot=2)
scatter!(p,x[3], leg = false, xlabel = "x3", ylabel = "y3", subplot=3)
scatter!(p,x[4], leg = false, xlabel = "x4", ylabel = "y4", subplot=4)

Result:

image

Thanks for your reply. Unfortunately, your recommendation does not achieve my goal. In my use case, there are 5 dimensions: the x,y and z axes of each contour plot are the same variables. The other two dimensions are the rows and columns of the grid. For example, the top left contour plot shows z as a function of x and y at R= r1 and C = c1. I realize that is a bit abstract. I can illustrate with a concrete example if that helps.

The best solution I have came up with so far is this:


using Plots
x = [(rand(10), rand(10)) for _ ∈ 1:9]
p = scatter(x, leg = false, xlabel = "x", ylabel = "y", layout = (3, 3))
[ylabel!(p[i,1], "r$i \n y") for i ∈ 1:3]
[title!(p[1,i], "c$i") for i ∈ 1:3]
p

Is there a way to rotate r1, r2 and r3 90 degrees clockwise and enlarge the font size to differentiate it from the y axes better?

I tried this, but the syntax is not quite right:

[ylabel!(p[i,1], "$(font("r $i", 12, rotation=90)) \n y") for i ∈ 1:3]

Any recommendations?

Sorry for misunderstanding.
Perhaps annotate! is the easiest way to go:

using Plots
x = [(rand(10), rand(10)) for _ ∈ 1:9]
p = scatter(x, leg = false, xlabel = "x", ylabel = "y", layout = (3, 3))
[ylabel!(p[i,1], "\n y") for i ∈ 1:3]
p

I have set ylabel only with a new line for making some room on the left.

Add a single row label to the first row:

annotate!(p[1,1], -0.35, 0.7, text("r1", 10))

It doesn’t work very well, as the coordinates depend on the values in the plot on position (1,1).

No problem. I could have been more clear in my initial post.

I do share your concern about annotate. Defining a rule for position might be challenging.

If I can figure out a way to improve the row label, it will be good.

Here is my best attempt:

using Plots
x = [(rand(10), rand(10)) for _ in 1:9]
l = @layout [
   a{0.1w,0.1h} b{0.3w,0.1h} c{0.3w,0.1h} d{0.3w,0.1h}
   e{0.1w,0.1h} f{0.3w,0.3h} g{0.3w,0.3h} h{0.3w,0.3h}
   i{0.1w,0.1h} j{0.3w,0.3h} k{0.3w,0.3h} l{0.3w,0.3h}
   m{0.1w,0.1h} n{0.3w,0.3h} o{0.3w,0.3h} p{0.3w,0.3h}
]
p = plot(
   layout = l, legend = false
)
headers=[1,2,3,4,5,9,13]
subplots=[6,7,8,10,11,12,14,15,16]
[ plot!(p[headers[i]],(0,0),xlimits = (-1,1),ylimits=(-1,1),grid = false, axis=([], false)) for i in 1:7 ];
[ scatter!(p[subplots[i]],x[i], leg = false, xlabel = "x", ylabel = "y") for i in 1:9 ];
p
annotate!(p[2], 0.0, 0.0, text("col1", 10, :center, :center))
annotate!(p[3], 0.0, 0.0, text("col2", 10, :center, :center))
annotate!(p[4], 0.0, 0.0, text("col3", 10, :center, :center))
annotate!(p[5], 0.0, 0.0, text("row1", 10, :center, :center))
annotate!(p[9], 0.0, 0.0, text("row2", 10, :center, :center))
annotate!(p[13], 0.0, 0.0, text("row3", 10, :center, :center))

Looks like:

1 Like

Thank you. This looks very good!

One thing that is unclear to me is how I might generalize this to an arbitrary grid size, especially the @layout part. Do you know if there is a way to generalize the macro?

No, I tried but failed.
But you can get the same result in a bit easier fashion without the macro:

using Plots
x = [(rand(10), rand(10)) for _ in 1:9]
p = plot(
  legend = false, layout = grid(4, 4, heights=[0.1 ,0.3, 0.3, 0.3], widths=[0.1 ,0.3, 0.3, 0.3])
)
headers=[1,2,3,4,5,9,13]
subplots=[6,7,8,10,11,12,14,15,16]
[ plot!(p[headers[i]],(0,0),xlimits = (-1,1),ylimits=(-1,1),grid = false, axis=([], false)) for i in 1:7 ];
[ scatter!(p[subplots[i]],x[i], leg = false, xlabel = "x", ylabel = "y") for i in 1:9 ];
p
annotate!(p[2], 0.0, 0.0, text("col1", 10, :center, :center))
annotate!(p[3], 0.0, 0.0, text("col2", 10, :center, :center))
annotate!(p[4], 0.0, 0.0, text("col3", 10, :center, :center))
annotate!(p[5], 0.0, 0.0, text("row1", 10, :center, :center))
annotate!(p[9], 0.0, 0.0, text("row2", 10, :center, :center))
annotate!(p[13], 0.0, 0.0, text("row3", 10, :center, :center))
1 Like

Thank you for the help. Here is what I have so far. Perhaps there are some improvments that could be made.

using Plots
n_rows = 5
n_cols = 5
x = [(rand(10), rand(10)) for _ in 1:n_rows, _ in 1:n_cols]

function compute_weights(n)
    w = [0.15 ,fill(.85, n)...]
    return w ./ sum(w)
end
p = plot(
  legend = false, layout = grid(n_rows+1, n_cols+1, 
    heights = compute_weights(n_rows+1), 
    widths = compute_weights(n_cols+1)),
    margin = .15Plots.cm,
    size = (800, 800)
)

[plot!(p[r,1],(0,0),xlimits = (-1,1),ylimits=(-1,1),grid = false, axis=([], false)) for r in 1:n_rows+1];
[plot!(p[1,c],(0,0),xlimits = (-1,1),ylimits=(-1,1),grid = false, axis=([], false)) for c in 1:n_cols+1];
[scatter!(p[r,c], x[r-1,c-1], leg=false, xlabel = "x", ylabel = "y") for r in 2:(n_rows+1), c in 2:(n_cols+1)]

[annotate!(p[i,1], 0.0, 0.0, text("row$i", 10, :center, :center)) for i in 2:n_rows+1]
[annotate!(p[1,i], 0.0, 0.0, text("col$i", 10, :center, :center)) for i in 2:n_cols+1]

p

For some values of n_rows and n_cols, I encounter the following error:

ERROR: The sum of heights must be 1!
Stacktrace:
 [1] error(s::String)
   @ Base ./error.jl:35
 [2] Plots.GridLayout(::Int64, ::Vararg{…}; parent::Plots.RootLayout, widths::Vector{…}, heights::Vector{…}, kw::@Kwargs{})
   @ Plots ~/.julia/packages/Plots/ju9dp/src/layouts.jl:221
 [3] grid(::Int64, ::Vararg{Int64}; kw::@Kwargs{heights::Vector{Float64}, widths::Vector{Float64}})
   @ Plots ~/.julia/packages/Plots/ju9dp/src/layouts.jl:209
 [4] top-level scope

which is due to numerical error, e.g.,

julia> sum(compute_weights(7))
1.0000000000000002

I suspect the following might be too strict:

if sum(heights) != 1
    error("The sum of heights must be 1!")
end

Do you agree, or is there a better way to handle the problem?

Yes, this error is a bit annoying. It could easily be calculated to a sum of 100% range in any case. But perhaps it’s clearer for the user if he has to think the proportions to be 100% in total. I don’t know which is best.

You could change your compute_weights function to:

function compute_weights(n)
    w = [0.15, fill(.85, n)...]
    weights = w ./ sum(w)
	[ 1.0-sum(weights[2:end]), weights[2:end]...]
end

(quick and dirty, no much brain used).

1 Like