Skip to content

Intro to AoG - VI - Editing and composing AoG plots with Makie

In the last chapters we have concentrated on creating standalong AlgebraOfGraphics figures, without focusing to much on fiddling with the visual results. However, in practice, many people spend a lot of time on tweaking their figures until they are satisfied with the smallest details.

Too often, such edits still happen in vector graphics software like Inkscape these days, which is both time consuming and non-reproducible, and should therefore be avoided if at all possible. Luckily, AlgebraOfGraphics is built on Makie, and Makie has a special focus on layouting and interactive editing. This means that it is easy to combine multiple AoG plots in a figure, as well as changing the underlying plot objects programmatically if that's desired.

Let's first bring our trusty old penguin dataset back in and draw a basic facet plot. We store the output of draw in a variable figuregrid, too:

julia
using AlgebraOfGraphics
using CairoMakie
using DataFrames

penguins = DataFrame(AlgebraOfGraphics.penguins())

spec = data(penguins) *
    mapping(
        :bill_length_mm => "Bill length (mm)",
        :bill_depth_mm => "Bill depth (mm)",
        row = :sex,
        col = :island,
        color = :species => "Species"
    ) *
    visual(Scatter)

figuregrid = draw(spec)

The type of figuregrid is:

julia
typeof(figuregrid)
AlgebraOfGraphics.FigureGrid

True to its name, it stores two fields, figure and grid.

The figure contains the Makie Figure object in which every other element is drawn. We can do basic modifications to its underlying Scene object, like changing its background color (check Makie's scene tutorial for more information on Scenes):

julia
figure = figuregrid.figure

figure.scene.backgroundcolor = Makie.to_color(:gray90)

figure

We can also resize the figure:

julia
resize!(figure, 500, 300)

figure

Drawing into an existing Figure

But we can also add further objects to our Figure. And we can do this directly with AlgebraOfGraphics by using the draw! function instead of draw (the ! is a naming convention in Julia that hints that something is being modified by a function, here a Figure).

Let's say we wanted to also show flipper length vs body mass in a separate plot. We can do that by draw!ing the specification into a grid position of the underlying figure layout (check, for example, the layout tutorial in the Makie docs to learn more about grid layouts).

To know where to put our new plot, it can be helpful to look at the current layout first:

julia
figure.layout
GridLayout[1:2, 1:4] with 14 children
 ┣━ [1, 1] Axis
 ┣━ [2, 1] Axis
 ┣━ [1, 2] Axis
 ┣━ [2, 2] Axis
 ┣━ [1, 3] Axis
 ┣━ [2, 3] Axis
 ┣━ [1:2, 1] Label
 ┣━ [1, 3] Label
 ┣━ [2, 3] Label
 ┣━ [2, 1:3] Label
 ┣━ [1, 1] Label
 ┣━ [1, 2] Label
 ┣━ [1, 3] Label
 ┗━ [1:2, 4] Legend

This [1:2, 1:4] layout has 2 rows and 4 columns, so if we wanted to put something to the right of it, that could live in fifth column and cross over both rows. Let's execute that idea by specifying the grid position figure[:, 5]. This means all rows, 5th column (which will be created if it doesn't exist):

julia
flipper_spec = data(penguins) *
    mapping(:flipper_length_mm => "Flipper length (mm)", :body_mass_g => "Body mass (g)", color = :species)

figuregrid_flipper = draw!(figure[:, 5], flipper_spec)

figure

As you can see, our flipper_spec, while using a color scale, did not create its own legend. AlgebraOfGraphics doesn't automatically draw legends when draw! is used, because it's likely that the position should be something special anyway. We could draw the legend by calling legend!(figure[row, col], figuregrid_flipper) but we don't need it here because it would be redundant. If you skip redundant legends, make sure that they really are the same though, and you didn't accidentally assign colors differently in one of the graphs!

Our composed figure is a bit squished, so we could resize again:

julia
resize!(figure, 700, 400)

figure

Layout modifications

Maybe we don't like the position of the shared legend. In Makie, we can move layout elements around whenever we want, we just need to get ahold of them first. We can get the only Legend object in different ways, one would be to get it from the figure.content vector:

julia
legend = only(filter(x -> x isa Legend, figure.content))
Legend()

Another way would be to use its layout position [1:2, 4] that we printed above, and grab it with Makie's content function:

julia
legend = content(figure[1:2, 4])
Legend()

We can move the legend to any other place by assigning it to a new layout position. I want to place it centered below both other plots, for which I will also change its orientation to horizontal:

julia
legend.orientation = :horizontal
legend.titleposition = :left
figure[end+1, :] = legend

figure

The legend has moved correctly, although it has also left a hole in column 4. We can delete that column with the deletecol! function from GridLayoutBase, which is the package that implements Makie's layouts:

julia
Makie.GridLayoutBase.deletecol!(figure.layout, 4)

figure

To label our two subplots, we can add Makie's Label objects to our figure. By using the TopLeft() side, we place the labels outside of the main grid and left-aligned into the gap space:

julia
Label(figure[1, 1, TopLeft()], "A", font = :bold, fontsize = 24, halign = :left)
Label(figure[1, 4, TopLeft()], "B", font = :bold, fontsize = 24, halign = :left)
figure

Maybe the fourth column is a little thin with 25%, we could push it up to 40% to balance out the look:

julia
colsize!(figure.layout, 4, Relative(0.4))

figure

Axis modifications

We haven't looked at the grid field of figuregrid, yet. It contains AxisEntries objects which store references to all the axes in a plot's facet layout. So if we want to make some axis modifications after the fact, we can grab them from there, too:

Let's pretend that the data in row 2, column 2 of our original facet plot is somehow of high importance, which we'd like to signal with a reddish background color:

julia
figuregrid.grid[2, 2].axis.backgroundcolor = :rosybrown1

figure

To explain why we colored one axis red, we can add a small footnote-like Label to the figure:

julia
Label(
    figure[end+1, :],
    rich(
        rich("⬛", color = :rosybrown1, fontsize = 18),
        " The male penguins on Dream island have to be mentioned specifically.",
        fontsize = 10
    ),
    halign = :right,
)

figure

Free modifications

One last example to really emphasize the flexibility of plotting with Makie and AlgebraOfGraphics. As the Figure is basically just a drawing canvas, we can freely plot into it wherever we want. You could imagine drawing connections between different axes or labels, annotations outside the margins, whatever you can think of.

Let's say we wanted to distribute our figure during review, and wanted to mark it as a draft. We can achieve this by plotting a text into the main Scene of the figure, effectively plotting across all other content.

julia
text!(figure.scene, 0.5, 0.5, space = :relative, fontsize = 200, text = "DRAFT",
    color = (:gray, 0.1), rotation = pi/8, font = :bold, align = (:center, :center))

figure

Summary

In this chapter, you have seen how to draw compose multiple AlgebraOfGraphics plots in a Makie Figure, make layout, legend and axis modifications, and achieve even more unusual effects by plotting directly into the figure's main scene. Making use of all of these options can save you a lot of time that you would otherwise spend editing and re-editing your plots manually. For more techniques and possibilities, refer to sources like Makie's documentation or the gallery Beautiful Makie.

In the next chapter, we'll have a look at one more special feature that mapping provides - the ability to have multiple scales of the same aesthetic in one plot.