people coming from ggplot2 who want the same grammar
AlgebraOfGraphics.jl
people who want an idiomatically Julian grammar of graphics
Plain Makie.jl
full control over the figure
This chapter shows all three on the same data, because the right choice depends on the task — and because knowing how to translate between them keeps you from getting stuck.
A figure cannot be checked automatically, so in most Your turn boxes you compare your plot with the one in the solution. Start each session by loading the packages and the data, with the book folder open in VS Code:
usingTidierPlotsggplot(ponds_c, @aes(x = canopy, y = richness, color = region)) +geom_point(alpha =0.7) +labs(x ="Canopy cover (%)", y ="Species richness", title ="Richness declines with shading")
Figure 6.1: Richness against canopy cover, by region.
In R
ggplot(ponds_c, aes(x = canopy, y = richness, colour = region)) +geom_point(alpha =0.7) +labs(x ="Canopy cover (%)", y ="Species richness")
The translation is nearly character for character; the difference is the @ in @aes.
NoteYour turn 6.1 — Richness and temperature
Redo Figure 6.1 with water temperature (temp_c) on the x axis and the points coloured by hydroperiod (hydro). Label both axes. Does richness change with temperature as clearly as with canopy?
There is no checker for this one: compare your figure with the one in the solution.
TipHint
Only the @aes mapping and the labels change.
CautionSolution
ggplot(ponds_c, @aes(x = temp_c, y = richness, color = hydro)) +geom_point(alpha =0.7) +labs(x ="Water temperature (°C)", y ="Species richness")
The cloud is much flatter than for canopy. The simulation does include a temperature effect (scripts/make_data.jl), but a small one: about 3% more species per degree, invisible next to the scatter between ponds.
6.2AlgebraOfGraphics.jl: an algebraic grammar
AoG treats plots as an algebra: * combines layers, + superimposes them.
Figure 6.2: The same plot in AlgebraOfGraphics, with a linear fit per region.
Note
AlgebraOfGraphics.data and AlgebraOfGraphics.linear are qualified because the names data and linear collide with other loaded packages. In a script using only AoG, data(...) and linear() suffice.
NoteYour turn 6.2 — Body size, with fitted lines
With AlgebraOfGraphics, plot body_size against temp_c, coloured by hydro, with a linear fit for each hydroperiod. Do permanent and temporary ponds differ?
There is no checker for this one: compare your figure with the one in the solution.
TipHint
Start from the block above and change the mapping. The fitted lines follow the colour grouping automatically.
The two lines almost coincide: in the simulation, body size depends on temperature and canopy but not on hydroperiod. We test this formally in Chapter 8.
Figure 6.3: Richness–area relationship faceted by hydroperiod.
NoteYour turn 6.3 — Facets in the ggplot grammar
Translate Figure 6.3 to TidierPlots.jl, using facet_wrap to split the panels by hydroperiod. Leave out the fitted lines (see the warning in the solution).
There is no checker for this one: compare your figure with the one in the solution.
TipHint
facet_wrap(facets = :hydro) is added with +, like any other layer.
CautionSolution
ggplot(ponds_c, @aes(x = log_area, y = richness, color = region)) +geom_point(alpha =0.6) +facet_wrap(facets =:hydro) +labs(x ="log(area)", y ="Richness")
WarningA limit of TidierPlots
In TidierPlots.jl 1.0.0 (checked in September 2026), adding geom_smooth(method = "lm") to a faceted plot fails with column name :facet not found. geom_smooth works without facets, and facets work without geom_smooth, but not together. When the ggplot grammar reaches a limit like this, AlgebraOfGraphics (Figure 6.3) is the way out.
When a figure needs a layout the grammar cannot express, drop down to Makie.
fig =Figure(size = (860, 380))ax1 =Axis(fig[1, 1], xlabel ="Canopy cover (%)", ylabel ="Richness", title ="(a) Canopy")scatter!(ax1, ponds_c.canopy, ponds_c.richness, color = (:steelblue, 0.6))ax2 =Axis(fig[1, 2], xlabel ="log(area)", ylabel ="Richness", title ="(b) Area")scatter!(ax2, ponds_c.log_area, ponds_c.richness, color = (:darkorange, 0.6))ax3 =Axis(fig[1, 3], xlabel ="Richness", ylabel ="Frequency", title ="(c) Distribution")hist!(ax3, ponds_c.richness, bins =12, color = (:seagreen, 0.7))fig
Figure 6.5: A composite figure assembled by hand with Makie.
Makie’s layout system is grid-based (fig[row, col]), covering the role of patchwork and cowplot in R without an extra package.
NoteYour turn 6.5 — Two panels by hand
Build a figure with size = (800, 350) and two panels: on the left, a boxplot of richness by region; on the right, a bar chart of the proportion of ponds with the focal species in each region (the numbers from 5.4). Store it in fig_regions.
There is no checker for this one: compare your figure with the one in the solution.
TipHint
Makie’s boxplot! and barplot! want numbers on the x axis. Give each region a number with [findfirst(==(r), regs) for r in ponds.region], where regs = ["Amazon", "Atlantic", "Cerrado"], and put the names back with xticks = (1:3, regs) in each Axis.
CautionSolution
regs = ["Amazon", "Atlantic", "Cerrado"]idx = [findfirst(==(r), regs) for r in ponds.region]props = [mean(ponds.focal[ponds.region .== r]) for r in regs]fig_regions =Figure(size = (800, 350))ax1 =Axis(fig_regions[1, 1], xticks = (1:3, regs), xlabel ="Region", ylabel ="Richness", title ="(a) Richness")boxplot!(ax1, idx, ponds.richness, color = (:steelblue, 0.6))ax2 =Axis(fig_regions[1, 2], xticks = (1:3, regs), xlabel ="Region", ylabel ="Proportion of ponds", title ="(b) Focal species")barplot!(ax2, 1:3, props, color = (:darkorange, 0.8))fig_regions
The two panels tell different stories: richness differs sharply between regions, focal-species occurrence hardly at all.
Save fig_regions from 6.5 as focal-by-region.png, at three pixels per unit, in the book folder. Then check the file: check("6.6", "focal-by-region.png").
TipHint
The block above shows the keyword that sets the resolution.
An 800 × 350 figure at three pixels per unit gives a 2400 × 1050 px image — enough for a two-column journal figure. For vector output, save as .pdf instead.
TipChoosing a backend
CairoMakie produces publication-quality static output (PNG, PDF, SVG) and is what this book uses. GLMakie opens GPU-accelerated interactive windows — useful for exploring data, unsuitable for rendering a book.
Challenges
In 6.1 you judged the temperature effect by eye. Add a linear fit per hydroperiod with geom_smooth(method = "lm"). Does the line change your reading of the plot? What would you need to know before trusting it?
Panel (b) of 6.5 shows proportions without uncertainty. Add an error bar of \(\pm 2\) standard errors to each bar, with \(\text{SE} = \sqrt{p(1-p)/n}\) and errorbars!. Do the regions still look different?
Redo Figure 6.5 with one colour per region in every panel and a single shared legend on the right (Legend(fig[1, 4], ...)).
Danisch, Simon, and Julius Krumbiegel. 2021. “Makie.jl: Flexible high-performance data visualization for Julia.”Journal of Open Source Software 6 (65): 3349. https://doi.org/10.21105/joss.03349.