Customization of AlgebraOfGraphics.jl Plots

Authors

Jose Storopoli

Juan Oneto

In this tutorial, we’ll explore different ways to customize your plots with AoG.jl. Some of these customizations will be directed towards Makie.jl, AoG.jl’s backend.

First, we’ll learn more about the syntax that we can use inside the mapping() function, along with some handy AoG.jl’s helper functions that can be also used inside mapping(). To conclude, we’ll take a dive into in-depth customizations of the following features:

  1. Axis: subplots inside the plot.
  2. Figure: general plot customizations.
  3. Legend: legend positioning and customizations.
  4. Colorbar: tweak colorbar’s position and appearance.
Note

We’ll cover advanced layouts with Axis in Tutorial 7 - Advanced Layouts with AlgebraOfGraphics.jl. Don’t forget to check it out.

1 🗺️ The mapping() syntax

AoG.jl uses a similar pair syntax that DataFrames.jl and DataFramesMeta.jl uses. As we’ve seen in Manipulating Tables with DataFramesMeta.jl of the Data Wrangling in Julia Tutorials, this syntax boils down to three variants:

  1. :column_name => "My Custom New Label": This is what we call a noop (no operation), it simply renames a column.
  2. :column_name => function(): This is an operation being applied to the column, but with no renaming.
  3. column_name => function() => "My Custom New Label": This is a mix of the previous two, i.e. an operation applied to a column along with renaming.
Tip

The function() inside the mapping() pair specification must have either zero arguments and no parentheses, such as :col => mean for example; or, in the case of arguments, be an anonymous function, e.g. col => (x -> round(x; digits = 2)). For more on Julia functions, don’t forget to check our Data Wrangling Tutorials, specifically Functions.

To begin, like before, let’s load AoG.jl, the data wrangling libraries, and the DataFrame we’ve used previously:

using PharmaDatasets
using DataFramesMeta

df = dataset("demographics_1")
first(df, 5)
5×6 DataFrame
Row ID AGE WEIGHT SCR ISMALE eGFR
Int64 Float64 Float64 Float64 Int64 Float64
1 1 34.823 38.212 1.1129 0 42.635
2 2 32.765 74.838 0.8846 1 126.0
3 3 35.974 37.303 1.1004 1 48.981
4 4 38.206 32.969 1.1972 1 38.934
5 5 33.559 47.139 1.5924 0 37.198

We will also do some columns transformations to CategoricalArrays:

using CategoricalArrays
@transform! df :SEX = categorical(:ISMALE);
@transform! df :SEX = recode(:SEX, 0 => "female", 1 => "male");
@transform! df :WEIGHT_cat = cut(:WEIGHT, 2; labels = ["light", "heavy"])

And now load AoG.jl along with CairoMakie.jl:

using CairoMakie
using AlgebraOfGraphics

Now that everything is loaded, let’s show some examples with the pair mapping() syntax.

First, a simple renaming of a column:

data(df) * mapping(:AGE => "Age in Years") * histogram() |> draw

It also works for keyword arguments inside mapping():

data(df) * mapping(:AGE; layout = :SEX => "Sex") * histogram() |> draw

Also works for x and y axes positional arguments:

data(df) *
mapping(:AGE => "Age in Years", :eGFR => "Estimated Glomerular Filtration Rate") |> draw

Now let’s us apply some operations inside mapping():

# Age in Months
data(df) * mapping(:AGE => (x -> x * 12)) * histogram() |> draw

The last plot works best with also a renaming:

# Much Better
data(df) * mapping(:AGE => (x -> x * 12) => "Age in Months") * histogram() |> draw

Here is a more advanced example where we are calculating the ratio for :AGE / :WEIGHT, just to show that you can stick a function with two or more column arguments inside mapping(). Just pass the columns as a tuple (between parentheses):

# a simple function just to showcase
ratio(x, y) = x / y
ratio (generic function with 1 method)
data(df) * mapping((:AGE, :WEIGHT) => ratio => "My Ratio") * histogram() |> draw

2 🦮 Helper functions

AoG.jl has some helper functions provided for the most data wrangling stuff you need to do inside a mapping() call within the pair syntax. They are:

  • renamer(): rename unique values.
  • sorter(): sort unique values.
  • nonnumeric(): treat values as categorial (i.e. factors or discrete variables)

Let’s cover renamer() first. We use renamer() whenever we want to rename values of a categorical column. It works right out of the bat inside a mapping() call using the pair syntax.

Let’s make a simple change in the unique values of the column :WEIGHT_cat:

data(df) *
mapping(
    :AGE;
    layout = :WEIGHT_cat => renamer("light" => "Underweight", "heavy" => "Overweight"),
) *
histogram() |> draw

renamer accepts either a sequence or a vector of pairs in the following convention:

"old value" => "new value"

sorter() works by passing either a sequence or vector of strings which will be used to reorder the unique values in the desired column. The ordering will be done by following the sequence of arguments inside sorter().

In a histogram above that we plotted using :AGE as the variable and facetting with :SEX, the first value (to the right) was female and the second (to the left) was male. We can change that with sorter():

data(df) *
mapping(:AGE; layout = :SEX => sorter("male", "female") => "Gender") *
histogram() |> draw

We could also have passed a vector of strings:

sorter(["male", "female"])

Sometimes, our dataset has integers to specify categories. AoG.jl will probably error on most of the operations that expects a categorical variable (String or CategoricalArray) as an input column.

For example, using the column :ISMALE which is filled with either 0 or 1 (integer types) fails if you use it in a “categorical” setting:

data(df) * mapping(:AGE; layout = :ISMALE) * histogram() |> draw
MethodError: MethodError(Core.kwcall, ((layout = [0, 1, 1, 1, 0, 0, 0, 0, 0, 1  …  1, 1, 0, 0, 1, 1, 1, 0, 1, 0], datalimits = ((19.187, 79.292),), closed = :left, normalization = :none), AlgebraOfGraphics._histogram, ([34.823, 32.765, 35.974, 38.206, 33.559, 53.758, 25.306, 39.897, 54.975, 40.732  …  30.317, 44.131, 20.009, 69.412, 43.751, 31.245, 45.48, 61.124, 33.803, 40.145],)), 0xffffffffffffffff)

To fix that you need to pass the nonnumeric() helper function. nonnumeric() tells AoG.jl that despite the column being “numeric” we do not want to use it in a numeric fashion, but instead in a discrete/categorical fashion. It takes no arguments and you can easily insert in a mapping() call within the pair syntax:

data(df) *
mapping(:AGE; layout = :ISMALE => nonnumeric) * # now it works!
histogram() |> draw

3 📈 Axis customization

Now we will delve into the thin line between AoG.jl and its backend Makie.jl. Since AoG.jl uses Makie.jl as a backend, some of the finer details we can only customize using Makie.jl keywords arguments. One of such finer customizations is the Axis customization, that controls the majority of customizations available in a plot.

Caution

Remember that pipe syntax that we were using almost exclusively to plot our AoG.jl visualizations? Let us refresh your memory. This one:

data(...) * mapping(...) * visual(...) |> draw

All of the customizations that we will cover in the remainder of this tutorial will need to be passed as keyword arguments to draw(). So this makes the piping operation to draw() inconvenient. Thus, we will be doing the following syntax now:

plt = data(...) * mapping(...) * visual(...)
draw(plt; ...)

We’ll define a AoG.jl Layer object and assign it to plt or plt_something and then afterwards we’ll call draw() on that Layer object.

To pass keyword arguments to customize Axis’ attributes, you need to pass a NamedTuple of the desired keyword arguments to draw() via:

draw(...; axis = (; keyword_1 = value_1, keyword_2 = value_2))
Caution

Note that we used the NamedTuple syntax with a leading semicolon to pass axis keywords, this is useful because it protects us from a common bug. That bug happens if you only want to pass one keyword, axis = (; a = 1) creates a NamedTuple but axis = (a = 1) creates a local variable a and your code will fail with a cryptic error. (For multi-element NamedTuples, however, it is allowed to leave out the semicolon.)

Let us define first a base plot for us to customize:

plt = data(df) * mapping(:AGE, :eGFR);

The first thing we’ll cover in Axis tweaking is the aspect ratio of our plots. If you want “instagram” like plots you can fix your aspect ratio to 1:

draw(plt; axis = (; aspect = 1))

Tip

One worthy mention is the DataAspect() which makes all plots in your AoG.jl visualization have the same aspect ratio as their x/y limits (this means the same distance along each axis is equivalent to the same distance in data space):

draw(plt; axis = (; aspect = DataAspect()))

If you want a title to be on your plot you can do so with title as a keyword argument to Axis:

draw(plt; axis = (; title = "My Plot"))

There’s also titlealign, titlecolor, titlefont, titlegap and titlesize:

draw(
    plt;
    axis = (;
        title = "My Plot",
        titlealign = :left, # using Makie.jl's symbols
        titlecolor = :blue, # using Makie.jl's symbols
        titlefont = "DejaVu Sans Mono",
        titlegap = 30,      # the gap between title and the plot
        titlesize = 32,     # title's font size
    ),
)

Sometimes we want to override the default resolution of the x- or y-axis ticks. This can be done with xticks and yticks Axis’ keyword arguments. It accepts a vector or a range:

draw(plt; axis = (; xticks = 20:5:80))

This is similar to ggplot2’s xlim(), ylim() and lims(). limits takes a tuple with 2 elements:

  1. x-axis limits: another tuple of (lower, upper).
  2. y-axis limits: another tuple of (lower, upper).

So, if you want to override x-axis limits to something between 0 and 2 and the y-axis limits to something between 10 and 20, you can with:

draw(...; axis = (; limits = ((0, 2), (10, 20))))
Caution

Watch out! This is a “nested” tuple: a tuple inside a tuple.

If you only want to specify either one of the x- or y-axis you can just input nothing to leave it unchanged. Thus, to only alter the y-axis limits you can use nothing as the first value in the tuple to represent the x-axis unchanged limits, and then you can pass a tuple which represents the y-axis limits:

draw(...; axis = (; limits = (nothing, (10, 20))))

Here’s an example:

draw(plt; axis = (; limits = ((30, 60), (50, 100))))

If you don’t want to have grids in your visualizations you can disable then with xgridvisible and ygridvisible:

draw(plt; axis = (; xgridvisible = false, ygridvisible = false))

This is analogous to ggplot2’s scale_[x|y]_log10(). You can specify AoG.jl to use log10 scale in either x- or y-axis with:

draw(plt; axis = (; [x | y]scale = log10))

For example:

draw(plt; axis = (; xscale = log10, yscale = log10))

4 🖼️ Figure customizations

The next finer customizations available in the interface between AoG.jl and Makie.jl is the Figure customizations. These are customizations that impact the whole visualization “figure”.

To pass keyword arguments to customize Figure’s attributes, you need to pass a NamedTuple of the desired keyword arguments to draw() via:

draw(...; figure = (; keyword_1 = value_1, keyword_2 = value_2))

The first Figure customization is the resolution. This is probably the most used Figure customization. It accepts a tuple with 2 elements. The first element is the Figure’s width in pixels. And the second element is the Figure’s height in pixels.

So, for example a 600x400 custom visualization can be specified as:

draw(plt; figure = (; resolution = (600, 400)))

Or an Ultra HD 1920x1080 visualization:

draw(plt; figure = (; resolution = (1920, 1080)))

Tip

If you specify a high resolution like 1920x1080, you’ll probably need to scale the plot attributes up accordingly. However, you can also simply scale up the output resolution of bitmap saved from the same figure by specifying a px_per_unit value higher than 1.

For example you can save a 1920x1080 pixel bitmap from a 960x540 figure fig by doing save(fig, px_per_unit = 2). You can also set this value for all inline output with CairoMakie.activate!(px_per_unit = 2).

We can increase or decrease the padding of a Figure with figure_padding:

draw(plt; figure = (; figure_padding = 0)) # NO padding

draw(plt; figure = (; figure_padding = 100)) # a LOT of padding

By default, a visualization’s background color is always white. You can change this with backgroundcolor:

draw(plt; figure = (; backgroundcolor = :gray80)) # light gray

5 Legend customizations

Another way to fine tune your visualizations is to tweak the legend. This is also accomplished by the interface between AoG.jl and Makie.jl.

To pass keyword arguments to customize Legend’s attributes, you need to pass a NamedTuple of the desired keyword arguments to draw() via:

draw(...; legend = (; keyword_1 = value_1, keyword_2 = value_2))

First, we need a new Layer AoG.jl object with a legend:

plt_legend = data(df) * mapping(:AGE, :eGFR; color = :SEX);

The first keyword argument that we’ll cover in legend tweaking is the position. By default it has the :right as value. We can specify some other Makie.jl symbols:

  • :top.
  • :bottom.
  • :left.
  • :right.
draw(plt_legend; legend = (; position = :top))

Next, we can also tweak the legend’s title position with the titleposition keyword argument with the same symbols as before:

draw(plt_legend; legend = (; position = :top, titleposition = :left))

We can also remove the frame that borders the legend with framevisible=false:

draw(plt_legend; legend = (; position = :top, titleposition = :left, framevisible = false))

Finally, like Figure, we can also increase or decrease the padding of a legend with padding:

draw(
    plt_legend;
    legend = (; position = :top, titleposition = :left, framevisible = true, padding = 0), # NO padding!
)

draw(
    plt_legend;
    legend = (; position = :top, titleposition = :left, framevisible = true, padding = 50), # LOTS of padding!
)

6 🖌️ Colorbar customizations

Our final fine tune customization is the Colorbar customizations. These are controlled with the colorbar keyword argument inside draw(). To pass keyword arguments to customize Colorbar’s attributes, you need to pass a NamedTuple of the desired keyword arguments to draw() via:

draw(...; colorbar = (; keyword_1 = value_1, keyword_2 = value_2))
Tip

Don’t forget to check out Makie.jl’s documentation on Colorbar to find more in-depth examples and use cases.

First, we need a new Layer AoG.jl object with a colorbar:

plt_colorbar = data(df) * mapping(:AGE, :WEIGHT; color = :eGFR);

And here’s our “base” visualization with a colorbar:

draw(plt_colorbar)

The first tweak that we can do in a colorbar is with respect to its positioning. The **keyword argument is position and it takes the same Makie.jl symbols we saw above:

  • :top.
  • :bottom.
  • :left.
  • :right (default).
draw(plt_colorbar; colorbar = (; position = :top))

We can also customize the size of a colorbar (the size is height for horizontal and width for vertical colorbars). You can alternatively set height and width directly:

draw(plt_colorbar; colorbar = (; position = :top, size = 30))

draw(plt_colorbar; colorbar = (; position = :top, height = 10, width = Relative(0.5)))

This is similar to the ticks in an axis. You can also specify which ticks are visible in a colorbar with the ticks keyword.

Our previous colorbar visualizations had ticks at multiples of 25, starting from 25 to 125. For example, we can tweak that to multiples of 10:

draw(plt_colorbar; colorbar = (; position = :top, height = 50, ticks = 20:10:120))

Tip

Like the xticks and yticks Axis customizations, colorbar’s ticks accepts either a vector or range.

7 🎨 Using themes

Most of the visual customizations you have seen above can be themed directly in Makie. This way, you don’t have to pass the same keyword arguments over and over again.

You can either use set_theme!() to set a theme that persists until set_theme!() is called again, or you use with_theme() which executes one function with a specific theme which is then reset.

Here are a couple of examples using with_theme() which apply analogously to set_theme!(). The themes used are Makie presets, which you can find out about in the Makie docs.

with_theme(theme_black()) do
    draw(plt_colorbar)
end

with_theme(theme_ggplot2()) do
    draw(plt_colorbar)
end

You can also override attributes from preset themes:

with_theme(theme_minimal(); Axis = (; bottomspinecolor = :red, leftspinecolor = :blue)) do
    draw(plt_colorbar)
end

8 🪧 Conclusion

In this tutorial, we showcase how to fine-tune and customize your AoG.jl visualization. Please notice that most of these customizations call Makie.jl under the hood. We highly recommend browsing Makie.jl’s documentation. It is a rich source of references on anything “customizable” in AoG.jl.