So you want to style your plot?
Diligently, you have read, cleaned and modelled your data. You have carefully crafted a plot that lets your data speak its story. Now it is time to polish. Now it is time to let your visualisation shine.
We will set out to illuminate how to set the stylistic finishing touches on your visualisations made with the ggplot2 package. The ggplot2 package has had a recent release that included some relevant changes to styling plots. In ggplot2, the theme system is responsible for many non-data aspects of how your plot looks. It covers anything from panels, to axes, titles and legends. Here, we’ll get started with digesting important parts of the theme system. We’ll start with complete themes, get into theme elements followed by how these elements are used in various parts of the plot and finish off with some tips, including how to write your own theme functions. Before we begin discussing themes, let’s make an example plot that can showcase many aspects.
library(ggplot2)
p <- ggplot(mpg, aes(displ, hwy, colour = cty, shape = drv)) +
geom_point() +
facet_grid(~ year) +
labs(
title = "Fuel efficiency",
subtitle = paste0("Described for ", nrow(mpg), " cars from 1999 and 2008"),
caption = "Source: U.S. Environmental Protection Agency",
x = "Engine Displacement",
y = "Highway miles per gallon",
colour = "City miles\nper gallon",
shape = "Drive train"
)
p
If you haven’t already accidentally triggered it, feel free to hover your mouse over the plot above. Hovering will tell you what theme element you are pointing at.
What is a theme?
In ggplot, a theme is a list of descriptions for various parts of the plot. It is where you can set the size of your titles, the colours of your panels, the thickness of your grid lines and placement of your legends.
Themes are declared using the
theme()
function, which populates these descriptions called ‘theme elements’. Some of these elements have a predefined set of properties and can be set using the element functions, like
element_text()
. Other theme elements can take simpler values like strings, numbers or units.
Some pre-arranged collections of elements can be found in complete themes, like the iconic
theme_gray()
. These are convenient ways to quickly swap out the complete look of a plot.
Complete themes
Let’s start big and work our way through the more nitty-gritty aspects of theming plots. The most thorough way to change the styling of a single plot is to swap out the complete theme. You can do this simply by adding one of the theme_*()
functions, like
theme_minimal()
.
p + theme_minimal()
Built-in themes
The base ggplot2 package already comes with a series of 9 built-in complete themes. For the sake of completeness about complete themes, they are displayed in the fold-out sections below. You can peruse them at your leisure to help you pick one you might like.
theme_grey()
(default)
p + theme_grey()
theme_bw()
p + theme_bw()
theme_linedraw()
p + theme_linedraw()
theme_light()
p + theme_light()
theme_dark()
p + theme_dark()
theme_minimal()
p + theme_minimal()
theme_classic()
p + theme_classic()
theme_void()
p + theme_void()
theme_test()
p + theme_test()
Additional themes
Some packages come with their own themes that you can add to your plots. For example the cowplot package has a theme that galvanises you to not use labels that are too small, and otherwise has a clean look.
cowplot::theme_cowplot()
p + cowplot::theme_cowplot()
The ggthemes package hosts themes that reflect other popular venues of data visualisation, such as the economist or FiveThirtyEight.
ggthemes::theme_fivethirtyeight()
p + ggthemes::theme_fivethirtyeight()
If the moods strikes you for a more playful plot, you can use the tvthemes package to style your plot according to TV shows!
tvthemes::theme_simpsons()
p + tvthemes::theme_simpsons()
Aside from these packages that live on CRAN, there are also non-CRAN packages that come with complete themes. You can visit the extension gallery and filter on the ‘themes’ tag to find more packages.
Tweaking complete themes
The complete themes have arguments that affect multiple components across the plot. Perhaps the most well known is the base_size
argument that globally controls the size of theme elements, ranging from the text sizes, to line widths, and —since recently— even point sizes.
p + theme_bw(base_size = 8)
A technique used to distinguish visual hierarchy is ‘font pairing’, meaning that you combine more than one font to convey visual hierarchy. In web design, it means displaying your headers different from your body text. In data visualisation, it can mean displaying your titles distinctly from labels. The most common pairing, and the default one baked into ggplot2, is to display titles larger than labels in the same typeface. Another popular choice is to use different weights, like ‘bold’ and ‘plain’. It is now also easier to use different typefaces by pairing the header_family
and the base_family
fonts together. In the example below, we pair a serif font for headers and a sans-serif font for the rest.
p + theme_bw(base_family = "Roboto", header_family = "Roboto Slab")
A recent addition to styling with complete themes are colour choices. The ink
argument roughly amounts to the colour for all foreground elements, like text, lines and points. This is complemented by the paper
argument, which affect background elements like the panels and plot background. Lastly, there is an accent
argument which controls the display of a few specific layers, like
geom_smooth()
or
geom_contour()
. For some aspects of the plot, the ink
and paper
arguments are mixed to produce intermediate colours. As an example, when we use
theme_bw()
, the strip fill colour is a mix between the foreground and background to slightly lift this part from the background. The ink
and paper
arguments can also be used to quickly recolour a plot, or to convert a plot to ‘dark mode’ by using a light ink
and dark paper
.
p +
# Turning off these aesthetics to prevent grouping
aes(shape = NULL, colour = NULL) +
geom_smooth(method = "lm", formula = y ~ x) +
theme_bw(
ink = "#BBBBBB",
paper = "#333333",
accent = "red"
)
Theme elements
Rather than swapping out complete themes in one fell swoop, themes can also be tweaked to various degrees. In ggplot2, themes are a collection of theme elements, where an element describes a property, or set of properties, for a part of the theme.
Element functions
The documentation in ?theme()
will tell you what type of input each theme element will expect. Some theme elements just expect scalar values and not collections of properties. You can simply set these in the theme directly. For example, we all know that the golden ratio is the best ratio, so we can use it in our plot as follows:
In the cases where a cohesive set of properties serves as a theme element, ggplot2 has element_*()
functions. One of the simpler elements is
element_line()
and we can declare a new set of line properties as follows:
red_line <- element_line(colour = "red", linewidth = 2)
red_line
#> <ggplot2::element_line>
#> @ colour : chr "red"
#> @ linewidth : num 2
#> @ linetype : NULL
#> @ lineend : NULL
#> @ linejoin : NULL
#> @ arrow : logi FALSE
#> @ arrow.fill : chr "red"
#> @ inherit.blank: logi FALSE
These elements can then be given to the
theme()
function to assign these properties to a specific part of the theme, like the axis.line
in this example.
p + theme(axis.line = red_line)
Below is an overview of elements and some common places where they are used:
Element | Description |
---|---|
element_blank() |
Indicator to skip drawing an element. |
element_line() |
Used for axis lines, grid lines and tick marks. |
element_rect() |
Used for (panel) backgrounds, borders and strips. |
element_text() |
Used for (sub)titles, labels, captions. |
element_geom() |
Used to set default properties of layers. |
element_polygon() |
Not used, but provided for reasons of extension. |
element_point() |
Not used, but provided for reasons of extension. |
In addition to these elements in ggplot2, extension packages can also define custom elements. Generally speaking, these elements are variants of the elements listed above and often have slightly different properties and are rendered differently. For example
marquee::element_marquee()
is a subclass of
element_text()
, but interprets the provided text as markdown. It applies some formatting like **
for bold, or allows for custom spans like {.red ...}
. Another example is
ggh4x::element_part_rect()
that can draw a subset of rectangle borders.
p +
labs(title = "**Fuel** {.red efficiency}") +
theme(
plot.title = marquee::element_marquee(),
strip.background = ggh4x::element_part_rect(colour = "black", side = "b")
)
Hierarchy and inheritance
Most theme elements are hierarchical. At the root, they are broadly applicable and change large parts of the plot. At leaves, they are very specific and allow fine grained control. Travelling from roots to leaves, properties of theme elements are inherited from parent to child. Some inheritance is very direct, where leaves directly inherit from roots (for example legend.text
). Other times, inheritance is more arduous, like for axis.minor.ticks.y.left
: it inherits from axis.ticks.y.left
, which inherits from axis.ticks.y
, which inherits from axis.ticks
, which finally inherits from line
. Most often, elements only have a single parent, but there are subtle exceptions.
In the example below we set the root text
element to red text. This is applied (almost) universally to all text in the plot. We also set the font of the leaf legend.text
element. We see that not only has the legend text font changed, but it is red as well because of the root text
element.
p + theme(
# A root element
text = element_text(colour = "red"),
# A leaf element
legend.text = element_text(family = "impact")
)
However, the keen eye spots that the strip text and axis text are not red. This is because in the line of succession, an ancestor declared a different colour property for the text, which overrules the colour property descending from the root text
element. In these specific cases, the deviating ancestors are axis.text
and strip.text
.
When we inspect the contents of a theme element, we may find that the elements are NULL
. This is simply an indicator that this element will inherit from its ancestor in toto. Another possibility is that some properties of an element are NULL
. A NULL
property means that the property will be inherited from the parent. When we truly want to know what properties are taken to display a theme element, we can use the
calc_element()
function to resolve the inheritance and populate all the fields.
# Will inherit entirely from parent
theme_gray()$axis.ticks.x.bottom
#> NULL
# The element is incomplete
theme_gray()$axis.ticks
#> <ggplot2::element_line>
#> @ colour : chr "#333333FF"
#> @ linewidth : NULL
#> @ linetype : NULL
#> @ lineend : NULL
#> @ linejoin : NULL
#> @ arrow : logi FALSE
#> @ arrow.fill : chr "#333333FF"
#> @ inherit.blank: logi TRUE
# Proper way to access the properties of an element
calc_element("axis.ticks.x.bottom", theme_gray())
#> <ggplot2::element_line>
#> @ colour : chr "#333333FF"
#> @ linewidth : num 0.5
#> @ linetype : num 1
#> @ lineend : chr "butt"
#> @ linejoin : chr "round"
#> @ arrow : logi FALSE
#> @ arrow.fill : chr "#333333FF"
#> @ inherit.blank: logi TRUE
The
?theme
documentation often tells you how the elements inherit and
calc_element()
will resolve it for you. If, for some reason, you need programmatic access to the inheritance tree, you can use
get_element_tree()
. Let’s say you want to find out exactly which elements have multiple parents. The resulting object is the internal structure ggplot2 uses to resolve inheritance and has an inherit
field for every element that discerns its direct parent.
tree <- get_element_tree()
tree$axis.line.x.bottom$inherit
#> [1] "axis.line.x"
Anatomy of a theme
The
theme()
function has a lot of arguments and can be a bit overwhelming to parse in one take. At the time of writing, it has 147 arguments and ...
is obfuscating additional options. Because we like structure rather than chaos, let us try to digest the
theme()
function one bite at a time. Much of the theme has been divided over parts in the theme_sub_*()
family of functions. This family are just simple shortcuts. For example the theme_sub_axis(title)
argument, populates the axis.title
element.
theme_sub_axis(title = element_blank())
#> <theme> List of 1
#> $ axis.title: <ggplot2::element_blank>
#> @ complete: logi FALSE
#> @ validate: logi TRUE
If you’re redefining a series of related settings, it can be beneficial to use the theme_sub_*()
. One benefit is brevity. For example, if you want to tweak the left y-axis a lot, it can be terser to use theme_sub_axis_left(title, text, ticks)
rather than theme(axis.title.y.left, axis.text.y.left, axis.ticks.y.left)
. The second benefit is that it helps organising your theme, preserving a shred of sanity while hatching your plots.
Whole plot
There are a series of mostly textual theme elements that mostly display outside the plot itself. Using the
theme_sub_plot()
function, we can omit the plot
prefix in the settings. We can us it to control the background, as well as the titles, caption and tag text and their placement. In the plot below, we’re tweaking these settings to show the scope. Note that the text (except for the tag) is now aligned across the plot as a whole, rather than aligned with the panels.
p +
labs(tag = "A") +
theme_sub_plot(
# Adjust the background colour
background = element_rect(fill = "cornsilk"),
# Align title and subtitle to plot instead of panels
title = element_text(hjust = 0), # default,
subtitle = element_text(colour = "dodgerblue"),
title.position = "plot",
# Align caption to plot instead of panels
caption = element_text(hjust = 1), # default
caption.position = "plot",
# Place the tag in the top right of the panels instead of top left of plot
tag.position = "topright",
tag.location = "panel"
)
Panels
An important aspect of the panels are the grid lines. The grid lines follow the major and minor breaks of the scale, which is also the major distinction in how they are displayed. The next distinction is whether the lines are horizontal and mark breaks vertically (y
) or the lines are vertical and mark breaks horizontally (x
).
p +
theme_sub_panel(
# Extra space between panels
spacing.x = unit(1, "cm"),
# Tweaking all the grid elements
grid = element_line(colour = "grey80"),
# Turning off the minor grid elements
grid.minor = element_blank(),
# Tweak the major x/y lines separately
grid.major.x = element_line(linetype = "dotted"),
grid.major.y = element_line(colour = "white")
)
Besides grid lines, also the border and the background are important for the panel styling. They can be confusing because they are similar, but not identical. Notably, the panel background is underneath the data (unless ontop = TRUE
), while the panel border is on top of the panel. You can see this in the plot below, because the white grid lines are visible over the blue background, but not over the red border.
p +
theme_sub_panel(
background = element_rect(fill = "cornsilk", colour = "blue", linewidth = 6),
border = element_rect(colour = "red", linewidth = 3, fill = "black"),
)
Both the background and the border are clipped by the coordinate systems clipping setting, e.g. coord_cartesian(clip)
. It should also be noted that any fill
property set on the border is ignored. Moreover, the legend key background takes on the appearance of the panel background by default, which is why the ‘Drive train’ legend is affected too.
A recent improvement is also that we can set the panel size via the theme. The panel.widths
and panel.heights
arguments take a unit (vector) and set the panels to this size. If you are trying to coordinate panel sizes with
ggsave()
, please mind that other plot components, like axes, titles and legends also take up additional space. If you have more than one panel in the vertical or horizontal direction, you can use a vector of units as demonstrated below for widths
.
p +
theme_sub_panel(
widths = unit(c(3, 5), "cm"),
heights = unit(4, "cm")
)
It is also possible to set the total size of panels. In the example above we can use widths = unit(c(3, 3), "cm")
to have each panel be 3 centimetres wide, separated by a gap determined by the panel.spacing.x
setting. If we instead had used widths = unit(6, "cm")
each panel would be smaller than 3 centimetres because the panel.spacing.x
is included.
Strips
The display text in strips is formatted by the labeller
argument in the facets. Styling this piece of text can be done with the
theme_sub_strip()
function, which replaces the strip
prefix in
theme()
. Similar to axes, strips also have positional variants with background.x
and background.y
specifying the backgrounds for horizontal and vertical strips specifically.
The text even has specific text.x.bottom
, text.x.top
, text.y.left
and text.y.right
variants. This allows text on the left to be rotated 90°, while text on the right is rotated -90°, which gives the sense that the text faces the panels. Out of principle, you could force the text.x.bottom
to be rotated 180° to achieve the same sense for horizontal text, but you may find out why readability trumps consistency.
Another important distinction is the placement
option, which affects how strips are displayed when they clash with axes. This author personally thinks that placement = "outside"
is the wiser choice 99% of the time. When strips are displayed outside of axes, the switch.pad.grid
/switch.pad.wrap
elements control the spacing.
# We're including a labeller to showcase formatting
my_labeller <- as_labeller(c(`1999` = "The Nineties", `2008` = "The Noughties",
V = "Vertical Strip"))
p +
# Using a dummy strip for the vertical direction
facet_grid("V" ~ year, labeller = my_labeller, switch = "x") +
theme_sub_strip(
# All strip backgrounds
background = element_rect(fill = "cornsilk"),
# Specifically the horizontal strips
background.x = element_rect(colour = "black", linewidth = 1),
# Tweak text, specifically for the bottom strip
text.x.bottom = element_text(size = 16),
placement = "outside",
# Spacing in between axes and strips. Note that it doesn't affect the
# vertical strip that doesn't have an axis.
switch.pad.grid = unit(1, "cm"),
clip = "off"
)
The clip = "on"
setting is the default and causes the strip border to be flush with the panel borders. By turning the clipping off, the strip border bleeds out, but it also allows text to exceed the boundaries.
Axes
Perhaps the most involved theme elements are the axis elements. They have the longest chain of inheritance of all elements and have variants for every side of the plot.
Let’s start from the top and work our way down. The
theme_sub_axis()
function lets you tweak all the axes at once. Note that the axis line now appears in the left and bottom axes.
# Turn on all lines
p + theme_sub_axis(line = element_line())
To control the directions separately, you can use the
theme_sub_axis_x()
and
theme_sub_axis_y()
functions.
p +
# Turn on horizontal line
theme_sub_axis_x(line = element_line()) +
# Turn off ticks for vertical
theme_sub_axis_y(ticks = element_blank())
If you are dealing with secondary axes, or you have placed your primary axes in unorthodox positions, you might find use in the even more granular theme_sub_axis_*()
functions for the top, left, bottom and right positions.
p +
# Extra axes
guides(x.sec = "axis", y.sec = "axis") +
# Turning off ticks
theme_sub_axis_bottom(ticks = element_blank()) +
# Extra long, coloured ticks
theme_sub_axis_top(
ticks.length = unit(5, "mm"),
ticks = element_line(colour = "red")
) +
# Extra spacing
theme_sub_axis_left(text = element_text(margin = margin_auto(10))) +
# Turning on the axis line
theme_sub_axis_right(line = element_line())
In addition to being globally controlled by the theme, axes are guides that can also be locally controlled by their guide_axis(theme)
argument. The same theme elements apply, but they are accessed from the local theme that masks the global theme. Note that besides from the colour changing, there is now also an axis line because the local
theme_classic()
draws axis lines.
red_axis <- guide_axis(theme = theme_classic(ink = "red"))
p + guides(x = red_axis)
Legend
While the legend inheritance is typically straightforward, it can be a challenge to get these right. To chop this problem in smaller pieces, we can separate the so called ‘guide box’ from the legend guides themselves.
Guide box
The guide box is a container for guides and is responsible for the placement and arrangement of its contents.
p +
theme_sub_legend(
# Showing the box
box.background = element_rect(fill = "cornsilk"),
# Put legends on the left
position = "left",
# Arrange legends horizontally
box = "horizontal",
# Align to legend box to top
justification = "top",
# location = "plot",
# But align legends within the box at the bottom
box.just = "bottom",
# Spacings and margins
box.margin = margin_auto(5),
box.spacing = unit(1, "cm")
)
Legend boxes can be split up by manually specifying the position
argument in guides. You cannot tweak every box setting for every position independently. However, the boxes can be justified individually.
p +
guides(shape = guide_legend(position = "left")) +
theme_sub_legend(
# Showing the boxes
box.background = element_rect(fill = "cornsilk"),
box.margin = margin_auto(5),
# Tweaking the justification per position
justification.left = "top",
justification.right = "bottom"
)
General legend guides
Moving on from guide boxes to the guides themselves; There are some theme settings that (almost) universally affect any guides, regardless of
guide_legend()
,
guide_colourbar()
, or
guide_bins()
. These settings pertain to the legend background, margins, labels and titles and their placement and key sizes.
p +
theme_sub_legend(
# Give guides a wider background
background = element_rect(fill = "cornsilk"),
margin = margin_auto(5, unit = "mm"),
# Display legend titles to the right of the guide
title = element_text(angle = 270),
title.position = "right",
# Display red labels to the left of the keys
text = element_text(colour = "red"),
text.position = "left",
# Set smaller keys
key.width = unit(5, "mm"),
key.height = unit(5, "mm")
)
Legend guide
There are also settings that affect
guide_legend()
but not
guide_colourbar()
. Most of these have to do with the arrangement of keys, like their spacing, justification or fill order (by row or column). The legend.key.justification
setting only matters when the text size exceeds the key size. If we remove that setting from the plot below, the keys will fill up to fit the space.
p +
# Set two columns and long label text
scale_shape_discrete(
labels = c("4\nwheel\ndrive", "front\nwheel\ndrive", "rear\nwheel\ndrive"),
guide = guide_legend(ncol = 2)
) +
theme_sub_legend(
# Fill items in grid in a row-wise fashion
byrow = TRUE,
# Increase spacing between keys
key.spacing.y = unit(5, "mm"),
key.spacing.x = unit(5, "mm"),
# Top-align keys with text
key.justification = "top"
)
Colourbar guide
Likewise, there are also settings specific to
guide_colourbar()
. Generally, you can see it as a legend guide with a single elongated key. This elongation has special behaviour in that the default is 5 times the original key size. If you need to set the size directly without special behaviour, you can use the guide_colourbar(theme)
argument. Aside from the special size behaviour, we can also set the colourbar frame and ticks.
p +
# Using a local guide theme to directly set the size
guides(colour = guide_colourbar(theme = theme(legend.key.height = unit(5, "cm")))) +
theme_sub_legend(
frame = element_rect(colour = "red"),
# Long blue ticks
ticks = element_line(colour = "blue"),
ticks.length = unit(-5, "mm"),
# Adapt margins to accommodate longer ticks
text = element_text(margin = margin(l = 6, unit = "mm")),
margin = margin(l = 6, unit = "mm")
)
A trick you can pull to have legends eat up all the available real estate, is to give them "null"
-unit size. Below, that trick stretches the colourbar across the full width of the plot.
p +
guides(colour = guide_colourbar(
theme = theme_sub_legend(
key.width = unit(1, "null"),
title.position = "top",
margin = margin_auto(NA, 0) # remove left/right margins
),
position = "bottom"
))
Binned legend
A binned legend acts as a hybrid between a typical legend guide and a colourbar. It depicts a discretised continuous (binned) legend, by properly displaying separate glyphs, but also displaying an axis with ticks at bin breaks.
p +
guides(colour = "bins") +
theme_sub_legend(
axis.line = element_line("red"),
ticks = element_line("blue")
)
Layers
Since recently we can also set default choices for layer aesthetics via the theme. We briefly saw this foreshadowed in the ‘tweaking complete themes’ section. But you can have more granular control over layers as well, without affecting the entirety of the theme.
Introducing the ‘geom’ element
The new theme element powering all this is the geom
argument. It takes the return value of the
element_geom()
function to control the default graphical properties of layers.
p +
# Turn off grouping
aes(colour = NULL, shape = NULL) +
geom_smooth(formula = y ~ x, method = "lm") +
theme(
geom = element_geom(
ink = "tomato",
paper = "dodgerblue",
accent = "forestgreen"
)
)
The
element_geom()
function has a number of properties that we’re about to describe. Just like other element_*()
function, it returns an object with properties, most of which are NULL
by default. These NULL
properties will get filled in when the plot is built.
element_geom()
#> <ggplot2::element_geom>
#> @ ink : NULL
#> @ paper : NULL
#> @ accent : NULL
#> @ linewidth : NULL
#> @ borderwidth: NULL
#> @ linetype : NULL
#> @ bordertype : NULL
#> @ family : NULL
#> @ fontsize : NULL
#> @ pointsize : NULL
#> @ pointshape : NULL
#> @ colour : NULL
#> @ fill : NULL
Colours
There are 5 colour related settings. In the plot above, we’ve already met three of them.
ink
is the foreground colour.paper
is the background colour. It is often used in a mixture withink
to dull the foreground and coordinate with the rest of the theme. You can see for example that the ribbon part ofgeom_smooth()
is a bit purple-ish due to the mixture of reddishink
and bluishpaper
.accent
is a speciality colour pick that only a few geoms use as default. These aregeom_contour()
,geom_quantile()
andgeom_smooth()
.
The remaining two are well known to anyone who has worked with ggplot2 before: colour
and fill
. These two overrule any ink
/paper
/accent
setting to directly set colour and fill without any mixing. For example, notice that the ribbon is a (semitransparent) purple, rather than a mixture with green paper.
last_plot() +
theme(geom = element_geom(
fill = "purple",
colour = "orange",
paper = "green" # Ignored
))
Lines
There are also 4 different line settings. You may already be familiar with linewidth
and linetype
setting how wide lines are, and how they are drawn respectively. Additionally, we’re now also using borderwidth
and bordertype
to denote these settings for closed shapes that can be filled, like the rectangles below.
ggplot(faithful, aes(eruptions)) +
geom_histogram(aes(y = after_stat(density)), bins = 30, colour = "black") +
geom_line(stat = "density") +
theme(
geom = element_geom(
# Applies to the bars
borderwidth = 0.5,
bordertype = "dashed",
# Applies to the line
linewidth = 4,
linetype = "solid"
)
)
Points and text
The four remaining settings pertains to text and points. Respectively fontsize
and pointsize
control the size. pointshape
and family
control the shape and font family.
ggplot(mtcars, aes(mpg, disp, label = rownames(mtcars))) +
geom_point() +
geom_label(nudge_x = 0.25, hjust = 0) +
theme(
geom = element_geom(
# Point settings
pointsize = 8,
pointshape = "←",
# Text settings
fontsize = 8,
family = "Ink Free"
)
)
Micro-managing layers
Aside from globally affecting every layer via theme(geom)
, you can also fine-tune the appearance of individual geometry types. Whereas we envision element_geom(ink, paper)
as the global ‘aura’ of a plot, the element_geom(colour, fill)
is intended for tailoring specific geom types. We can add theme elements for specific geoms by replacing the snake_case layer function name by dot.case argument name. This works for layers that have an equivalent Geom ggproto class, which is the case for all geoms in ggplot2.
ggplot(mpg, aes(class, displ)) +
geom_boxplot(outliers = FALSE) +
geom_jitter() +
theme(
geom.point = element_geom(colour = "dodgerblue"),
geom.boxplot = element_geom(fill = "orchid", colour = "turquoise")
)
Macro-managing layers
There are now various options for how to change non-data parts of layers, and it can be a bit tricky to determine when you should use what option. Essentially, this is a 2-by-2 table covering the option of which layers to set (single, all) and when it is used (local, global).
-
If you want to change the look of a single layer in a single plot, you can just use the static (unmapped) aesthetics in a layer. For example:
geom_point(colour = "blue")
. -
If you want to change the look of a single layer in all plots, you can use
update_theme()
to globally set a new (micro-managed) option. For example:update_theme(geom.point = element_geom(colour = "blue"))
. You can also use theelement_geom(ink, paper)
settings but for single layers it may be more direct to useelement_geom(colour, fill)
instead. We no longer recommend, and even discourage (!) usingupdate_geom_defaults()
for this purpose. -
If you want to change the look of all layers in a single plot, you can use the
theme(geom)
argument and add it to a plot. For example:theme(geom = element_geom(ink = "blue"))
. -
If you want to change the look of all layers in all plots, you can also use
update_theme()
to globally set thegeom
option. For example:update_theme(geom = element_geom(ink = "blue"))
. Alternatively, you can also coordinate the entire theme by using for exampleset_theme(theme_gray(ink = "blue"))
.
Access from layers
Up to now, we’ve mostly described how to use the theme to instruct layers, but we can also instruct layers to lookup things from the theme too. Using the
from_theme()
function in aesthetics allows you to use expressions with the variables present in
element_geom()
. For example, if you want to use a darker variant of the accent
colour instead of ink
, you might want to write your mapping as follows:
p + aes(colour = from_theme(scales::col_darker(accent, 20)))
Palettes
In addition to controlling the default aesthetics from the theme, you can also control the default palettes from the theme. The palette theme settings all follow the following pattern, separated by dots: palette
, aesthetic, type. The type
can be either continuous
or discrete
. If you’re using the default binned scale, it takes the continuous palette. For example, if we want to change the default shape
and colour
palettes, we can declare that as follows:
p + theme(
palette.shape.discrete = c("plus", "triangle", "diamond"),
palette.colour.continuous = c("maroon", "hotpink", "white")
)
The values of these palette theme elements are passed down to
scales::as_discrete_pal()
and
scales::as_continuous_pal()
for discrete and continuous scales respectively.
Theme elements in extensions
Aside from extensions providing whole, complete themes, extensions may also define new theme elements. You can sometimes see these in facets, coords or guide extensions. With these wide use-cases, we cannot really describe these as much as just acknowledge they exist. For example, the ggforce package has a zoom element that controls the appearance of zooming indicators.
p + ggforce::facet_zoom(ylim = c(20, 30), xlim = c(3, 4)) +
theme(zoom = element_rect(colour = "red", linewidth = 0.2, fill = NA))
If you are writing your own extension and need to compute a bespoke element from the theme, you can use
register_theme_elements()
to ensure ggplot2 knows about your element and can use it in
calc_element()
.
# A custom element comes up empty
calc_element("my_element", complete_theme())
#> NULL
# Register element
register_theme_elements(
my_element = element_rect(),
element_tree = list(
my_element = el_def(
class = "element_rect", # Must be a rect element
inherit = "rect" # Get settings from theme(rect)
)
)
)
# Now custom element can be computed
calc_element("my_element", complete_theme())
#> <ggplot2::element_rect>
#> @ fill : chr "white"
#> @ colour : chr "black"
#> @ linewidth : num 0.5
#> @ linetype : num 1
#> @ linejoin : chr "round"
#> @ inherit.blank: logi TRUE
Writing your own theme
When you are writing your own theme there are a few things to keep in mind. A guiding principle is to write your themes such that it is robust to upstream changes. Not only can ggplot2 add, deprecate or reroute elements, also theme elements used by extensions should be accommodated.
1. Use a function
First, this principle means that you should write your theme as a function. Writing your theme as a function ensures it can be rebuild. This is opposed to assigning a theme object to a variable in your package’s namespace —or heaven forbid— save it as a file, If you assign your theme object to a variable in your namespace, the object will get compiled into your code and can cause build time warnings or errors if an element function or argument get updated.
my_theme <- function(...) {}
2. Use a base theme
Secondly, it is good practise to start your own theme as a function that calls a complete theme function as its base. It ensures that when ggplot2 adds new elements that belong in complete themes, your theme also remains complete.
my_theme <- function(...) {
theme_gray(...)
}
3. Use theme()
to add elements
Third, you should use
theme()
to add new elements to the base. While it is technically possible to assign additional elements by sub-assignment ($<-
), we strong advice against this. Using
theme()
ensures that any deprecated arguments are redirected to an appropriate place.
# Do *not* do the following!
my_fragile_theme <- function(...) {
t <- theme_gray(...)
t$legend.text <- element_text() # BAD
t
}
You can use + theme()
or %+replace% theme()
, where +
merges elements and %+replace%
replaces elements by completely removing old settings. If you use %+replace%
for a root element, like text
or line
, you should take care that every property has non-null values.
my_theme <- function(...) {
theme_gray(...) %+replace%
theme(
# Because we're replacing, we should fully define root elements
text = element_text(
family = "", face = "plain", colour = "red", size = 11,
hjust = 0.5, vjust = 0.5, angle = 0, lineheight = 1, margin = margin()
),
# Non-root elements can be partially defined
legend.text = element_text(colour = "blue")
) +
# Here we're updating the root line element with `+`, instead of replacing it
theme(line = element_line(linetype = "dotted"))
}
p + my_theme()
4. Caching themes
We mentioned in 1. that you shouldn’t assign a theme object to a variable in your namespace. However, you may want to reuse a theme without having to reconstruct it every time because you may never need to change arguments in your package. The solution we recommend for this use case, is to cache your theme when your package is loaded. It ensures that we observe all the formalities of building a theme, with all the protections this offers, but we need to do this only once per session.
# Create a variable for your future theme
cached_theme <- NULL
# In your .onLoad function, construct the theme
.onLoad <- function(libname, pkgname) {
cached_theme <<- my_theme()
}
# In your package's functions, you can now use the cached theme
my_plotting_function <- function() {
ggplot(mpg, aes(displ, hwy)) +
geom_point() +
cached_theme
}
# Simulate loading
.onLoad()
# Works!
my_plotting_function()
Tips and tricks
Global theme
Are you also used to writing entire booklets of theme settings at every plot? Do your fingers tire of typing panel.background = element_blank()
dozens of times in a script? Worry no more! Set your theme settings to permanent today by using the one-time offer of
set_theme()
!
my_theme <- function(...) {
theme_gray() +
theme(
panel.background = element_blank(),
panel.grid = element_line(colour = "grey95"),
palette.colour.continuous = "viridis"
)
}
set_theme(my_theme())
# Global goodness galore!
p
To undo any globally set theme, you can use
reset_theme_settings()
.
Fonts
Setting the typography of your plots is important and discussed more thoroughly in
this blog post. Here we’re simply giving the suggestion to use the
systemfonts::require_font()
when you are writing theme functions that include special fonts. It will not cover font behaviour for every graphics device, but it will for devices that use
systemfonts for finding fonts, like
ragg and
svglite.
my_theme <- function(header_family = "Impact", ...) {
systemfonts::require_font(header_family)
theme_gray(header_family = header_family, ...)
}
p + my_theme()
Bundling theme settings
Not every theme needs to be a complete theme. You can write partial themes that bundle together related settings to achieve an effect you want. For example, here are some settings that left-aligns the title and legend at the top of a plot.
upper_legend <- function() {
theme(
plot.title.position = "plot",
legend.location = "plot",
legend.position = "top",
legend.justification.top = "left",
legend.title.position = "top",
legend.margin = margin_part(l = 0)
)
}
p +
aes(colour = NULL) +
upper_legend()
Another example for bottom placement of colour bars:
bottom_colourbar <- function() {
theme_sub_legend(
position = "bottom",
title.position = "top",
justification.bottom = "left",
# Stretch bar across width of panels
key.width = unit(1, "null"),
margin = margin_part(l = 0, r = 0)
)
}
p +
aes(shape = NULL) +
bottom_colourbar()
If you don’t mind venturing outside the grammar for a brisk stroll, you can also bundle theme settings together with other components. For example, in a bar chart you may wish to suppress vertical grid lines and not expand the y-axis at the bottom.
barchart_settings <- function() {
list(
theme(panel.grid.major.x = element_blank()),
coord_cartesian(expand = c(bottom = FALSE))
)
}
ggplot(mpg, aes(class)) +
geom_bar() +
barchart_settings()
The point here is not to make an exhaustive list of all useful bundles, it is to highlight that it possible to create reusable chunks of theme.
Pattern rectangles
Did you know that element_rect(fill)
can be a grid pattern? You can use it to place images in the panel background, which can be neat for branding.
pattern <- "https://raw.githubusercontent.com/tidyverse/ggplot2/refs/heads/main/man/figures/logo.png" |>
magick::image_read() |>
grid::rasterGrob(
x = 0.8, y = 0.8,
width = unit(0.2, "snpc"),
height = unit(0.23, "snpc"),
) |>
grid::pattern(extend = "none")
p +
theme(
panel.background = element_rect(fill = pattern),
# legend.key inherits from panel background, so we tweak it
legend.key = element_blank(),
# make grid semitransparent to lay over pattern
panel.grid = element_line(colour = alpha("black", 0.05))
)
Finally
This article has been light on advice on how you should or should not use themes. Mostly, this is to encourage experimentation. Don’t be afraid to put in a personal twist. Make mistakes. Discover why a theme does or doesn’t work for a plot. If you cannot be bothered, there are extension packages that offer plenty of options. The tidytuesday project has spawned a rich source of varied plotting code, including themes people use. If you like a tidytuesday plot, find the source code and see how the sausage is made. Find whatever theme works for you and your plots.