Uneven grid layout in Makie.jl

Hello everyone,

I try to great a two row figure in Makie, where the first row contains 3 Axis elements and the second row only two and the plot legend.
Sadly, I cannot figure out the logic behind Makie’s layout algorithm.

First consider the MWE:

function makeFigure()
	fig = Figure(resolution = (300, 200), fontsize = 12, figure_padding = 2)
	
	Axis(fig[1,1:2])
	Axis(fig[1,3:4])
	Axis(fig[1,5:6])
	
	Axis(fig[2,2:3])
	Axis(fig[2,4:5])
	Box( fig[2,6])
	
	colgap!(fig.layout, Relative(0.02))
	fig
end

Which results in the figure
WeirdMakie
For some reasons Makie thinks it is cool that the upper right Axis has a different width than the other two in the top row. This happens also if I do not include the Box (which should be the legend later) or use for n ∈ 1:6 colsize!(fig.layout, n, Relative(1/6)) end

My second approach went along the line of:

function makeFigure()
	fig = Figure(resolution = (300, 200), fontsize = 12, figure_padding = 2)
	
	Axis(fig[1,1:2])
	Axis(fig[1,3:4])
	Axis(fig[1,5:6])
	
	Axis(fig[2,:][1,2:3])
	Axis(fig[2,:][1,4:5])
	Box( fig[2,:][1,6])

	colgap!(fig.layout, Relative(0.02))
	fig
end

i.e., using a subgrip for the lower row.

This results in
WeirdMakie2
Problem: There seems to be no way to set colgap for subgrids and also the width of the lower axis is uncorrelated to the ones from the top row.

How can I convince Makie to simply put all Axis into the same width?

This is actually more tricky than you might think at first. The reason why your top-right axis is smaller is that the other two both need to include the width of the y-axis decorations of the lower axes. The Box doesn’t have that. The space in the layout is distributed equally between the elements only based on the inner size of the objects. The thing is that you conflate inner area of the top-row elements with outer area of the bottom-row elements. Actually all columns have the same size:

fig = Figure(resolution = (300, 200), fontsize = 12, figure_padding = 2)

Axis(fig[1,1:2])
Axis(fig[1,3:4])
Axis(fig[1,5:6])

Axis(fig[2,2:3])
Axis(fig[2,4:5])
Box( fig[2,6])

colgap!(fig.layout, Relative(0.02))

for i in 1:6
    Box(fig[:, i], color = (:red, 0.2), strokewidth = 0)
end

fig

So the intuition to put the lower row into a nested GridLayout is good, but then the two layouts don’t know anything about each other and therefore the automatic distribution of column widths is uneven.
This kind of “shifted” layout where you as a human know about some relationship between widths is not easy to represent for an automatic grid-based algorithm. But you can help out with your knowledge. If you’re sure that the legend will be less wide than one of the three subplots in the upper row, you can be sure that if you manually set the lower two axes to the width of the upper ones, the lower row will still fit. You can get the current inner size of an axis by looking at the px_area Observable of its Scene (the inner plot area). That is of course a bit of internal knowledge and might break later, but for custom plotting it’s always good to know your way around the internals a bit.

So here I derive the width setting for the lower two axes by the px_area of the top-left axis (all three are the same anyway). I give the legend box a fixed width as that simulates the legend better. The lower row can be put into a separate GridLayout so the columns are free relative to row 1. The lower GridLayout will be fixed in width if the two axes and the legend have known width, so it will center itself in the available space by default. You can set halign on it to :left or :right if you like that better.

fig = Figure(resolution = (300, 200), fontsize = 12, figure_padding = 6)

ax1 = Axis(fig[1,1])
Axis(fig[1,2])
Axis(fig[1,3])

# create a width observable from the first axis' inner area
w = @lift widths($(ax1.scene.px_area))[1]

subgl = GridLayout(fig[2, :])
Axis(subgl[1, 1], width = w)
Axis(subgl[1, 2], width = w)
Box(subgl[1, 3], width = 30)

colgap!(fig.layout, 5)
rowgap!(fig.layout, 10)
colgap!(subgl, 5)

fig

1 Like