While quite complex compositions can be achieved using
+
, |
, and /
, it may be necessary
to take even more control over the layout. All of this can be controlled
using the plot_layout()
function along with a couple of
special placeholder objects. We’ll use our well-known mtcars plots to
show the different options.
library(ggplot2)
p1 <- ggplot(mtcars) +
geom_point(aes(mpg, disp)) +
ggtitle('Plot 1')
p2 <- ggplot(mtcars) +
geom_boxplot(aes(gear, disp, group = gear)) +
ggtitle('Plot 2')
p3 <- ggplot(mtcars) +
geom_point(aes(hp, wt, colour = mpg)) +
ggtitle('Plot 3')
p4 <- ggplot(mtcars) +
geom_bar(aes(gear)) +
facet_wrap(~cyl) +
ggtitle('Plot 4')
Adding an empty area
Sometimes all that is needed is to have an empty area in between
plots. This can be done by adding a plot_spacer()
. It will
occupy a grid cell in the same way a plot without any outer elements
(titles, ticks, strips, etc.):
p1 + plot_spacer() + p2 + plot_spacer() + p3 + plot_spacer()
It is important to understand that the added area only corresponds to the size of a plot panel. This means that spacers in separate nesting levels may have different dimensions:
(p1 + plot_spacer() + p2) / (plot_spacer() + p3 + plot_spacer())
Controlling the grid
If nothing is given, patchwork will try to make a grid as square as
possible, erring to the side of a horizontal grid if a square is not
possible (it uses the same heuristic as facet_wrap()
in
ggplot2). Further, each column and row in the grid will take up the same
space. Both of these can be controlled with
plot_layout()
p1 + p2 + p3 + p4 +
plot_layout(ncol = 3)
p1 + p2 + p3 + p4 +
plot_layout(widths = c(2, 1))
When grid sizes are given as a numeric, it will define the relative sizing of the panels. In the above, the panel area of the first column is twice that of the second column. It is also possible to supply a unit vector instead:
p1 + p2 + p3 + p4 +
plot_layout(widths = c(2, 1), heights = unit(c(5, 1), c('cm', 'null')))
In the last example the first row will always occupy 5cm, while the second will expand to fill the remaining area.
It is important to remember that sizing only affects the plotting region (panel area). If a plot has, e.g., very wide y-axis text it will not be penalized and get a smaller overall plotting region.
Moving beyond the grid
Earlier, when we’ve wanted to create non-grid compositions, we’ve used nesting. While this is often enough, you end up losing the alignment between subplots from different nested areas. An alternative is to define a layout design to fill the plots into. Such a design can be defined in two different ways. The easiest is to use a textual representation:
layout <- "
##BBBB
AACCDD
##CCDD
"
p1 + p2 + p3 + p4 +
plot_layout(design = layout)
When using the textual representation it is your responsibility to
make sure that each area is rectangular. The only exception is
#
which denotes empty areas and can thus be of any
shape.
A more programmatic approach is to build up the layout using the
area()
constructor. It is a bit more verbose but easier to
program with. Further, this allows you to overlay plots.
layout <- c(
area(t = 2, l = 1, b = 5, r = 4),
area(t = 1, l = 3, b = 3, r = 5)
)
p1 + p2 +
plot_layout(design = layout)
The design specification can of course also be used with
widths
and heights
to specify the size of the
columns and rows in the design.
A small additional feature of the design
argument exists
if used in conjunction with wrap_plots()
function (See the
Plot Assembly guide). If the design is given
as a textual representation, you can name the plots to match them to the
different areas, instead of letting them be filled in in the order they
appear:
layout <- '
A#B
#C#
D#E
'
wrap_plots(D = p1, C = p2, B = p3, design = layout)
Fixed aspect plots
A special case when it comes to assembling plots are when dealing
with fixed aspect plots, such as those created with
coord_fixed()
, coord_polar()
, and
coord_sf()
. It is not possible to simultaneously assign
even dimensions and align fixed aspect plots. The default value for the
widths
and heights
arguments in
plot_layout()
is NA
, which is treated
specially. In general it will behave just as 1null
(i.e. expand to fill available space but share evenly with other
1null
panels), but if the row/column is occupied by a fixed
aspect plot it will expand and contract in order to keep the aspect of
the plot and may thus not have the same dimension as grid width/heights
containing plots without fixed aspect plots. If you need to mix this
behavior with fixed dimensions you can use the special value of
-1null
which behaves the same as NA
(unit
vectors doesn’t allow NA
values).
width = unit(c(1, 3, -1), c("null", "cm", "null")
would
specify one column that fills out available space, one column fixed to 3
cm, and one column that expands to match a fixed aspect ratio if needed
or otherwise takes the same size as the first column.
p_fixed <- ggplot(mtcars) +
geom_point(aes(hp, disp)) +
ggtitle('Plot F') +
coord_fixed()
p_fixed + p1 + p2 + p3
Contrast this with setting the widths to a non-NA value:
p_fixed + p1 + p2 + p3 + plot_layout(widths = 1)
As you can see, the fixed aspect plot keeps its aspect ratio, but loses the axis alignment in one of the directions. Which solution is needed is probably dependent on the specific use case. The default optimizes the use of space.
There are some restrictions to the space optimization. The fixed aspect plot cannot take up multiple rows or columns, and if one fixed aspect plot conflicts with another one, one of them will end up not using the full space.
Avoiding alignment
Patchwork is designed to do it’s utmost to align the plotting areas,
and while this is generally sensible in order to create a calm and good
looking layout it sometimes gets in the way of creating what you want.
Below we will see two such cases. In both situation the answer is the
wrap_elements()
function.
Huge axis text causing white space.
Sometimes one of the plots has much longer axis text or axis labels than the other:
p2mod <- p2 + labs(x = "This is such a long\nand important label that\nit has to span many lines")
p1 | p2mod
Depending on what you prefer you might want the leftmost plot to fill
out as much room as possible instead of being aligned to the rightmost
panel. Putting a ggplot or a patchwork inside free()
removes any alignment from the plot.
free(p1) | p2mod
Designs don’t keep an even width
When creating a complex layout using a design you may create setups where plots spans the edge of other plots and be surprised how that affects the width of the panels in the design:
design <- "#AAAA#
#AAAA#
BBCCDD
EEFFGG"
p3 + p2 + p1 + p4 + p4 + p1 + p2 +
plot_layout(design = design)
In the above we see that the 3 bottom columns all have different
widths despite being given the same amount of space in the design
matrix. The reason for this is that the top plot has a y-axis to the
left and a guide to the right, and those fall in between the two columns
that make up the 1st and 3rd column in the bottom. Subsequently those
plots are expanded to fill out the space. Once again free()
will save us from this:
free(p3) + p2 + p1 + p4 + p4 + p1 + p2 +
plot_layout(design = design)
More freeing
free()
does more than just un-align the whole panel. You
can use the side
argument to control which sides to not
align. Below we instruct patchwork to free the left side with the
ridiculous amount of significant digits, but keep the remaining sides
aligned
p3_very_precise <- p3 +
scale_y_continuous(labels = \(x) format(as.numeric(x), nsmall = 10))
p1 / free(p3_very_precise, side = "l")
free()
also has two other modes besides the default. One
is to free the alignment of axis labels so they stick to their axis:
free(p1, type = "label") /
p3_very_precise
The other is to free content outside of the panel from claiming space. For instance, in the below plot, there is no need for the top plot to claim space for it’s wide y-axis as it has a lot of empty space to the left:
plot_spacer() + p3_very_precise +
p1 + p2
Using type = "space"
will fix this:
plot_spacer() + free(p3_very_precise, type = "space", side = "l") +
p1 + p2
Insets
An alternative to placing plots in a grid is to place a plot as an
inset in another plot. As we saw above, this is achievable with a
setting up a layout with multiple overlapping area()
specifications. However, this approach still uses an underlying grid,
which may be constraining. Another approach is to use the
inset_element()
function which marks a plot or graphic
object to be placed as an inset on the previous plot. It will thus not
take up a slot in the provided layout, but share the slot with the
previous plot. inset_element()
allows you to freely
position your inset relative to either the panel, plot, or full area of
the previous plot, by specifying the location of the left, bottom,
right, and top edge of the inset.
p1 + inset_element(p2, left = 0.6, bottom = 0.6, right = 1, top = 1)
p1 + inset_element(p2, left = 0, bottom = 0.6, right = 0.4, top = 1, align_to = 'full')
The location can be specified either as numerics like above or as
grid units. If specified as numerics they will be converted to
'npc'
units which goes from 0 to 1 in the specified area.
As an example of this we will place an inset exactly 1 cm from the panel
border in the code below:
p1 + inset_element(
p2,
left = 0.5,
bottom = 0.5,
right = unit(1, 'npc') - unit(1, 'cm'),
top = unit(1, 'npc') - unit(1, 'cm')
)
Further, inset_element()
allows you to place the inset
below the previous plot, should you choose to, and controlling clipping
and tagging in the same way as wrap_elements()
.
p3 + inset_element(p1, left = 0.5, bottom = 0, right = 1, top = 0.5,
on_top = FALSE, align_to = 'full')
Controlling guides
Plots often have guides to help viewers deduce the aesthetic mapping.
When composing plots we need to think about what to do about these. The
default behavior is to leave these alone and simply let them follow the
plot around. Examples of this can be seen above where the color guide is
always positioned beside Plot 3. Such behavior is fine if the purpose is
simply to position a bunch of otherwise unrelated plots next to each
other. If the purpose of the composition is to create a new tightly
coupled visualization, the presence of guides in between the plots can
be jarring, though. The plot_layout()
function provides a
guides
argument that controls how guides should be treated.
To show how it works, let’s see what happens if we set it to
'collect'
:
p1 + p2 + p3 + p4 +
plot_layout(guides = 'collect')
We can see that the guide has been hoisted up and placed beside all
the plots. The alternative to 'collect'
is
'keep'
, which makes sure that guides are kept next to their
plot. The default value is 'auto'
, which is sort of in
between. It will not collect guides, but if the patchwork is nested
inside another patchwork and that patchwork collect guides, it is
allowed to hoist them up. To see this in effect, compare the two
plots:
((p2 / p3 + plot_layout(guides = 'auto')) | p1) + plot_layout(guides = 'collect')
((p2 / p3 + plot_layout(guides = 'keep')) | p1) + plot_layout(guides = 'collect')
The guide collection has another trick up its sleeve. If multiple plots provide the same guide, you don’t want to have it show up multiple times. When collecting guides it will compare them with each other and remove duplicates.
p1a <- ggplot(mtcars) +
geom_point(aes(mpg, disp, colour = mpg, size = wt)) +
ggtitle('Plot 1a')
p1a | (p2 / p3)
(p1a | (p2 / p3)) + plot_layout(guides = 'collect')
Guides are compared by their graphical representation, not by their declarative specification. This means that different theming among plots may mean that two guides showing the same information is not merged.
Now and then you end up with an empty area in you grid. Instead of
leaving it empty, you can specify it as a place to put the collected
guides, using the guide_area()
placeholder. It works much
the same as plot_spacer()
, and if guides are not collected
it will do exactly the same. But if guides are collected they will be
placed there instead of where the theme tells it to.
p1 + p2 + p3 + guide_area() +
plot_layout(guides = 'collect')
Guide areas are only accessible on the same nesting level. If a nested patchwork has a guide area it will not be possible to place guides collected at a higher level there.
Controlling Axes
Much like the legends above, axes can sometimes be shared between
plots. However, this only makes sense if the plots are positioned
besides each other on top of having the exact same axis. patchwork
provides two arguments to plot_layout()
for controlling
axes and they work much like the guides
argument except
they don’t allow recursing into nested patchworks (for obvious
reasons).
In the plot below the y-axis is redundant and could be kept to the side like we are used to from faceted plots:
p1 + p2
This is easily fixed with the axes
argument
p1 + p2 + plot_layout(axes = "collect")
By default the axis title follows the collecting setting for the axis. We could also only collect the titles
p1 + p2 + plot_layout(axis_titles = "collect")
This makes a lot of sense if the axes doesn’t match exactly but still share the same title. Further, title collecting also work in the other direction:
p1 / p2 + plot_layout(axis_titles = "collect")
In the above we see that the y-axis title now only appears once and is positioned centrally.
Want more?
This is all for layouts. There is still more to patchwork. Explore the other guides to learn about annotations, special operators and multipage alignment.