Getting started with theme()
The theme()
function in {ggplot2} is awesome. Although it’s only one function, it gives you so much control over your final plot. theme()
allows us to generate a consistent, in-house style for our graphics, modify the text within our plots and more. Getting comfortable with theme()
will really take your {ggplot2} skills up a notch.
Normally, when people want help with an R function I tell them to use the built-in documentation about the function. This is normally done by typing ?function_name
into the console. It’s usually pretty informative and often enough to help people understand a new function.
So let’s try this with theme()
…
library("ggplot2")
?theme
You probably feel a bit like this now:
If you’re coding along with me you’ll be able to see there’s loads of arguments to theme()
. If you’re patient and like counting, you’ll find that there are ninety-nine arguments to the theme()
function.
Do you need to know all of these arguments? No.
Do I know all of these arguments? Also no.
What to we want to achieve?
By the end of this blog post, you are going to:
- become familiar with a handful of
theme()
arguments - be able to understand how to modify theme elements
- have built the confidence to try modifying aspects of a theme on your own
What I’m not aiming to do:
- construct a the world’s most elegant {ggplot2} theme
- show you every single thing that can be modified via
theme()
Basically, I want to give you the tools to make your plots look the way you want them to.
It’s also worth mentioning that this post is peppered with personal opinion; I want you to absorb how I’ve managed to implement my stylistic choices, not take these choices as the “truth”.
Building a basic plot
In order to modify a plot theme, we’re going to need a plot to start with. We’re going to work with a simple scatter plot derived from the Palmer Penguins data set. The data are freely available via the {palmerpenguins} package.
library("tidyr")
library("RColorBrewer") # pkg for nice colours
penguins = palmerpenguins::penguins
palette = brewer.pal(3, "Set2") # pick some nice colours
base_plot = penguins %>%
drop_na() %>% # remove missing values
ggplot(aes(x = body_mass_g, y = bill_length_mm, colour = species)) +
geom_point() +
scale_colour_manual(values = palette) +
ggtitle("Do heavier penguins have longer bills?") +
labs(colour = "Species") +
xlab("Body mass (g)") +
ylab("Bill length (mm)")
base_plot
We’ve created a basic scatter plot here. It’s perhaps one you’ve seen before. Perfectly functional, but lacks personality.
Using built in {ggplot2} themes
A good starting point for modifying your plot theme is actually to side-step theme()
and use one of the themes provided by {ggplot2}. The themes all have similar names: theme_*()
. I personally tend to start with theme_minimal()
, but feel free to try some others. For example, theme_classic()
or theme_light()
. The usage of these themes is super simple, just add it to your plot a bit like a geom_*()
:
plot = base_plot +
theme_minimal()
We’ve now got a plot which looks cleaner. There’s still some things that I don’t like. For example, I don’t like having my legend at the side of the plot and I don’t like the grid lines. We can modify these with theme()
.
Our first theme() modification
The first thing I like to do is move the legend to the bottom of the plot. This is where we start to use the theme()
function to modify our plot appearance. This one isn’t too tricky, we just specify (as a string) where we want our legend to sit.
plot +
theme(
legend.position = "bottom"
)
And here’s the result:
Okay, that wasn’t too bad. The other options here would be "top"
, "left"
or "right"
to modify the position, or "none"
to remove the legend entirely.
I’d like to you meet my friends: the element_*() functions
Most arguments of the theme()
function don’t take simple values like character or numeric values as arguments. A large number of the arguments take in a list of a specific class, where the list elements describe what the plot looks like. We can generate this list via an element_*()
function. The functions are element_blank()
, element_rect()
, element_line()
, element_text()
. Each of these functions has arguments which modify a given feature of our plot. I’ll run you through how each of the element_*()
functions might be used, but remember just like any other function ?element_*()
will show you more
Modifying line elements
Now our plot doesn’t have any lines on to indicate where the axes are. Suppose I want to make it clear where the axis lines are. Because these are, well, lines, I can use element_line()
to include them as so:
plot +
theme(
legend.position = "bottom",
axis.line = element_line(colour = "grey50")
)
obviously we all have different favourite colours, use whatever you like. Neutral colours (probably just shades of grey) are probably best for anything professional or for publication.
So here we’ve specified the colour of the line element which corresponds to axis.line
. We can change other characteristics here like linewidth
or linetype
, but I’ll leave that for you to experiment with later.
Removing plot elements
The next thing I want to modify is the grid lines. I personally don’t like them so I’m going to remove them. This could be done with element_line()
by matching the grid line colour to the background colour, but off the top of my head I don’t know what the background colour is, and there’s a much simpler solution: element_blank()
. This removes aspects of our plot by generating an empty list entry for that plot component.
plot +
theme(
legend.position = "bottom",
axis.line = element_line(colour = "grey50"),
panel.grid = element_blank()
)
assigning element_blank()
to panel.grid()
removes all grid lines. If we only wanted to remove the minor ones, we would set panel.grid.minor = element_blank()
. If we wanted to remove only the vertical lines (for whatever reason), we can set panel.grid.minor.x = element_blank()
and panel.grid.major.x = element_blank()
. Removing only the horizontal ones is the same, but swap x
for y
. This shows that although theme()
might have 99 arguments, there is structure to argument names which reduces how many you really need to remember.
Modifying text features
Text features are the next thing I’m going to change. We’re going to do two things at once here:
- change the font for all text features with the
text
argument - change the positioning and size of the plot title with the
plot.title
argument
we’ll adjust both of these via the element_text()
function
plot +
theme(
legend.position = "bottom",
axis.line = element_line(colour = "grey50"),
panel.grid = element_blank(),
text = element_text(family = "lato"), ## modify font
plot.title = element_text(hjust = 0.5, size = 18) ## modify positioning and size
)
the family
argument lets us specify the font: we’re using the lato font here. I won’t delve too much into fonts, but {showtext} is a package that makes using a wide variety of fonts straightforward. I’d definitely recommend playing with fonts if you’re looking to develop a theme for a corporate identity, or simply add some personality to a plot.
The hjust
argument controls the horizontal justification, essentially, the positioning of the text. hjust = 0
is left justification (the text is moved to the left), hjust = 1
is right justification (text moved to the right), and numbers between 0 and 1 will position the text somewhere between the far right and far left. Setting hjust = 0.5
centers the text for us. I’ve not used vjust
, but this argument adjusts the vertical justification. Note that on the y axis, hjust
moves the text along the axis, and vjust
moves the text closer to/further away from the y axis. The size
argument is just the font size in pts
, something we will all be familiar with. The result is that the text, especially the title, shines through a little more.
Borders and backgrounds
Borders and backgrounds are next on our list of things to modify. We use element_rect()
(rect as in rectangle) to change the styling of things such as the background around our legend or the entire plot background. By default, the legend background will be the same as the plot background, so it isn’t actually obvious that the legend even has a background! We’re going to modify the plot background here. The plot is currently sitting on a white background, and the web page you’re viewing also has a white background. This means that the plot melts into the webpage. This isn’t necessarily a bad thing, but you might want to frame your plot a bit by putting in on a coloured background. Alternatively, you might have a corporate slide deck with a coloured background, and want the plot to melt into this background.
plot +
theme(
legend.position = "bottom",
axis.line = element_line(colour = "grey50"),
panel.grid = element_blank(),
text = element_text(family = "lato"),
plot.title = element_text(hjust = 0.5, size = 18),
plot.background = element_rect(fill = "#dffffc", colour = "#dffffc")
)
The fill
argument here changes the actual plot background. The colour
argument (color
will also work if you’re u-averse) controls the colour of a thin border all the way around the plot. The colour #dffffc
is a very pale blue, which ensures that there is sufficient contract between the points and background of the plot. This is an important accessibility feature, so do think very carefully about how changing the background colour of your plot may impact the ability for other people to properly absorb the message you are trying to communicate. I’d generally avoid changing the background colour, but it’s useful here for demonstration purposes. To reiterate: if you do change the background colour, take care to ensure that accessibility is not compromised.
That brings to a close our introduction to each of the element_*()
functions. I know it was a bit traumatic before, but if you type ?theme
into your R console, you’ll notice that the help page tells you which element_*()
function you need to use for each theme()
argument.
Creating space
The last function that we use to modify aspects of a theme is margin()
. Margin is a little bit different to the element_*()
functions, instead of controlling colours, fonts and line types, margin()
lets us create or remove space around certain aspects of our theme by modify distances. If an argument needs to be modified with margin()
, it’s likely that the argument name looks like something.margin
. You may also notice that element_text()
has a margin argument - use margin()
here to create space around the text aspects of your plot.
There are 5 arguments to margin()
: t
, r
, b
, l
and unit
. t
, r
, b
and l
are short for top, right, bottom and left - to remember the order, it’s just clockwise from the top. You should assign a number to these arguments, then unit
is simply the units of these values. unit
defaults to pt
, which scales well with text, but you can choose something else if that makes more sense to you.
Let’s now modify the space between (a) the plot title and the scatterplot itself and also (b) the legend and the x axis.
plot +
theme(
legend.position = "bottom",
axis.line = element_line(colour = "grey50"),
panel.grid = element_blank(),
text = element_text(family = "lato"),
plot.title = element_text(
hjust = 0.5, size = 18, margin = margin(b = 30) # modify title-plot spacing
),
plot.background = element_rect(fill = "#dffffc", colour = "#dffffc"),
legend.margin = margin(t = 15) # modify x axis-legend spacing
)
Now this is our final plot! Each individual step made a minor adjustment to the plot, but added together, we have a plot with a much improved appearance.
Bringing it all together
An important programming concept is don’t repeat yourself (DRY). This applies to constructing graphics as well. We don’t want to copy and paste our theme for every plot we make. The great thing about {ggplot2} themes is that they can be effortlessly applied to basically any plot. All we have to do it turn out theme into a function, and then it can be used just like any of the “built in” {ggplot2} themes. For example:
my_theme = function(){
theme_minimal() +
theme(
legend.position = "bottom",
axis.line = element_line(colour = "grey50"),
panel.grid = element_blank(),
text = element_text(family = "lato"),
plot.title = element_text(
hjust = 0.5, size = 18, margin = margin(b = 30)
),
plot.background = element_rect(fill = "#dffffc", colour = "#dffffc"),
legend.margin = margin(t = 15)
)
}
notice that the first line of the function is theme_minimal()
, we used this theme as a starting point for our custom theme.
Then we can apply this to any other plot as we would apply a standard {ggplot2} theme:
my_boxplot = penguins %>%
drop_na() %>%
ggplot(aes(x = species, y = flipper_length_mm)) +
geom_boxplot(fill = "#ff9300") +
xlab("Species") +
ylab("Flipper length (mm)") +
ggtitle("Which type of penguin has the longest flippers?")
my_boxplot + my_theme()
That was easy! We can just add my_theme()
to all of our plots to ensure consistent styling. If we wanted to make minor adjustments to the theme for a specific plot, we can just add on the theme()
command again and make the required adjustments.
If you want to apply the style to all plots in a single script or report, theme_set()
is a really handy way to do this.
What next?
The theme()
function has a lot of arguments, and it can feel overwhelming if you’re wanting to start modifying your own theme. We’ve managed to gain a little bit of experience with the tools that modify a {ggplot2} theme, and now you should have the confidence to modify other elements on your own, and try out the different arguments in element_*()
functions.
If you’re wanting to explore what makes a really good chart, I’d recommend the RSS style guide for practical, actionable advice on constructing publication-ready graphics with examples in both R and Python. Cara Thompson gave the keynote talk at our 2023 edition of Shiny in Production; her talk walked us though 10 important considerations for making text shine within a data visualisation, and her slides are packed with with ways to make your plots awesome.
If you feel like going back to basics would really help you out, booking onto one of our upcoming Data Visualisation with {ggplot2} is a great way to get to grips with a wide variety of {ggplot2} features.