This handout accompanies the workshop given on September 4, 2019 at UGA’s DigiLab in the Main Library. There is some overlap with a blog post I did a couple years ago, but this is the first time this material has been presented in a workshop format. There is also a supplemental handout, which goes into more detail about ggplot2::theme. As always, please visit joeystanley.com/r for the latest materials.


1 Unncessary background story

A few years ago, I found myself developing a look and feel that I liked for my term papers, powerpoint slides, conference posters, and my website (including this handout!). I found a font and color scheme that I thought looked nice, and because it was unique, I thought it might turn into a branding of some sort.

The problem was the visuals I made in ggplot2 didn’t match at all. They didn’t use the same colors or font or anything like that. Even after I made some of the changes I’ve talked about in previous workshops, it was quite obvious that I just copy and pasted the plots right in the middle of my presentation or website and it they didn’t match. A plot for example might have looked like this:

You may find yourself in the same situation, whether it’s for academic output or because you want your plots to match your your business’ colors and general look.

Because ggplot2 has nearly infinite flexibility, you can customize your plot so that it matches whatever powerpoint theme you have, for example. But the 80-20 rule definitely applies here: 20% of my ggplot code was to produce the majority of the plot, and 80% of the code was the all the nitty-gritty changes I made to get it to match my aesthetic.

kelloggs_plot + 
    theme_bw(base_size = 12, base_family = "Iowan Old Style") + 
    theme(panel.background = element_blank(), 
          plot.background = element_rect(fill = "gray99"), 
          legend.background = element_rect(fill = "transparent"), 
          legend.key = element_rect(fill = "transparent"), 
          legend.justification = c(1, 1), 
          legend.position = c(1, 1), 
          plot.title = element_text(hjust = 0.5),
          plot.subtitle = element_text(hjust = 0.5))

Assuming the plot is rendering for you the way it is on my screen, the color and font should now match this handout and create a much more seamless integration. When it’s just one plot, it’s not a big deal, but when I found myself copying and pasting that code over and over, I decided I had to find a solution.

I did some digging and fortunately there is a solution! Create a custom theme in ggplot2! I can wrap all these functions up into a single new function and tag that to the end of my plots. Now, my code is much shorter, and I can feel confident that my plots are consistent every time. Furthermore, if I want to make changes to the plot, I just modify the code and then the changes propogate to all the plots when I rerun it. In other words, that large block of code above would be reduced down to just this:

kelloggs_plot + 
    theme_joey()

I hadn’t seen any other tutorial out there so I decided to write a quick blog post on the topic and it has consistently been my number 1 most visted page on my website ever since. By a long shot. So there seems to be a need for a good explanation for creating a custom theme in ggplot2.

That tutorial is almost three years old and I think it could use a little bit of a facelift. So this workshop serves as a new and improved version of that.

2 Data prep and sample plots

First off, let’s load ggplot2 before we get carried away.

library(ggplot2)

For this workshop, I’m going to use similar plots to the ones I used in previous workshops. I’ve got four different plots because when creating a theme, it’s good to try it out on several kinds of plots to make sure they all integrate well.

2.1 Amount of sugar per McDonald’s category

For some of the workshop, I’ll work with the McDonald’s menu items dataset, which you can access from my wesite. To simplify things a little bit, I’ll take a subset—just four of the nine categories. To make that subset, I’ll use the subset function.

menu <- read.csv("http://joeystanley.com/downloads/menu.csv")
menu_subset <- subset(menu, Category %in% c("Smoothies & Shakes", "Desserts", "Beverages", "Snacks & Sides"))

The default plot will just be the distribution of the number of sugars in each of the four remaining categories. For the color, I’ll use Paul Tol’s themes, which I access using the package ggthemes.

m <- ggplot(menu_subset, aes(Category, Sugars, fill = Category)) +
    geom_boxplot(size = 0.75) +
    geom_jitter(color = "gray15") + 
    ggthemes::scale_fill_ptol()

So now, to plot it, all I need to do is call m.

m

Conveniently for us, if I want to make changes to the plot, I can just add additional lines of ggplot2 code to the p and it’ll work out like normal. In other words…

m + ggtitle("A Default Plot")

…is shorthand for…

ggplot(menu_subset, aes(Category, Sugars, fill = Category)) +
    geom_boxplot(size = 0.75) +
    geom_jitter(color = "gray15") + 
    ggthemes::scale_fill_ptol() + 
    ggtitle("A Default Plot")

Both will produce this plot:

2.2 Stranger Things ratings

Another dataset I’ll use is the Stranger Things dataset that was used in a previous workshop. Like before, I’ll change the season column to a factor too. For the default plot here, s, I’ll make the same scatterplot of the number of votes the episode got on IMDB by the average rating. I’ll make the dots bigger so they’re easier to see. And for fun, I’ll use a Wes Anderson theme inspired by the movie Fantastic Mr. Fox.

stranger <- read.csv("http://joeystanley.com/data/stranger.csv")
stranger$season = factor(stranger$season)
s <- ggplot(stranger, aes(votes, rating, color = season)) + 
    geom_point(size = 4) + 
    scale_color_manual(values = wesanderson::wes_palette("FantasticFox1")) + 
    labs(title = "Stranger Things episodes",
         subtitle = "Average rating by number of votes on IMDB")
s

2.3 Girlnames

The last is a dataset of the top 25 most common baby girl names in the US in 2017. I’ll read it in and prep it as I did in the intro workshop, except I’ll reverse the order of the girlnames and will make a horizontal bar chart. I’ll also only keep the top 15 just so there aren’t so many bars and color it using Color Brewer.

girlnames <- read.delim("http://joeystanley.com/data/girlnames.txt", sep = "\t")
girlnames <- girlnames[1:15,]
girlnames$name = factor(girlnames$name, levels = rev(girlnames$name))
g <- ggplot(girlnames, aes(name, n, fill = n)) + 
    geom_bar(stat = "identity") + 
    scale_fill_distiller(type = "seq", palette = "Purples") + 
    coord_flip() + 
    labs(title = "Top 15 baby girl names in the US in 2017",
         caption = "Data Source: Social Security data via the babynames package")
g

2.4 Cereal nutritional facts

Finally, the last dataset tht we’ll use—which was also made available through Kaggle.com—contains nutritional information from about 80 different kinds of cereal. We’ll load it in, make a few changes as before, and make a faceted plot showing the amount of fiber per cereal and its rating, split up by brand.

cereal <- read.csv("http://joeystanley.com/data/cereal.csv")
cereal$mfr <- forcats::fct_recode(cereal$mfr,
                                  "Kellogg's" = "K",
                                  "General Mills" = "G",
                                  "Post" = "P",
                                  "Quaker Oats" = "Q",
                                  "Ralston Purina" = "R",
                                  "Nabisco" = "N")
c <- ggplot(cereal, aes(fiber, rating)) + 
    geom_text(aes(label = name),
              check_overlap = TRUE, vjust = "inward", hjust = "inward") + 
    facet_wrap(~mfr)
c

Note that within facet_wrap, I’ve added a couple extra arguments (check_overlap, vjust, and hjust). I recently learned about these from Hadley Wichkham’s book-in-progress, ggplot2 (version 3) which you can view here. They ensure that labels don’t overlap (removing some if necessary), and then make sure they don’t spill off over the edges of the plot. Very handy.

3 Pre-existing ggplot2 themes

Before we get started on creating custom themes, it’s important to understand a little bit about the built-in themes that ggplot2 provides for you.

3.1 theme_gray()

By default, ggplot2 will display using a theme that is generally pretty good. The creators have taken some liberties to decide a few things, some founded upon principles of good data visualization and others that are more opinionated. In Hadley Wickham’s words:

The theme is designed to put the data forward while supporting comparisons, following the advice of (Tufte 2006; Brewer 1994; Carr 2002, 1994; Carr and Sun 1999). We can still see the gridlines to aid in the judgement of position (Cleveland 1993a), but they have little visual impact and we can easily ‘tune’ them out. The grey background gives the plot a similar typographic colour to the text, ensuring that the graphics fit in with the flow of a document without jumping out with a bright white background. Finally, the grey background creates a continuous field of colour which ensures that the plot is perceived as a single visual entity.

Using the defaut theme will produce beautiful plots. Here’s what that would look like:

m
s
g
c

3.2 Other themes in ggplot2

Fortunately, if you don’t like the look of the gray background, you can easily switch to one of the other seven these that ggplot2 has bult in.

My go-to theme is called theme_bw() for just “black and white.” The biggest difference is that now the background is white instead of gray. But it also adds a thin black border around the whole thing and has faint grey grid lines instead of white ones.

m + theme_bw()
g + theme_bw()
s + theme_bw()
c + theme_bw()

The linedraw theme uses only pure black lines, rather than any gray ones, which makes it kind of look a little vintage. In facets is where I see the biggest change: the boxes at the top (called strips) are black.

m + theme_linedraw()
g + theme_linedraw()
s + theme_linedraw()
c + theme_linedraw()

The light theme is very similar to bw. The biggest difference is the outside box is lighter. In facets, the strips are darker and the text is colored white.

m + theme_light()
g + theme_light()
s + theme_light()
c + theme_light()

The classic theme has no grid and the top and right parts of the box are gone too, just leaving the x and y axes. The facet strips are empty white boxes now.

m + theme_classic()
g + theme_classic()
s + theme_classic()
c + theme_classic()

If you really like the gray, you can go for the dark theme, which has a darker background and a darker gray for the grid lines. In the girl names plot, you can see that the lighter colors pop out more effectively, but the middle ones blend in with the background a little bit, which is something to keep in mind when you add color to your plots.

m + theme_dark()
g + theme_dark()
s + theme_dark()
c + theme_dark()

The minimal theme is even simpler than light. It doens’t have the outside border but it does retain the inside grid. The strips are gone entirely, leaving just the text behind, which is a little confusing with this plot.

m + theme_minimal()
g + theme_minimal()
s + theme_minimal()
c + theme_minimal()

Finally, you can go completely blank with void. This may seem a little weird, but it does have useful purposes, like when creating maps.

m + theme_void()
g + theme_void()
s + theme_void()
c + theme_void()

Something to keep in mind with themes is that they override any other theme layer you might have included. For example, if you want to remove the legend and then use the classic theme, it’s still there:

m + theme(legend.position = "none") + 
    theme_classic()

No, this isn’t a bug. The reason for this is because theme_* is actually a shortcut for a whole bunch of other theme elements. As a result, there are two theme functions, so the second one overrides the first one. Fortunately, we can fix this by just reversing the order of theme and theme_classic:

m + theme_classic() +
    theme(legend.position = "none")

If you’re still relatively new to ggplot2, my recommendation is to try out one of these default themes for a while. Once you’ve gotten some experience with creating your own plots, which will help you spot things you like and dislike in others’ plots, you may get a sense of what you like in a plot and what you want to see changed. If that’s the case, then it might be time to start moving on to creating a custom theme.

3.3 Customizing existing themes

Out of the box, these themes already come with some customization. If you look at the help file for theme_bw, you can see what kinds of arguments it takes: base_size, base_family, base_line_size, and base_rect_size. These arguments have default values, but they can be modified.

With base_size, you can change the overall size of all text from the default of 10. Note that not all text is the same size: the title is bigger than the axis titles, wich are bigger than the axis labels. Changing this value will change them all proportionally. This is a useful change for making pretty big or pretty small graphs.

m + theme_bw(base_size = 5)
g + theme_bw(base_size = 5)
s + theme_bw(base_size = 5)
c + theme_bw(base_size = 5)

m + theme_bw(base_size = 15)
g + theme_bw(base_size = 15)
s + theme_bw(base_size = 15)
c + theme_bw(base_size = 15)

The argument you might change the most is the base_family. If you’re unsatisfied with the fonts, you can choose from a somewhat small set of R fonts and all the text will change.

m + theme_bw(base_family = "Palatino")
g + theme_bw(base_family = "Palatino")
s + theme_bw(base_family = "Palatino")
c + theme_bw(base_family = "Palatino")

With base_line_size, you can change how thick all the lines in the plot are, and with base_rect_size you can change how thick all the rectangle lines are.

m + theme_bw(base_line_size = 5)
g + theme_bw(base_line_size = 5)
s + theme_bw(base_line_size = 5)
c + theme_bw(base_line_size = 5)

m + theme_bw(base_rect_size = 5)
g + theme_bw(base_rect_size = 5)
s + theme_bw(base_rect_size = 5)
c + theme_bw(base_rect_size = 5)

So if you’re mostly satisfied with the themes but just want to tweak something small like the font and/or one of these other options, you can do that quick change and it’ll already look great.

4 Theme elements

Before we get too carried away with modifying a plot, we’ll have to take a look at the basic building blocks of a theme: element_line, element_text, element_rect, and element_blank. These are called “theme elements.” Many of the arguments of theme require the output of these functions as their value. This means that you can’t simply change the size of a line with a number like you can in a regular ggplot function (as in geom_line(size = 2)—you can’t do that here). So, we’re going to have to look at some of these elements to be able to use them in theme.

4.1 Drawing lines with element_line

One of the most fundamental concepts in a plot is a line. Assuming we have a starting and stopping point set already by the data we’re plotting, we can think of properties of a line that are purely aesthetic: color, thickness, and what type of line it is (solid, dotted, dashed, etc). In ggplot2, we modify these attributes (and a few others, with element_line). One examlpe of a line in a ggplot is the faint white grid underneath the data. We can modify that grid with the panel.grid argument. We’ll get into more detail about modifying the grid later, but for now we’ll use it as an example of a line. If we want to modify the color of the lines, we add element_line and specify that the color is light blue

m + theme(panel.grid = element_line(color = "lightblue"))

We can add other arguments too:

m + theme(panel.grid = element_line(color = "lightblue", size = 4, linetype = "dotted"))

In fact, if you inspect the element_line function itself, you can see that you can also add things like lineend. With this argument, you can change the ends of the line to can make the ends "round", "butt", or "square". In this example, it’s not very illustrative since the lines extend past the edge of the plotting area, but later when we come across other lines you’d like to modify, you can use that option if you’d like.

However, if you like arrows, you’ll also see that you can add them with the arrow argument. But if you thought you were done with basic ggplot2 elements, it turns out the arrow argument takes the output of the arrow() function, which allows you to modify things like the angle, length, type, and which sides of the line should get the arrow. And if that wasn’t Inception enough for you, the length argument requires the output of the unit function, in which you specify the length and the units.

m + theme(panel.grid = element_line(color = "lightblue", 
                                    arrow = arrow(angle = 20, 
                                                  length = unit(0.2, "inches"), 
                                                  ends = "both", 
                                                  type = "closed")))

This may seem like an overly complex network of functions, but the benefit is that it allows you to modify whatever you want.

4.2 Drawing rectangles with element_rect

One step up from drawing lines is to draw rectanges with element_rect. Because they consist of four lines, many of the arguments are the same as element_line.

c + theme(panel.border = element_rect(color = "darkred", size = 2, linetype = "dashed"))

Oops. Where did the plot go? So turns out rectangles aren’t just lines anymore because they create the space in the middle. So in element_rect we need to worry about what goes in the middle. By default, it seems like the color is white. What we probably want to do is remove it, or rather, make it transparent. We can do that with the fill argument:

c + theme(panel.border = element_rect(color = "darkred", size = 2, linetype = "dashed",
                                      fill = "transparent"))

So in the theme function, there are several things that are rectangles that you can modify. You can probably think of a few now like the legend or the strips in facets. We’ll use element_rect to modify those.

4.3 Drawing text with element_text

The last major element in ggplot is to draw text, which we do with element_text. As you can imagine, there are a lot of things you can change with text, like the font (“family”), it’s “face” ("bold", "italic", "bold.italic"), color, and size. In this example, we’ll modify a plot’s title.

g + theme(plot.title = element_text(family = "Palatino", face = "bold", color = "mediumpurple4", size = 20))

In addition to these attributes, we can also adjust other things. I’m also going to add the extremely handy debug = TRUE argument. This will make the plot title area yellow and will show a yellow dot where the text is anchored. So, for example, if I want to modify the angle to something onconventional, I can do so, but the result might not be very pretty.

g + theme(plot.title = element_text(family = "Palatino", face = "bold", color = "mediumpurple4", size = 20,
                                    angle = 3, debug = TRUE))

You can see that the anchor point is the top left corner of the plotting area. We may want to adjust the vertical position of this title. Both the hjust and vjust arguments take a number, ranging from 0 (left/bottom) to 1 (right/top). When we set the vertical adjustment to 0, it’ll anchor it to the bottom, which is probably what we wanted.

g + theme(plot.title = element_text(family = "Palatino", face = "bold", color = "mediumpurple4", size = 20,
                                    angle = 3, vjust = 0, debug = TRUE))

If you want to center your plot, you can do so by setting hjust to 0.5.

g + theme(plot.title = element_text(family = "Palatino", face = "bold", color = "mediumpurple4", size = 20,
                                    hjust = 0.5, debug = TRUE))

Note that you can use values outside of the range [0,1] and they’ll move around just as you expect:

g + theme(plot.title = element_text(family = "Palatino", face = "bold", color = "mediumpurple4", size = 20,
                                    vjust = -2, hjust = 2, debug = TRUE))

Finally, you may want to adjust the spacing around the text. To illustrate lineheight, I’ll change the title to something with two lines so you can get a better picture of what it’s doing. So if I really want to add double spacing, I can set it to 2. If I want to do something more subtle, maybe like 1.15.

g + labs(title = "Here is a\nTwo-lined Title",
         subtitle = "This is the subtitle") + 
    theme(plot.title = element_text(family = "Palatino", face = "bold", color = "mediumpurple4", size = 20,
                                    lineheight = 2, debug = TRUE))

For titles, this often won’t be necessary, but for other text elements in your plot, the lineheight may be useful.

The last thing is the margin, which takes the output of the margin function. In it, you specify how large the margins should be around your title area. The order is top, right, bottom, left, which you can maybe remember with the mneumonic “trouble”. For the title, left and right margins don’t mean much, but you can adjust how far the title is from the top of the graph or from the plotting area. Of course, if you want it right up against the plotting area, you can set the bottom margin to zero.

g + theme(plot.title = element_text(family = "Palatino", face = "bold", color = "mediumpurple4", size = 20,
                                    margin = margin(1, 0, 0, 0, unit = "cm"), debug = TRUE))

So that’s the element_text function. It’s powerful because you can control a lot of things with it.

4.4 Drawing nothing with element_blank

The final category of ggplot elements is element_blank and it’s pretty simple: it draws nothing. An important detail though is that not only does draw nothing, but it doesn’t even reserve space for that thing. So in the Stranger Things plot, the x- and y-axis labels are useful, but if we wanted to purposely make the plot confusing, we could remove them.

s + theme(axis.title = element_blank())

So if there’s something in your plot by default and you want to remove it, you just simply use element_blank and it’ll zap it from existence.

5 Exploring theme()

So now that we’ve seen the basic elements of ggplot, we can now use them in practice. The main function that has been driving all the changes in the various themes is the theme function. In previous workshops, I’ve mentioned in it passing and I’ve used it to rotate the text in the x-axis labels, remove the legend, and mess with the background grid. All very different things, but all controlled by one function. In the previous section we saw that theme is used to modify the title, plotting area, and axes too. Let’s take a dive into the function and see what’s going on.

If you look at the help page for theme, you’ll see that there are dozens and dozens of components:

?theme

# theme(line, rect, text, title, aspect.ratio, axis.title, axis.title.x,
#   axis.title.x.top, axis.title.x.bottom, axis.title.y, axis.title.y.left,
#   axis.title.y.right, axis.text, axis.text.x, axis.text.x.top,
#   axis.text.x.bottom, axis.text.y, axis.text.y.left, axis.text.y.right,
#   axis.ticks, axis.ticks.x, axis.ticks.x.top, axis.ticks.x.bottom,
#   axis.ticks.y, axis.ticks.y.left, axis.ticks.y.right, axis.ticks.length,
#   axis.ticks.length.x, axis.ticks.length.x.top, axis.ticks.length.x.bottom,
#   axis.ticks.length.y, axis.ticks.length.y.left, axis.ticks.length.y.right,
#   axis.line, axis.line.x, axis.line.x.top, axis.line.x.bottom, axis.line.y,
#   axis.line.y.left, axis.line.y.right, legend.background, legend.margin,
#   legend.spacing, legend.spacing.x, legend.spacing.y, legend.key,
#   legend.key.size, legend.key.height, legend.key.width, legend.text,
#   legend.text.align, legend.title, legend.title.align, legend.position,
#   legend.direction, legend.justification, legend.box, legend.box.just,
#   legend.box.margin, legend.box.background, legend.box.spacing,
#   panel.background, panel.border, panel.spacing, panel.spacing.x,
#   panel.spacing.y, panel.grid, panel.grid.major, panel.grid.minor,
#   panel.grid.major.x, panel.grid.major.y, panel.grid.minor.x,
#   panel.grid.minor.y, panel.ontop, plot.background, plot.title,
#   plot.subtitle, plot.caption, plot.tag, plot.tag.position, plot.margin,
#   strip.background, strip.background.x, strip.background.y,
#   strip.placement, strip.text, strip.text.x, strip.text.y,
#   strip.switch.pad.grid, strip.switch.pad.wrap, ..., complete = FALSE,
#   validate = TRUE)

That’s a lot. And if you scroll through the help page, there is lots of description about how to use these arguments. Admittedly, the help files can take some practice to understand, but with practice you’ll be able to decipher these without any problems. If you look closely at the arguments though, most of them are pretty well-organized into major categories: axis, legend, panel, plot, and strip.

It is completely out of the scope of this workshop to cover all the arguments of theme. (Believe me, I tried! I decided to push it all onto the supplemental handout if you’d like to see more detail.) However, with exposure to the theme elements, the help files, and some practice problems, you should now be ready to explore them on your own.

Exercise 1: Legends

The problem

  1. Open the help page for theme by running the command ?theme. Scroll through the long list of arguments and find one that looks like it might be able to change the color of the background of the plot’s legend. Take the McDonald’s dataset and change the legend’s background to pink, with a dahsed black line around it.

  2. In previous workshops, you’ve seen how to remove the legend entirely with legend.position. Explore the help file and see what other options are available.

The solution

  1. Here’s what I got. I used the legend.background argument, which takes the output of element_rect. Using that, I could change the color and border. I also made the border extra thin so the dashes looked alright.

    m + theme(legend.background = element_rect(fill = "pink", linetype = "dashed", color = "black", size = 0.25))

  2. There are lots of options for a legend position, but one is that you can put it on the bottom.

    m + theme(legend.position = "bottom")

    If you look closely, you can actually move it to an arbitrary location with a pair of coordinates. This is often helpful with legend.justification. Here I’ll pin the top right corner of the legend to the top right corner of the plotting area.

    m + theme(legend.position = c(1,1),
              legend.justification = c(1,1))

Exercise 2: Axes

The problem

  1. By default, the y-axis is rotated 90 degrees. Take the Stranger Things plot and change it so that there is no rotation. While you’re there, make it in a bold font face. Make sure that the label is still centered vertically. Hint, you may need to specify that you just want to change the y-axis if things aren’t working out.

  2. Make the actual numbers that run along the axes bigger and italic.

The solution

  1. This one was a little tricky. You would think that by changing axis.title and making the angle set to 0 that it would work, but it turns out that the more specific axis.title.y overrides it. So, first, I made both the x- and y-axis labels bold with axis.title. Then, since I’m just focusing in on the y-axis, I use axis.title.y to set the angle and center it vertically.

    s + theme(axis.title = element_text(face = "bold"),
              axis.title.y = element_text(angle = 0, vjust = 0.5))

  2. Since we’re just targeting the numbers along the axes, we want axis.text, which can easily be controlled with element_text.

    s + theme(axis.text = element_text(size = 15, face = "italic"))

Exercise 3: The plotting area

The problem

  1. Take the Stranger Things plot and center the title and subtitle. Take the girl names plot and right align the title, making it bigger, a shade purple, and in a different font. Make the caption left aligned.

  2. Using the cereal plot, change the background of the panels to white. Meanwhile, change the background of the rest of the area a light gray (specifically, “gray95”).

The solution

  1. We’ve already seen how to center a title, so by applying the same principles to plot.subtitle you can do the same thing to the subtitle as well.

    s + theme(plot.title = element_text(hjust = 0.5),
              plot.subtitle = element_text(hjust = 0.5))

    With this, you just add some additional properties to plot.title. For the caption, use plot.caption and left align it by setting hjust to 0.

    g + theme(plot.title = element_text(hjust = 1, size = 15, color = "mediumpurple", family = "Courier"),
              plot.caption = element_text(hjust = 0))

  2. Controlling the panel backgrounds is done with panel.background and is a simple fix with element_rect. Controlling everything outside of the panels is done with plot.background. Basically, we’ve just swapped the colors of theme_gray. You’ll notice we lost our grid lines though since they’re white. What would you change to make them appear in a light gray?

c + theme(panel.background = element_rect(fill = "white"),
          plot.background = element_rect(fill = "gray95"))

6 Finalizing your theme

Now that you’ve practiced some of what theme can do for your plot, you should have an idea of what kind of theme you’d like to put together when you make your own. This section helps you create that final custom theme.

6.1 Adding with + and replacing with %+replace%

It’s time to start creating your own custom theme During the course of the workshop, I’ve made some changes that I kind of liked, and I’d like to try a theme that incporates those elements.

When building a theme, the simplest way to create your own is to add to an existing theme. Using the +, which is what we’ve been doing this whole time, you can create a theme based on an existing one. Recall that theme_bw has the legend position to the right of the plotting area (i.e. legend.position = "right"). When we add a theme function to theme_bw, it’ll overwrite whatever is there with the elements we specify. Here, I’m changing it so that the position is in the top right. I’m also adding a couple other subtle things like making the background gray, italicizing the caption, and making the title bigger.

g + theme_bw() + 
    theme(legend.background = element_rect(fill = "white", linetype = "solid", color = "gray50", size = 0.5),
          legend.position = c(1,1),
          legend.justification = c(1,1),
          axis.title = element_text(color = "gray10", size = 12, face = "bold"),
          plot.title = element_text(size = 15),
          plot.caption = element_text(face = "italic"),
          plot.background = element_rect(fill = "gray90"))

As an alternative to +, you can use the funny-looking %+replace% function. On the surface, they may seem pretty similar, but there are some important differences.

Let’s say I want create a theme where the y-axis title text is red. When we use +, it behaves as expected. When we use %+replace% there are some additional changes. What’s happening?

g + labs(title = "Standard black and white theme") + theme_bw()

g + labs(title = "Adding theme elements with +", 
         subtitle = "Added to theme_bw()") + 
    theme_bw() + 
    theme(axis.title.y = element_text(color = "red"))

g + labs(title = "Adding theme elements with %+replace%", 
         subtitle = "Added to theme_bw()") + 
    theme_bw() %+replace%
    theme(axis.title.y = element_text(color = "red"))

Why did it rotate? As it turns out, when you add theme elements with +, it simply adds whatever changes you specify to the existing complete theme. Part of the underlying code in theme_bw (actually theme_gray, which theme_bw is based on) is axis.title.y = element_text(angle = 90) among other things. So since the black color is specified already elsewhere in the code, changing it to red just alters that one specification, leaving everything else intact.

However, when you use %+replace%, everything relating to axis.title.y gets replaced by whatever you specify; the crucial difference is that if you don’t specify something, rather than falling back on whatever is defined in the base theme, it’ll remove it entirely. So in the case of the angle, because we didn’t specify an angle after doing %+replace%, it was set to its defalt (0) instead of falling back on what the base theme uses (90). This can be problematic it not used carefully because failing to specify some things may cause an error in the code.

To be on the safe side, it’s probably best to use +, unless you’re pretty confident in what you’re doing.

6.2 Complete themes

One more argument to theme is complete, which can either be TRUE or FALSE. Normally it’s set to FALSE. This means that whatever elements you do not specify in theme will fall back on the values in the existing theme. (Keep in mind that even if you don’t specify a theme, it falls back on theme_gray.) If you set complete to true, it will not do this. So, you only want to set complete to TRUE if you have explicitly stated everything you need for a complete theme. That is, it’s not based on or a modification of an existing theme, but rather an entirely new theme built from scratch My guess is that most ggplot2 users will not need this function unless you are investing quite a bit of time creating something very different from existing themes.

6.3 Create the function

At long last, it’s time we create the theme function. I’ll call my new theme theme_joey. For simplicity, I’ll restict my theme to just modifying where the legend goes and I’ll center the title.

theme_joey <- function() {
    theme_bw() + 
    theme(legend.background = element_rect(fill = "white", linetype = "solid", color = "gray50", size = 0.5),
          legend.position = c(1,1),
          legend.justification = c(1,1),
          axis.title = element_text(color = "gray10", size = 12, face = "bold"),
          plot.title = element_text(size = 15),
          plot.caption = element_text(face = "italic"),
          plot.background = element_rect(fill = "gray90"))
}
g + theme_joey()

For a small bit of flexibliity, I may want to take advantage of the arguments to the base themes, as shown in section 3.3. A shortcut for making all those arguments accessible to theme_joey is to use .... Now I can change the font family and size without making any modifications to the function.

theme_joey <- function(...) {
    theme_bw(...) + 
    theme(legend.background = element_rect(fill = "white", linetype = "solid", color = "gray50", size = 0.5),
          legend.position = c(1,1),
          legend.justification = c(1,1),
          axis.title = element_text(color = "gray10", size = 12, face = "bold"),
          plot.title = element_text(size = 15),
          plot.caption = element_text(face = "italic"),
          plot.background = element_rect(fill = "gray90"))
}
g + theme_joey(base_family = "Palatino")

And that’s it! I can add this to all my plots and it’ll be formatted consistently every time. Furthermore, I can add to the theme just one time in the function, and it’ll show up on all the plots without any other extra work.

m + theme_joey()
g + theme_joey()
s + theme_joey()
c + theme_joey()

Now, as you may have seen already, moving the legend to the top right corner may not be the best idea to incorporate into a theme. There’s no guarantee that that space won’t be used by some data. In fact, in the bottom left plot, it covers the end of the longest bar in the bar plot. So, when creating your themes, you either need to make them robust enough to handle all sorts of different plots. Or, if you know you’ll be using your theme in a finite set of plots (like all 10 in preparation for a paper or something), you can more more aggressive in specifying theme elements, since you know your data.

7 Conclusion

This workshop has taken us through the long journey of exploring themes in ggplot2. I say “us” because I learned a lot in the process of putting this handout together. But, I think the main takeaways are these:

  1. Learn the theme elements (element_text, element_line, element_rect) as well as margin, arrow, and unit. Most of the arguments to theme are built around those.

  2. Be familiar with the hierarchy of arguments and how inheritance works.

  3. Explore the code behind exising themes to get an idea for how they’re built.

It will take some practice to get used to these. But, the benefit is that you’ll have the flexibility to control whatever elements of your plot you want. And that is something that very few other data visualization tools can do for you.