Themes

Content for week of Monday, June 20, 2022–Friday, June 24, 2022

The lesson for today’s session is a fairly comprehensive introduction to using the theme() function in ggplot, and this page by Henry Wang is a good cheat sheet for remembering which theme elements are which on a plot.

For your exercise, you’re going to create the world’s ugliest plot. For this example, we’ll use the principles of CRAP to make a great theme.

I’m going to build the theme semi-incrementally here. Instead of showing how the plot updates with each change in setting, I do most of the updates all at once, with tons of comments explaining what each line does. Importantly, I did not write this all at once. When you’re tinkering with themes, you generally start with something like theme_minimal() or theme_bw() and then gradually add new things to theme(), like modifying plot.title, then plot.subtitle, etc. It’s a very iterative process with lots of tinkering. Because of this, there is no live-coding video for this example—it would be incredibly long and boring. Instead, look through each of the lines and see what they’re doing.

For this example, I’m going to use the gapminder dataset that we’ve been using throughout this week. You can get it if you install the gapminder package in R, or you can download this CSV file (you may need to right click on it and select “Save As…"):

I’m also going to use the Roboto Condensed font in the theme. Download and install it on your computer if you don’t have it.

Basic plot

When I’m creating a theme, I like to use a basic plot with everything that might show up, complete with a title, subtitle, caption, legend, facets, and other elements.

library(tidyverse)  # For ggplot, dplyr, and friends
library(gapminder)  # For gapminder data
library(scales)     # For nice axis labels

gapminder_filtered <- gapminder %>% 
  filter(year > 2000)

base_plot <- ggplot(data = gapminder_filtered,
                    mapping = aes(x = gdpPercap, y = lifeExp, 
                                  color = continent, size = pop)) +
  geom_point() +
  # Use dollars, and get rid of the cents part (i.e. $300 instead of $300.00)
  scale_x_log10(labels = dollar_format(accuracy = 1)) +
  # Format with commas
  scale_size_continuous(labels = comma) +
  # Use viridis
  scale_color_viridis_d(option = "plasma", end = 0.9) +
  labs(x = "GDP per capita", y = "Life expectancy",
       color = "Continent", size = "Population",
       title = "Here's a cool title",
       subtitle = "And here's a neat subtitle",
       caption = "Source: The Gapminder Project") +
  facet_wrap(vars(year))

base_plot

Now we have base_plot to work with. Here’s what it looks like with theme_minimal() applied to it:

base_plot +
  theme_minimal()

That gets rid of the grey background and is a good start, but we can make lots of improvements. First let’s deal with the gridlines. There are too many. We can get rid of the minor gridlines with by setting them to element_blank():

base_plot +
  theme_minimal() +
  theme(panel.grid.minor = element_blank())

Next let’s add some typographic contrast. We’ll use Roboto Condensed Regular as the base font. Before trying this, make sure you do the following:

On macOS:

  • Run capabilities() in your console and verify that TRUE shows up under cairo
  • If not, download and install XQuartz

On Windows:

  • Run windowsFonts() in your console and you’ll see a list of all the fonts you can use with R. It’s not a very big list.

    #> $serif
    #> [1] "TT Times New Roman"
    #>
    #> $sans
    #> [1] "TT Arial"
    #> 
    #> $mono
    #> [1] "TT Courier New"
    

    You can add Roboto Condensed to your current R session by running this in your console:

    windowsFonts(`Roboto Condensed` = windowsFont("Roboto Condensed"))
    

    Now if you run windowsFonts(), you’ll see it in the list:

    #> $serif
    #> [1] "TT Times New Roman"
    #>
    #> $sans
    #> [1] "TT Arial"
    #> 
    #> $mono
    #> [1] "TT Courier New"
    #>
    #> $`Roboto Condensed`
    #> [1] "Roboto Condensed"
    

    This only takes effect for your current R session, so if you are knitting a document or if you ever plan on closing RStudio, you’ll need to incorporate this font creation code into your script.

We’ll use the font as the base_family argument. Note how I make it bold with face and change the size with rel(). Instead of manually setting some arbitrary size, I use rel() to resize the text in relation to the base_size argument. Using rel(1.7) means 1.7 × base_size, or 20.4 That will rescale according to whatever base_size is—if I shrink it to base_size = 8, the title will scale down accordingly.

plot_with_good_typography <- base_plot +
  theme_minimal(base_family = "Roboto Condensed", base_size = 12) +
  theme(panel.grid.minor = element_blank(),
        # Bold, bigger title
        plot.title = element_text(face = "bold", size = rel(1.7)),
        # Plain, slightly bigger subtitle that is grey
        plot.subtitle = element_text(face = "plain", size = rel(1.3), color = "grey70"),
        # Italic, smaller, grey caption that is left-aligned
        plot.caption = element_text(face = "italic", size = rel(0.7), 
                                    color = "grey70", hjust = 0),
        # Bold legend titles
        legend.title = element_text(face = "bold"),
        # Bold, slightly larger facet titles that are left-aligned for the sake of repetition
        strip.text = element_text(face = "bold", size = rel(1.1), hjust = 0),
        # Bold axis titles
        axis.title = element_text(face = "bold"),
        # Add some space above the x-axis title and make it left-aligned
        axis.title.x = element_text(margin = margin(t = 10), hjust = 0),
        # Add some space to the right of the y-axis title and make it top-aligned
        axis.title.y = element_text(margin = margin(r = 10), hjust = 1))
plot_with_good_typography

Whoa. That gets us most of the way there! We have good contrast with the typography, with the strong bold and the lighter regular font (✓ contrast). Everything is aligned left (✓ alignment and ✓ repetition). By moving the axis titles a little bit away from the labels, we’ve enhanced proximity, since they were too close together (✓ proximity). We repeat grey in both the caption and the subtitle (✓ repetition).

The only thing I don’t like is that the 2002 isn’t quite aligned with the title and subtitle. This is because the facet labels are in boxes along the top of each plot, and in some themes (like theme_grey() and theme_bw()) those facet labels have grey backgrounds. We can turn off the margin in those boxes, or we can add a background, which will then be perfectly aligned with the title and subtitle.

plot_with_good_typography +
  # Add a light grey background to the facet titles, with no borders
  theme(strip.background = element_rect(fill = "grey90", color = NA),
        # Add a thin grey border around all the plots to tie in the facet titles
        panel.border = element_rect(color = "grey90", fill = NA))

👩‍🍳 💋! That looks great!

To save ourselves time in the future, we can store this whole thing as an object that we can then reuse on other plots:

my_pretty_theme <- theme_minimal(base_family = "Roboto Condensed", base_size = 12) +
  theme(panel.grid.minor = element_blank(),
        # Bold, bigger title
        plot.title = element_text(face = "bold", size = rel(1.7)),
        # Plain, slightly bigger subtitle that is grey
        plot.subtitle = element_text(face = "plain", size = rel(1.3), color = "grey70"),
        # Italic, smaller, grey caption that is left-aligned
        plot.caption = element_text(face = "italic", size = rel(0.7), 
                                    color = "grey70", hjust = 0),
        # Bold legend titles
        legend.title = element_text(face = "bold"),
        # Bold, slightly larger facet titles that are left-aligned for the sake of repetition
        strip.text = element_text(face = "bold", size = rel(1.1), hjust = 0),
        # Bold axis titles
        axis.title = element_text(face = "bold"),
        # Add some space above the x-axis title and make it left-aligned
        axis.title.x = element_text(margin = margin(t = 10), hjust = 0),
        # Add some space to the right of the y-axis title and make it top-aligned
        axis.title.y = element_text(margin = margin(r = 10), hjust = 1),
        # Add a light grey background to the facet titles, with no borders
        strip.background = element_rect(fill = "grey90", color = NA),
        # Add a thin grey border around all the plots to tie in the facet titles
        panel.border = element_rect(color = "grey90", fill = NA))

Now we can use it on any plot. Remember that first plot you made in your exercise from session 1 with the cars dataset? Let’s throw this theme on it! (only here the dataset is named mpg instead of cars; the mpg dataset is loaded invisibly whenever you load ggplot)

mpg_example <- ggplot(data = mpg, 
                      mapping = aes(x = displ, y = hwy, color = class)) +
  geom_point(size = 3) + 
  scale_color_viridis_d() +
  facet_wrap(vars(drv)) +
  labs(x = "Displacement", y = "Highway MPG", color = "Car class",
       title = "Heavier cars get worse mileage",
       subtitle = "Except two-seaters?",
       caption = "Here's a caption") +
  my_pretty_theme

mpg_example

Super neat!

Nice pre-built themes

This custom theme we just made is just one iteration of a theme. There are countless ways to tinker with a theme and have it meet the different CRAP principles. People have even published their own themes in different R packages. Check these out to see lots of different examples:

Check this blog post for examples of a bunch of others

Bonus: ggthemeassist

If you’re intimidated by constantly referring to the documentation and figuring out what little line of code affects which part of the graph, install and check out the ggthemeassist package. It provides an interactive menu for manipulating different theme elements, and then generates all the corresponding code, which is really magical.

Here’s a brief example of how to use it.

Saving plots

If we want to save these plots, we can use ggsave(). For that to work, we need to store the plot as an object, which I already did in the examples above:

name_of_plot_object <- ggplot(...)

We then feed our saved plot object to ggsave() and specify the filename and dimensions we want to use. If we’re using PNG, we don’t need to worry about any extra options. If we’re using PDF, we need to tell R to use the Cairo PDF writing engine instead of R’s normal one, since R’s normal one can’t deal with custom fonts.

# Add my_pretty_theme to the gapminder base_plot and save as an object
final_gampinder_plot <- base_plot +
  my_pretty_theme

# Save as PNG and PDF
ggsave("fancy_gapminder.png", final_gampinder_plot,
       width = 8, height = 5, units = "in")
ggsave("fancy_gapminder.pdf", final_gampinder_plot,
       width = 8, height = 5, units = "in", device = cairo_pdf)

# Save the mpg plot as PNG and PDF
ggsave("fancy_mpg.png", mpg_example,
       width = 8, height = 5, units = "in")
ggsave("fancy_mpg.pdf", mpg_example,
       width = 8, height = 5, units = "in", device = cairo_pdf)