Engaging and Beautiful Data Visualizations with ggplot2

Working with Themes

Cédric Scherer // posit::conf // September 2023

Theming


= stylistic changes of non-data elements

  • complete themes plus custom theme adjustments
    • add complete themes via theme_*()
    • theme defaults can be overwritten via theme()

Preparation: Data

library(readr)
library(ggplot2)

bikes <-
  read_csv(
    here::here("data", "london-bikes-custom.csv"),
    col_types = "Dcfffilllddddc"
  )

Preparation: Plot

bikes |>
  dplyr::group_by(
    month = lubridate::month(date, label = TRUE), 
    day_night, year
  ) |> 
  dplyr::summarize(count = sum(count))
# A tibble: 48 × 4
# Groups:   month, day_night [24]
   month day_night year   count
   <ord> <chr>     <fct>  <int>
 1 Jan   day       2015  398555
 2 Jan   day       2016  423622
 3 Jan   night     2015  148084
 4 Jan   night     2016  158896
 5 Feb   day       2015  398429
 6 Feb   day       2016  438254
 7 Feb   night     2015  145165
 8 Feb   night     2016  154656
 9 Mar   day       2015  511860
10 Mar   day       2016  487395
# ℹ 38 more rows

Preparation: Plot

bikes |>
  dplyr::group_by(
    month = lubridate::month(date, label = TRUE), 
    day_night, year
  ) |> 
  dplyr::summarize(count = sum(count)) |> 
  ggplot(aes(x = month, y = count, color = day_night)) +
  geom_line(aes(group = day_night)) +
  geom_point(size = 2) +
  facet_wrap(~ year, ncol = 1, scales = "free_x")

Preparation: Plot

g <- bikes |>
  dplyr::group_by(month = lubridate::month(date, label = TRUE), day_night, year) |> 
  dplyr::summarize(count = sum(count)) |> 
  ggplot(aes(x = month, y = count, color = day_night)) +
  geom_line(aes(group = day_night)) +
  geom_point(size = 2) +
  facet_wrap(~year, ncol = 1, scales = "free_x") +
  coord_cartesian(clip = "off") +
  scale_y_continuous(
    limits = c(0, 830000), expand = c(0, 0),
    labels = scales::label_comma(scale = 1/10^3, suffix = "K")
  ) +
  scale_color_manual(values = c(day = "#FFA200", night = "#757BC7")) +
  labs(
    x = NULL, y = "# rented bikes", color = NULL,
    title = "TfL Bike Shares per Month and Year",
    caption = "Data: TfL (Transport for London)"
  )

Complete Themes

Complete Themes

g + theme_light()

g + theme_bw()

Complete Themes

g + theme_minimal()

g + theme_classic()

Complete Themes

g + theme_dark()

g + theme_void()

Complete Themes via Extension Packages

g + ggthemes::theme_stata()

g + ggthemes::theme_gdocs()

Complete Themes via Extension Packages

g + hrbrthemes::theme_ipsum_rc()

g + tvthemes::theme_simpsons()

Modify Theme Elements

g + 
  hrbrthemes::theme_ipsum_rc() +
  theme(
    panel.grid.minor = element_blank(),
    plot.title = element_text(color ="#28A87D"),
    plot.title.position = "plot"
  )

Non-Default Typefaces

The {systemfonts} Package

locates installed fonts and provides font-related utilities to graphic devices

library(systemfonts)
match_font("Asap", bold = TRUE)
$path
[1] "/Users/cedric/Library/Fonts/Asap-Bold.ttf"

$index
[1] 0

$features
NULL

Use Non-Default Typefaces

system_fonts()
# A tibble: 1,808 × 9
   path                   index name  family style weight width italic monospace
   <chr>                  <int> <chr> <chr>  <chr> <ord>  <ord> <lgl>  <lgl>    
 1 /System/Library/Fonts…     0 Note… Notew… Light normal norm… FALSE  FALSE    
 2 /System/Library/Fonts…     0 Msht… Mshta… Regu… normal norm… FALSE  FALSE    
 3 /Users/cedric/Library…     0 Inpu… Input… Ligh… normal cond… TRUE   FALSE    
 4 /Users/cedric/Library…    13 Iose… Iosev… Exte… normal norm… TRUE   TRUE     
 5 /System/Library/Fonts…     3 Kohi… Kohin… Bold  bold   norm… FALSE  FALSE    
 6 /Users/cedric/Library…     0 Fami… Famil… Medi… medium norm… TRUE   FALSE    
 7 /Users/cedric/Library…     0 Lite… Liter… Bold… bold   norm… TRUE   FALSE    
 8 /System/Library/Fonts…     4 ITFD… ITF D… Medi… medium norm… FALSE  FALSE    
 9 /Users/cedric/Library…     0 Open… Open … Bold… bold   semi… TRUE   FALSE    
10 /Users/cedric/Library…     0 Kola… Kolage Extr… normal norm… FALSE  FALSE    
# ℹ 1,798 more rows

Use Non-Default Typefaces

system_fonts() |>
  dplyr::filter(stringr::str_detect(family, "Asap")) |>
  dplyr::select(family) |>
  unique() |> 
  dplyr::arrange(family)
# A tibble: 5 × 1
  family            
  <chr>             
1 Asap              
2 Asap Condensed    
3 Asap Expanded     
4 Asap SemiCondensed
5 Asap SemiExpanded 

Use Non-Default Typefaces

g +
  theme_minimal(
    base_family = "Asap SemiCondensed",
    base_size = 13
  )

Use Font Features

system_fonts() |>
  dplyr::filter(family == "Asap SemiCondensed") |>
  dplyr::select(name) |>
  dplyr::arrange(name)
# A tibble: 18 × 1
   name                              
   <chr>                             
 1 AsapSemiCondensed-Black           
 2 AsapSemiCondensed-BlackItalic     
 3 AsapSemiCondensed-Bold            
 4 AsapSemiCondensed-BoldItalic      
 5 AsapSemiCondensed-ExtraBold       
 6 AsapSemiCondensed-ExtraBoldItalic 
 7 AsapSemiCondensed-ExtraLight      
 8 AsapSemiCondensed-ExtraLightItalic
 9 AsapSemiCondensed-Italic          
10 AsapSemiCondensed-Light           
11 AsapSemiCondensed-LightItalic     
12 AsapSemiCondensed-Medium          
13 AsapSemiCondensed-MediumItalic    
14 AsapSemiCondensed-Regular         
15 AsapSemiCondensed-SemiBold        
16 AsapSemiCondensed-SemiBoldItalic  
17 AsapSemiCondensed-Thin            
18 AsapSemiCondensed-ThinItalic      

Use Font Features

register_variant(
  name = "Asap SemiCondensed Semibold S1",
  family = "Asap SemiCondensed",
  weight = "semibold",
  features = font_feature(letters = "stylistic")
)

Use Font Features

g + 
  theme_minimal(
    base_family = "Asap SemiCondensed Semibold S1",
    base_size = 13
  )

Use Font Features

Use Font Features

register_variant(
  name = "Spline Sans Tabular",
  family = "Spline Sans",
  weight = "normal",
  features = font_feature(numbers = "tabular")
)

Use Font Features

Like a Pro: Set Themes Globally

theme_set(theme_minimal(base_family = "Asap SemiCondensed", base_size = 13))

theme_update(
  panel.grid.minor = element_blank(),
  strip.text = element_text(face = "bold", size = rel(1.1)),
  plot.title = element_text(face = "bold", size = rel(1.3)),
  plot.title.position = "plot",
  plot.caption.position = "plot"
)

Like a Pro: Set Themes Globally

g

Custom Themes

Complete Themes: What’s Inside?

theme_grey
function (base_size = 11, base_family = "", base_line_size = base_size/22, 
    base_rect_size = base_size/22) 
{
    half_line <- base_size/2
    t <- theme(line = element_line(colour = "black", linewidth = base_line_size, 
        linetype = 1, lineend = "butt"), rect = element_rect(fill = "white", 
        colour = "black", linewidth = base_rect_size, linetype = 1), 
        text = element_text(family = base_family, face = "plain", 
            colour = "black", size = base_size, lineheight = 0.9, 
            hjust = 0.5, vjust = 0.5, angle = 0, margin = margin(), 
            debug = FALSE), axis.line = element_blank(), axis.line.x = NULL, 
        axis.line.y = NULL, axis.text = element_text(size = rel(0.8), 
            colour = "grey30"), axis.text.x = element_text(margin = margin(t = 0.8 * 
            half_line/2), vjust = 1), axis.text.x.top = element_text(margin = margin(b = 0.8 * 
            half_line/2), vjust = 0), axis.text.y = element_text(margin = margin(r = 0.8 * 
            half_line/2), hjust = 1), axis.text.y.right = element_text(margin = margin(l = 0.8 * 
            half_line/2), hjust = 0), axis.text.r = element_text(margin = margin(l = 0.8 * 
            half_line/2, r = 0.8 * half_line/2), hjust = 0.5), 
        axis.ticks = element_line(colour = "grey20"), axis.ticks.length = unit(half_line/2, 
            "pt"), axis.ticks.length.x = NULL, axis.ticks.length.x.top = NULL, 
        axis.ticks.length.x.bottom = NULL, axis.ticks.length.y = NULL, 
        axis.ticks.length.y.left = NULL, axis.ticks.length.y.right = NULL, 
        axis.minor.ticks.length = rel(0.75), axis.title.x = element_text(margin = margin(t = half_line/2), 
            vjust = 1), axis.title.x.top = element_text(margin = margin(b = half_line/2), 
            vjust = 0), axis.title.y = element_text(angle = 90, 
            margin = margin(r = half_line/2), vjust = 1), axis.title.y.right = element_text(angle = -90, 
            margin = margin(l = half_line/2), vjust = 1), legend.background = element_rect(colour = NA), 
        legend.spacing = unit(2 * half_line, "pt"), legend.spacing.x = NULL, 
        legend.spacing.y = NULL, legend.margin = margin(half_line, 
            half_line, half_line, half_line), legend.key = NULL, 
        legend.key.size = unit(1.2, "lines"), legend.key.height = NULL, 
        legend.key.width = NULL, legend.key.spacing = unit(half_line, 
            "pt"), legend.text = element_text(size = rel(0.8)), 
        legend.title = element_text(hjust = 0), legend.ticks.length = rel(0.2), 
        legend.position = "right", legend.direction = NULL, legend.justification = "center", 
        legend.box = NULL, legend.box.margin = margin(0, 0, 0, 
            0, "cm"), legend.box.background = element_blank(), 
        legend.box.spacing = unit(2 * half_line, "pt"), panel.background = element_rect(fill = "grey92", 
            colour = NA), panel.border = element_blank(), panel.grid = element_line(colour = "white"), 
        panel.grid.minor = element_line(linewidth = rel(0.5)), 
        panel.spacing = unit(half_line, "pt"), panel.spacing.x = NULL, 
        panel.spacing.y = NULL, panel.ontop = FALSE, strip.background = element_rect(fill = "grey85", 
            colour = NA), strip.clip = "inherit", strip.text = element_text(colour = "grey10", 
            size = rel(0.8), margin = margin(0.8 * half_line, 
                0.8 * half_line, 0.8 * half_line, 0.8 * half_line)), 
        strip.text.x = NULL, strip.text.y = element_text(angle = -90), 
        strip.text.y.left = element_text(angle = 90), strip.placement = "inside", 
        strip.placement.x = NULL, strip.placement.y = NULL, strip.switch.pad.grid = unit(half_line/2, 
            "pt"), strip.switch.pad.wrap = unit(half_line/2, 
            "pt"), plot.background = element_rect(colour = "white"), 
        plot.title = element_text(size = rel(1.2), hjust = 0, 
            vjust = 1, margin = margin(b = half_line)), plot.title.position = "panel", 
        plot.subtitle = element_text(hjust = 0, vjust = 1, margin = margin(b = half_line)), 
        plot.caption = element_text(size = rel(0.8), hjust = 1, 
            vjust = 1, margin = margin(t = half_line)), plot.caption.position = "panel", 
        plot.tag = element_text(size = rel(1.2), hjust = 0.5, 
            vjust = 0.5), plot.tag.position = "topleft", plot.margin = margin(half_line, 
            half_line, half_line, half_line), complete = TRUE)
    ggplot_global$theme_all_null %+replace% t
}
<bytecode: 0x129e7c850>
<environment: namespace:ggplot2>

Complete Themes: What’s Inside?

theme_minimal
function (base_size = 11, base_family = "", base_line_size = base_size/22, 
    base_rect_size = base_size/22) 
{
    theme_bw(base_size = base_size, base_family = base_family, 
        base_line_size = base_line_size, base_rect_size = base_rect_size) %+replace% 
        theme(axis.ticks = element_blank(), legend.background = element_blank(), 
            legend.key = element_blank(), panel.background = element_blank(), 
            panel.border = element_blank(), strip.background = element_blank(), 
            plot.background = element_blank(), complete = TRUE)
}
<bytecode: 0x10c1ffe40>
<environment: namespace:ggplot2>

Create a Custom Theme

theme_asap <- function(base_size = 13, base_family = "Asap SemiCondensed", 
                       base_line_size = base_size/22, base_rect_size = base_size/22) {
  
  theme_minimal(base_size = base_size, base_family = base_family, 
                base_line_size = base_line_size, base_rect_size = base_rect_size) 
}

Create a Custom Theme

theme_asap <- function(base_size = 13, base_family = "Asap SemiCondensed", 
                       base_line_size = base_size/22, base_rect_size = base_size/22) {
  
  theme_minimal(base_size = base_size, base_family = base_family, 
                base_line_size = base_line_size, base_rect_size = base_rect_size)  %+replace%
    theme(
      # add your theme changes here
    )
}

Create a Custom Theme

theme_asap <- function(base_size = 13, base_family = "Asap SemiCondensed", 
                       base_line_size = base_size/22, base_rect_size = base_size/22) {
  
  theme_minimal(base_size = base_size, base_family = base_family, 
                base_line_size = base_line_size, base_rect_size = base_rect_size) %+replace%
    theme(
      plot.title = element_text(size = rel(1.3), margin = margin(b = base_size/2),
                                family = "Asap SemiCondensed Extrabold", hjust = 0),
      plot.title.position = "plot",
      plot.caption = element_text(color = "grey30", margin = margin(t = base_size),
                                  size = rel(0.8), hjust = 1, vjust = 1),
      plot.caption.position = "plot",
      axis.title.x = element_text(hjust = 0, vjust = 0, margin = margin(t = base_size/3)),
      axis.title.y = element_text(hjust = 1, vjust = 0, angle = 90, margin = margin(r = base_size/3)),
      panel.background = element_rect(fill = "white", color = "grey20"), 
      panel.border = element_rect(fill = NA, color = "grey20"), 
      plot.background = element_rect(fill = "grey85", color = NA), 
      legend.justification = "top",
      strip.text = element_text(size = rel(1.05), margin = margin(base_size/2, 0, base_size/2, 0)),
      panel.grid.minor = element_blank(), 
      complete = TRUE
    )
}

Apply the Custom Theme

g + 
  theme_asap()

Modify an Existing Theme


⁠%+replace%

replaces the entire element; any element of a theme not specified in e2 will not be present in the resulting theme (i.e. NULL).
Thus this operator can be used to overwrite an entire theme.


+

updates the elements of e1 that differ from elements specified (not NULL) in e2.
Thus this operator can be used to incrementally add or modify attributes of a ggplot theme.

Create a Custom Theme

theme_asap_plus <- function(base_size = 13, base_family = "Asap SemiCondensed", 
                            base_line_size = base_size/22, base_rect_size = base_size/22) {
  
  theme_minimal(base_size = base_size, base_family = base_family, 
                base_line_size = base_line_size, base_rect_size = base_rect_size) + 
    theme( 
      plot.title = element_text(size = rel(1.3), hjust = 0,
                                family = "Asap SemiCondensed Extrabold"),
      plot.title.position = "plot",
      plot.caption = element_text(color = "grey30", margin = margin(t = base_size)),
      plot.caption.position = "plot",
      axis.title.x = element_text(hjust = 0, margin = margin(t = base_size/3)),
      axis.title.y = element_text(hjust = 1, margin = margin(r = base_size/3)),
      panel.background = element_rect(fill = "white", color = "grey20"), 
      panel.border = element_rect(fill = NA, color = "grey20"), 
      plot.background = element_rect(fill = "grey85", color = NA), 
      legend.justification = "top",
      strip.text = element_text(size = rel(1.05), margin = margin(base_size/2, 0, base_size/2, 0)),
      panel.grid.minor = element_blank()
    )
}

+ versus %+replace%

g + theme_asap_plus()

g + theme_asap_replace()

Modify the Custom Theme

g + 
  theme_asap() +
  theme(
    legend.position = "top",
    plot.background = element_rect(
      fill = NA, color = NA
    )
  )

Modify the Custom Theme

g + 
  theme_asap(
    base_size = 9,
    base_family = "Hepta Slab"
  )

Modify the Custom Theme

g + 
  theme_asap(
    base_size = 9,
    base_family = "Hepta Slab"
  ) +
  theme(
    plot.title = element_text(
      family = "Hepta Slab"
    )
  )

Create a Custom Theme

theme_asap_title <- function(base_size = 13, base_family = "Asap SemiCondensed", 
                             title_family = "Asap SemiCondensed Extrabold",
                             base_line_size = base_size/22, base_rect_size = base_size/22) {
  
  if (title_family == "Asap SemiCondensed Extrabold") {
    register_variant(name = "Asap SemiCondensed Extrabold",
                     family = "Asap SemiCondensed",
                     weight = "ultrabold")
  }
  
  theme_minimal(base_size = base_size, base_family = base_family, 
                base_line_size = base_line_size, base_rect_size = base_rect_size) + 
    theme(
      plot.title = element_text(size = rel(1.3), hjust = 0, family = title_family),
      # fill in other theme adjustments here
    )
}

Apply the Custom Theme

g +
  theme_asap_title(
    base_size = 9,
    base_family = "Hepta Slab",
    title_family = "Hepta Slab"
  )

Advanced Font Handling (for Themes)

theme_fonts <- function(base_size = 12, base_line_size = base_size/22, 
                        base_rect_size = base_size/22) {
  
  unavailable <- vector("character")
  
  if (sum(grepl("Hepta Slab", systemfonts::system_fonts()$family)) > 0) {
    systemfonts::register_variant(
      name = "Hepta Slab Extrabold",
      family = "Hepta Slab",
      weight = "ultrabold"
    )
    title_family <- "Hepta Slab Extrabold"
  } else {
    title_family <- ""
    unavailable <- c(unavailable, "Hepta Slab")
  }
  
  if (sum(grepl("Spline Sans", systemfonts::system_fonts()$family)) > 0) {
    base_family <- "Spline Sans"
  } else {
    base_family <- ""
    unavailable <- c(unavailable, "Spline Sans")
  }
  
  if (length(unavailable) > 0) {
    unavailable <- data.frame(
      name = unavailable, 
      url = paste0("https://fonts.google.com/specimen/", sub(" ", "+", unavailable))
    )
    message(paste(
      "Using system default typefaces.", 
      "For proper use, please install the following typeface(s):",
      paste0("  - ", unavailable$name, ": ", unavailable$url, collapse = "\n"),
      "Then restart your R session.",
      sep = "\n"
    ))
  }
  
  theme_asap(base_size = base_size, base_family = base_family, 
             base_line_size = base_line_size, base_rect_size = base_rect_size) + 
    theme(
      plot.title = element_text(size = rel(1.3), hjust = 0, family = title_family)
    )
}

Apply the Custom Theme

g + theme_fonts()

Using system default typefaces.
For proper use, please install the following typeface(s):
 - Hepta Slab: https://fonts.google.com/specimen/Hepta+Slab
 - Spline: https://fonts.google.com/specimen/Spline+Sans
Then restart your R session.

Apply the Custom Theme

g + theme_fonts()

Add More Arguments

  • turn grid lines on and off
  • define alternative styles
  • set a tabular fonts for axis and legend text
  • adjust legend position
  • add/remove plot margin
  • control rendering of text elements

Pro: Users don’t have to use theme().

Con: Users don’t have to use theme().

Add More Arguments

  • turn grid lines on and off
  • define alternative styles
  • set a tabular fonts for axis and legend text
  • adjust legend position
  • add/remove plot margin
  • control rendering of text elements

Pro: Users don’t have to use theme().

Con: Users don’t have to use theme().

Add More Arguments

theme_asap_grid <- function(base_size = 13, base_family = "Asap SemiCondensed", grid = "xy", 
                            base_line_size = base_size/22, base_rect_size = base_size/22) {
  out <- 
    theme_minimal(base_size = base_size, base_family = base_family, 
                  base_line_size = base_line_size, base_rect_size = base_rect_size) + 
    theme(
      panel.grid.major = element_blank(),
      axis.ticks = element_line(color = "grey20"),
      axis.ticks.length = unit(base_size/2, "pt"),
      # fill in other theme adjustments here
    )
  
  if (stringr::str_detect(grid, "x|X")) {
    out <- out + theme(panel.grid.major.x = element_line(color = "grey87"),
                       axis.ticks.x = element_blank(),
                       axis.ticks.length.x = unit(base_size/6, "pt"))
  }
  if (stringr::str_detect(grid, "y|Y")) {
    out <- out + theme(panel.grid.major.y = element_line(color = "grey87"),
                       axis.ticks.y = element_blank(),
                       axis.ticks.length.y = unit(base_size/4, "pt"))
  }
  
  return(out)
}

Apply the Custom Theme

g + 
  theme_asap_grid(
    grid = "y"
  )

Apply the Custom Theme

g + theme_asap_grid()

g + theme_asap_grid(grid = "none")

Apply the Custom Theme

g + 
  theme_asap_grid(
    grid = "all"
  )

Add Checks to the Custom Theme

theme_asap_grid <- function(base_size = 13, base_family = "Asap SemiCondensed", grid = "xy", 
                            base_line_size = base_size/22, base_rect_size = base_size/22) {
  
  if(!stringr::str_detect(grid, "none|x|X|y|Y")) stop('grid must be a character: "none" or any combination of "X", "Y", "x" and "y".')
  
  out <- 
    theme_minimal(base_size = base_size, base_family = base_family, 
                  base_line_size = base_line_size, base_rect_size = base_rect_size) + 
    theme(
      panel.grid.major = element_blank(),
      axis.ticks = element_line(color = "grey20"),
      axis.ticks.length = unit(base_size/2, "pt"),
      # fill in other theme adjustments here
    )
  
  if (stringr::str_detect(grid, "x|X")) {
    out <- out + theme(panel.grid.major.x = element_line(color = "grey87"),
                       axis.ticks.x = element_blank(),
                       axis.ticks.length.x = unit(base_size/6, "pt"))
  }
  if (stringr::str_detect(grid, "y|Y")) {
    out <- out + theme(panel.grid.major.y = element_line(color = "grey87"),
                       axis.ticks.y = element_blank(),
                       axis.ticks.length.y = unit(base_size/4, "pt"))
  }
  
  return(out)
}

Apply the Custom Theme

g + 
  theme_asap_grid(
    grid = "all"
  )

Error in theme_asap_grid(grid = “all”) :
grid must be a character: “none” or any combination of “X”, “Y”, “x” and “y”.

Recap

  • apply complete themes via theme_*() and modify theme defaults via theme()
  • change the appearance for all plots with theme_set() and theme_update()
  • {systemfonts} allows to use non-default typefaces and register font variants
  • use the source code of complete themes to create a custom theme
  • add additional arguments to allow for different styles and settings

Exercise

Exercise

  • Create a corporate or funny custom theme.
    • Make use of an existing complete theme to get started.
    • Pick a non-default font (or multiple) for your theme.
    • Optional: Try working with font variants.
    • Optional: Add other helpful arguments to your theme_* function.

Exercise

  • Create a corporate or funny custom theme.
    • Make use of an existing complete theme to get started.
    • Pick a non-default font (or multiple).
    • Optional: Try working with font variants.
    • Optional: Add other helpful arguments to your theme_* function.
  • Showcase your theme using some example graphics.
    • Save the plots to disk and share them with the group.
    • Did you add some additional arguments?
      Feel free to share your thoughts on “why” and “how”.