Layout Tutorial
In this tutorial, you will learn how to create a complex figure using Makie's layout tools.
Let's say that we want to create the following figure:
Here's the full code for reference:
using CairoMakie
using Makie.FileIO
f = Figure(backgroundcolor = RGBf(0.98, 0.98, 0.98),
resolution = (1000, 700))
ga = f[1, 1] = GridLayout()
gb = f[2, 1] = GridLayout()
gcd = f[1:2, 2] = GridLayout()
gc = gcd[1, 1] = GridLayout()
gd = gcd[2, 1] = GridLayout()
axtop = Axis(ga[1, 1])
axmain = Axis(ga[2, 1], xlabel = "before", ylabel = "after")
axright = Axis(ga[2, 2])
linkyaxes!(axmain, axright)
linkxaxes!(axmain, axtop)
labels = ["treatment", "placebo", "control"]
data = randn(3, 100, 2) .+ [1, 3, 5]
for (label, col) in zip(labels, eachslice(data, dims = 1))
scatter!(axmain, col, label = label)
density!(axtop, col[:, 1])
density!(axright, col[:, 2], direction = :y)
end
ylims!(axtop, low = 0)
xlims!(axright, low = 0)
axmain.xticks = 0:3:9
axtop.xticks = 0:3:9
leg = Legend(ga[1, 2], axmain)
hidedecorations!(axtop, grid = false)
hidedecorations!(axright, grid = false)
leg.tellheight = true
colgap!(ga, 10)
rowgap!(ga, 10)
Label(ga[1, 1:2, Top()], "Stimulus ratings", valign = :bottom,
font = "TeX Gyre Heros Bold",
padding = (0, 0, 5, 0))
xs = LinRange(0.5, 6, 50)
ys = LinRange(0.5, 6, 50)
data1 = [sin(x^1.5) * cos(y^0.5) for x in xs, y in ys] .+ 0.1 .* randn.()
data2 = [sin(x^0.8) * cos(y^1.5) for x in xs, y in ys] .+ 0.1 .* randn.()
ax1, hm = contourf(gb[1, 1], xs, ys, data1,
levels = 6)
ax1.title = "Histological analysis"
contour!(ax1, xs, ys, data1, levels = 5, color = :black)
hidexdecorations!(ax1)
ax2, hm2 = contourf(gb[2, 1], xs, ys, data2,
levels = 6)
contour!(ax2, xs, ys, data2, levels = 5, color = :black)
cb = Colorbar(gb[1:2, 2], hm, label = "cell group")
low, high = extrema(data1)
edges = range(low, high, length = 7)
centers = (edges[1:6] .+ edges[2:7]) .* 0.5
cb.ticks = (centers, string.(1:6))
cb.alignmode = Mixed(right = 0)
colgap!(gb, 10)
rowgap!(gb, 10)
brain = load(assetpath("brain.stl"))
ax3d = Axis3(gc[1, 1], title = "Brain activation")
m = mesh!(
ax3d,
brain,
color = [tri[1][2] for tri in brain for i in 1:3],
colormap = Reverse(:magma),
)
Colorbar(gc[1, 2], m, label = "BOLD level")
axs = [Axis(gd[row, col]) for row in 1:3, col in 1:2]
hidedecorations!.(axs, grid = false, label = false)
for row in 1:3, col in 1:2
xrange = col == 1 ? (0:0.1:6pi) : (0:0.1:10pi)
eeg = [sum(sin(pi * rand() + k * x) / k for k in 1:10)
for x in xrange] .+ 0.1 .* randn.()
lines!(axs[row, col], eeg, color = (:black, 0.5))
end
axs[3, 1].xlabel = "Day 1"
axs[3, 2].xlabel = "Day 2"
Label(gd[1, :, Top()], "EEG traces", valign = :bottom,
font = "TeX Gyre Heros Bold",
padding = (0, 0, 5, 0))
rowgap!(gd, 10)
colgap!(gd, 10)
for (i, label) in enumerate(["sleep", "awake", "test"])
Box(gd[i, 3], color = :gray90)
Label(gd[i, 3], label, rotation = pi/2, tellheight = false)
end
colgap!(gd, 2, 0)
n_day_1 = length(0:0.1:6pi)
n_day_2 = length(0:0.1:10pi)
colsize!(gd, 1, Auto(n_day_1))
colsize!(gd, 2, Auto(n_day_2))
for (label, layout) in zip(["A", "B", "C", "D"], [ga, gb, gc, gd])
Label(layout[1, 1, TopLeft()], label,
textsize = 26,
font = "TeX Gyre Heros Bold",
padding = (0, 5, 5, 0),
halign = :right)
end
colsize!(f.layout, 1, Auto(0.5))
rowsize!(gcd, 1, Auto(1.5))
f
How do we approach this task?
In the following sections, we'll go over the process step by step. We're not always going to use the shortest possible syntax, as the main goal is to get a better understanding of the logic and the available options.
Basic layout plan
When building figures, you always think in terms of rectangular boxes. We want to find the biggest boxes that enclose meaningful groups of content, and then we realize those boxes either using
GridLayout
or by placing content objects there.
If we look at our target figure, we can imagine one box around each of the labelled areas A, B, C and D. But A and C are not in one row, neither are B and D. This means that we don't use a 2x2 GridLayout, but have to be a little more creative.
We could say that A and B are in one column, and C and D are in one column. We can have different row heights for both groups by making one big nested
GridLayout
within the second column, in which we place C and D. This way the rows of column 2 are decoupled from column 1.
Ok, let's create the figure first with a gray backgroundcolor, and a predefined font:
using CairoMakie
using FileIO
f = Figure(backgroundcolor = RGBf(0.98, 0.98, 0.98),
resolution = (1000, 700))
Setting up GridLayouts
Now, let's make the four nested GridLayouts that are going to hold the objects of A, B, C and D. There's also the layout that holds C and D together, so the rows are separate from A and B. We are not going to see anything yet as we have no visible content, but that will come soon.
Note
It's not strictly necessary to first create separate
GridLayout
s, then use them to place objects in the figure. You can also implicitly create nested grids using multiple indexing, for example like
Axis(f[1, 2:3][4:5, 6])
. This is further explained in
GridPositions and GridSubpositions
. But if you want to manipulate your nested grids afterwards, for example to change column sizes or row gaps, it's easier if you have them stored in variables already.
ga = f[1, 1] = GridLayout()
gb = f[2, 1] = GridLayout()
gcd = f[1:2, 2] = GridLayout()
gc = gcd[1, 1] = GridLayout()
gd = gcd[2, 1] = GridLayout()
Panel A
Now we can start placing objects into the figure. We start with A.
There are three axes and a legend. We can place the axes first, link them appropriately, and plot the first data into them.
axtop = Axis(ga[1, 1])
axmain = Axis(ga[2, 1], xlabel = "before", ylabel = "after")
axright = Axis(ga[2, 2])
linkyaxes!(axmain, axright)
linkxaxes!(axmain, axtop)
labels = ["treatment", "placebo", "control"]
data = randn(3, 100, 2) .+ [1, 3, 5]
for (label, col) in zip(labels, eachslice(data, dims = 1))
scatter!(axmain, col, label = label)
density!(axtop, col[:, 1])
density!(axright, col[:, 2], direction = :y)
end
f
There's a small gap between the density plots and their axes, which we can remove by fixing one side of the limits.
ylims!(axtop, low = 0)
xlims!(axright, low = 0)
f
We can also choose different x ticks with whole numbers.
axmain.xticks = 0:3:9
axtop.xticks = 0:3:9
f
Legend
We have set the
label
attribute in the scatter call so it's easier to construct the legend. We can just pass
axmain
as the second argument to
Legend
.
leg = Legend(ga[1, 2], axmain)
f
Legend Tweaks
There are a couple things we want to change. There are unnecessary decorations for the side axes, which we are going to hide.
Also, the top axis does not have the same height as the legend. That's because a legend is usually used on the right of an
Axis
and is therefore preset with
tellheight = false
. We set this attribute to
true
so the row in which the legend sits can contract to its known size.
hidedecorations!(axtop, grid = false)
hidedecorations!(axright, grid = false)
leg.tellheight = true
f
The axes are still a bit too far apart, so we reduce column and row gaps.
colgap!(ga, 10)
rowgap!(ga, 10)
f
We can make a title by placing a label across the top two elements.
Label(ga[1, 1:2, Top()], "Stimulus ratings", valign = :bottom,
font = "TeX Gyre Heros Bold",
padding = (0, 0, 5, 0))
f
Panel B
Let's move to B. We have two axes stacked on top of each other, and a colorbar alongside them. This time, we create the axes by just plotting into the right
GridLayout
slots. This can be more convenient than creating an
Axis
first.
xs = LinRange(0.5, 6, 50)
ys = LinRange(0.5, 6, 50)
data1 = [sin(x^1.5) * cos(y^0.5) for x in xs, y in ys] .+ 0.1 .* randn.()
data2 = [sin(x^0.8) * cos(y^1.5) for x in xs, y in ys] .+ 0.1 .* randn.()
ax1, hm = contourf(gb[1, 1], xs, ys, data1,
levels = 6)
ax1.title = "Histological analysis"
contour!(ax1, xs, ys, data1, levels = 5, color = :black)
hidexdecorations!(ax1)
ax2, hm2 = contourf(gb[2, 1], xs, ys, data2,
levels = 6)
contour!(ax2, xs, ys, data2, levels = 5, color = :black)
f
Colorbar
Now we need a colorbar. Because we haven't set specific edges for the two contour plots, just how many levels there are, we can make a colorbar using one of the contour plots and then label the bins in there from one to six.
cb = Colorbar(gb[1:2, 2], hm, label = "cell group")
low, high = extrema(data1)
edges = range(low, high, length = 7)
centers = (edges[1:6] .+ edges[2:7]) .* 0.5
cb.ticks = (centers, string.(1:6))
f
Mixed alignmode
The right edge of the colorbar is currently aligned with the right edge of the upper density plot. This can later cause a bit of a gap between the density plot and content on the right.
In order to improve this, we can pull the colorbar labels into its layout cell using the
Mixed
alignmode. The keyword
right = 0
means that the right side of the colorbar should pull its protrusion content inward with an additional padding of
0
.
cb.alignmode = Mixed(right = 0)
f
As in A, the axes are a bit too far apart.
colgap!(gb, 10)
rowgap!(gb, 10)
f
Panel C
Now, we move on to panel C. This is just an
Axis3
with a colorbar on the side.
brain = load(assetpath("brain.stl"))
ax3d = Axis3(gc[1, 1], title = "Brain activation")
m = mesh!(
ax3d,
brain,
color = [tri[1][2] for tri in brain for i in 1:3],
colormap = Reverse(:magma),
)
Colorbar(gc[1, 2], m, label = "BOLD level")
f
Note that the z label overlaps the plot to the left a little bit.
Axis3
can't have automatic protrusions because the label positions change with the projection and the cell size of the axis, which is different from the 2D
Axis
.
You can set the attribute
ax3.protrusions
to a tuple of four values (left, right, bottom, top) but in this case we just continue plotting until we have all objects that we want, before we look if small tweaks like that are necessary.
Panel D
We move on to Panel D, which has a grid of 3x2 axes.
axs = [Axis(gd[row, col]) for row in 1:3, col in 1:2]
hidedecorations!.(axs, grid = false, label = false)
for row in 1:3, col in 1:2
xrange = col == 1 ? (0:0.1:6pi) : (0:0.1:10pi)
eeg = [sum(sin(pi * rand() + k * x) / k for k in 1:10)
for x in xrange] .+ 0.1 .* randn.()
lines!(axs[row, col], eeg, color = (:black, 0.5))
end
axs[3, 1].xlabel = "Day 1"
axs[3, 2].xlabel = "Day 2"
f
We can make a little title for the six axes by placing a
Label
in the top protrusion of row 1 and across both columns.
Label(gd[1, :, Top()], "EEG traces", valign = :bottom,
font = "TeX Gyre Heros Bold",
padding = (0, 0, 5, 0))
f
Again, we bring the subplots closer together by reducing gap sizes.
rowgap!(gd, 10)
colgap!(gd, 10)
f
EEG labels
Now, we add three boxes on the side with labels in them. In this case, we just place them in another column to the right.
for (i, label) in enumerate(["sleep", "awake", "test"])
Box(gd[i, 3], color = :gray90)
Label(gd[i, 3], label, rotation = pi/2, tellheight = false)
end
f
The boxes are in the correct positions, but we still need to remove the column gap.
colgap!(gd, 2, 0)
f
Scaling axes relatively
The fake eeg data we have created has more datapoints on day 1 than day 2. We want to scale the axes so that they both have the same zoom level. We can do this by setting the column widths to
Auto(x)
where x is a number proportional to the number of data points of the axis. This way, both will have the same relative scaling.
n_day_1 = length(0:0.1:6pi)
n_day_2 = length(0:0.1:10pi)
colsize!(gd, 1, Auto(n_day_1))
colsize!(gd, 2, Auto(n_day_2))
f
Subplot labels
Now, we can add the subplot labels. We already have our four
GridLayout
objects that enclose each panel's content, so the easiest way is to create
Label
s in the top left protrusion of these layouts. That will leave all other alignments intact, because we're not creating any new columns or rows. The labels belong to the gaps between the layouts instead.
for (label, layout) in zip(["A", "B", "C", "D"], [ga, gb, gc, gd])
Label(layout[1, 1, TopLeft()], label,
textsize = 26,
font = "TeX Gyre Heros Bold",
padding = (0, 5, 5, 0),
halign = :right)
end
f
Final tweaks
This looks pretty good already, but the first column of the layout is a bit too wide. We can reduce the column width by setting it to
Auto
with a number smaller than 1, for example. This gives the column a smaller weight when distributing widths between all columns with
Auto
sizes.
You can also use
Relative
or
Fixed
but they are not as flexible if you add more things later, so I prefer using
Auto
.
colsize!(f.layout, 1, Auto(0.5))
f
The EEG traces are currently as high as the brain axis, let's increase the size of the row with the panel C layout a bit so it has more space.
And that is the final result:
rowsize!(gcd, 1, Auto(1.5))
f
These docs were autogenerated using Makie: v0.18.4, GLMakie: v0.7.4, CairoMakie: v0.9.4, WGLMakie: v0.7.4