Layout Tutorial

In this tutorial, we will see some of the capabilities of layouts in Makie while building a complex figure step by step. This is the final result we will create:

layout_tutorial_final

All right, let's get started!

Importing a backend

First, we import CairoMakie, which re-exports AbstractPlotting and MakieLayout.

using CairoMakie

The same works for the other backends WGLMakie and GLMakie. You can find an overview of the different backends with their capabilities in Backends & Output. The old MakieLayout package which you needed to install separately is deprecated, since it now lives directly in AbstractPlotting. As a side note, if you do not want to make plotting code backend dependent, for example inside a package where the user should choose the backend themselves, you can depend on AbstractPlotting alone. This allows the user to do:

using CustomPlots # depends only on AbstractPlotting
using GLMakie # chooses GLMakie as the backend for CustomPlots

Creating a figure

We create an empty Figure which will hold all our content elements and organize them in a layout.

using CairoMakie

noto_sans = "../assets/NotoSans-Regular.ttf"
noto_sans_bold = "../assets/NotoSans-Bold.ttf"

fig = Figure(resolution = (1200, 700), backgroundcolor = RGBf0(0.98, 0.98, 0.98), font = noto_sans)

fig

First axis

The figure is completely empty, I have made the background light gray so it's easier to see. Now we add an Axis.

We create the axis and place it into the figure's layout in one go. You place objects in a figure by using indexing syntax. You can save the axis in a variable by chaining the = expressions.

We call the axis title "Pre Treatment" because we're going to plot some made up measurements, like they could result from an experimental trial.

ax1 = fig[1, 1] = Axis(fig, title = "Pre Treatment")

fig

Plotting into an axis

We can plot into the axis with the ! versions of Makie's plotting functions. Such mutating function calls return the plot object that is created, which we save for later.

data1 = randn(50, 2) * [1 2.5; 2.5 1] .+ [10 10]

line1 = lines!(ax1, 5..15, x -> x, color = :red, linewidth = 2)
scat1 = scatter!(ax1, data1,
    color = (:red, 0.3), markersize = 15px, marker = '■')

fig

Multiple axes

This looks nice already, but we want another axis with a second dataset, to the right of the one we have. Currently our layout has one row and one cell, and only one Axis inside of it:

fig.layout
GridLayout[1, 1] with 1 children
 ┗━ [1, 1] Axis

We can extend the grid with a new axis by plotting into a new grid position. Let's place a new axis with another line plot next to the one we have, in row 1 and column 2.

We can use the non-mutating plotting syntax and pass a position in our figure as the first argument. When we index into a figure, we get a FigurePosition object which describes the position we want to put our new axis in.

The plotting call returns an AxisPlot object which we can directly destructure into axis and plot.

ax2, line2 = lines(fig[1, 2], 7..17, x -> -x + 26,
    color = :blue, linewidth = 2,
    axis = (title = "Post Treatment",))

fig

As you can see, the first axis has shrunk to the left to make space for the new axis on the right. We can take another look at the layout to see how it has changed:

fig.layout
GridLayout[1, 2] with 2 children
 ┣━ [1, 1] Axis
 ┗━ [1, 2] Axis

Let's plot into the new axis, the same way we did the scatter plots before. We can also leave out the axis as the first argument if we just want to plot into the current axis.

data2 = randn(50, 2) * [1 -2.5; -2.5 1] .+ [13 13]

scat2 = scatter!(data2,
    color = (:blue, 0.3), markersize = 15px, marker = '▲')

fig

Linking axes

We want to make the left and right axes correspond to each other, so we can compare the plots more easily. To do that, we link both x and y axes. That will keep them synchronized. The function linkaxes! links both x and y, linkxaxes! links only x and linkyaxes! links only y.

linkaxes!(ax1, ax2)

fig

This looks good, but now both y-axes are the same, so we can hide the right one to make the plot less cluttered. We keep the grid lines, though. You can see that now that the y-axis is gone, the two Axes grow to fill the gap.

hideydecorations!(ax2, grid = false)

fig

Even though our plots are entirely made up, we should follow best practice and label the axes. We can do this with the xlabel and ylabel attributes of the Axis.

ax1.xlabel = "Weight [kg]"
ax2.xlabel = "Weight [kg]"
ax1.ylabel = "Maximum Velocity [m/sec]"

fig

Adding a legend

Let's add a legend to our figure that describes elements from both axes. We use Legend for that. Legend is a relatively complex object and there are many ways to create it, but here we'll keep it simple. We place the legend on the right again, in row 1 and column 3. Instead of specifying column three, we can also say end+1.

leg = fig[1, end+1] = Legend(fig,
    [line1, scat1, line2, scat2],
    ["f(x) = x", "Data", "f(x) = -x + 26", "Data"])

fig

You can see one nice feature of Makie here, which is that the legend takes much less horizontal space than the two axes. In fact, it takes exactly the space that it needs. This is possible because layoutable objects in Makie can tell their width or height to their parent GridLayout, which can then shrink the row or column appropriately.

One thing that could be better about this plot, is that the legend looks like it belongs only to the right axis, even though it describes elements from both axes. So let's move it in the middle below the two. This is easily possible in Makie, without having to recreate the plot from scratch. We simply assign the legend to its new slot.

We want it in the second row, and spanning the first two columns.

fig[2, 1:2] = leg

fig

Fixing spacing issues

There are a couple of things wrong with this. The legend is where we want it, below the two axes. But it takes too much space vertically, and there is a large gap on the right.

Let's deal with the gap on the right first. It's the hole that was left by the legend, and it's even bigger now because it gets an equal share of space with the two axes, now that there is no legend shrinking the column width to its own size.

We can remove empty cells in a layout by calling trim! on it:

trim!(fig.layout)

fig

This is much better already! But the legend still takes too much space vertically. The reason for that is the default tellheight setting of the legend. It's set to false, which essentially means that it can compute its own height, but doesn't tell the layout about it. This makes sense for the most common situation where the legend sits on the right of an axis. We wouldn't want the axis to shrink to the height of the legend. But now that the legend has its own row, we do want this behavior. So we set the tellheight attribute to true.

leg.tellheight = true

fig

Now the legend's row is shrunk to fit. One thing that we can do to improve the use of space is to change the legend's orientation to :horizontal.

leg.orientation = :horizontal

fig