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:
You can click here to see the full code
using CairoMakie noto_sans = assetpath("fonts", "NotoSans-Regular.ttf") noto_sans_bold = assetpath("fonts", "NotoSans-Bold.ttf") fig = Figure(backgroundcolor = RGBf0(0.98, 0.98, 0.98), resolution = (1000, 700), font = noto_sans) ax1 = fig[1, 1] = Axis(fig, title = "Pre Treatment") 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 = '■') ax2, line2 = lines(fig[1, 2], 7..17, x -> -x + 26, color = :blue, linewidth = 2, axis = (title = "Post Treatment",)) data2 = randn(50, 2) * [1 -2.5; -2.5 1] .+ [13 13] scat2 = scatter!(data2, color = (:blue, 0.3), markersize = 15px, marker = '▲') linkaxes!(ax1, ax2) hideydecorations!(ax2, grid = false) ax1.xlabel = "Weight [kg]" ax2.xlabel = "Weight [kg]" ax1.ylabel = "Maximum Velocity [m/sec]" leg = fig[1, end+1] = Legend(fig, [line1, scat1, line2, scat2], ["f(x) = x", "Data", "f(x) = -x + 26", "Data"]) fig[2, 1:2] = leg trim!(fig.layout) leg.tellheight = true leg.orientation = :horizontal hm_axes = fig[1:2, 3] = [Axis(fig, title = t) for t in ["Cell Assembly Pre", "Cell Assembly Post"]] heatmaps = [heatmap!(ax, i .+ rand(20, 20)) for (i, ax) in enumerate(hm_axes)] hm_sublayout = GridLayout() fig[1:2, 3] = hm_sublayout # there is another shortcut for filling a GridLayout vertically with # a vector of content hm_sublayout[:v] = hm_axes hidedecorations!.(hm_axes) for hm in heatmaps hm.colorrange = (1, 3) end cbar = hm_sublayout[:, 2] = Colorbar(fig, heatmaps, label = "Activity [spikes/sec]") cbar.height = Relative(2/3) cbar.ticks = 1:0.5:3 supertitle = fig[0, :] = Label(fig, "Complex Figures with Makie", textsize = 24, font = noto_sans_bold, color = (:black, 0.25)) label_a = fig[2, 1, TopLeft()] = Label(fig, "A", textsize = 24, font = noto_sans_bold, halign = :right) label_b = fig[2, 3, TopLeft()] = Label(fig, "B", textsize = 24, font = noto_sans_bold, halign = :right) label_a.padding = (0, 6, 16, 0) label_b.padding = (0, 6, 16, 0) # Aspect(1, 1) means that relative to row 1 # (row because we're setting a colsize, # and aspect ratios are always about the other side) # we set the column to an aspect ratio of 1 colsize!(hm_sublayout, 1, Aspect(1, 1)) save("layout_tutorial_final.svg", fig)
All right, let's get started!
First, we import CairoMakie, which re-exports Makie.
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 Makie. 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
Makie alone. This allows the user to do:
using CustomPlots # depends only on Makie using GLMakie # chooses GLMakie as the backend for CustomPlots
We create an empty
Figure which will hold all our content elements and organize them in a layout.
using CairoMakie noto_sans = assetpath("fonts", "NotoSans-Regular.ttf") noto_sans_bold = assetpath("fonts", "NotoSans-Bold.ttf") fig = Figure(backgroundcolor = RGBf0(0.98, 0.98, 0.98), resolution = (1000, 700), font = noto_sans) fig
The figure is completely empty, I have made the background light gray so it's easier to see. Now we add an
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
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
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
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:
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:
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
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
ylabel attributes of the
ax1.xlabel = "Weight [kg]" ax2.xlabel = "Weight [kg]" ax1.ylabel = "Maximum Velocity [m/sec]" fig
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
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
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:
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
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
leg.orientation = :horizontal fig