Deep dive: themes

Lecture 09

Dr. Benjamin Soltoff

Cornell University
INFO 3312/5312 - Spring 2026

February 19, 2026

Announcements

Announcements

  • Homework 03
  • Revised project 01 proposals due tonight by 11:59pm
  • Aim to have a solid draft for your report (question 1) before class next Thursday

Learning objectives

  • Define {ggplot2} themes and their elements
  • Identify existing and extension themes for {ggplot2}
  • Implement custom fonts for charts
  • Develop custom themes based on organizational style guides

Themes

Themes in {ggplot2}

A theme is a list of descriptions for various parts of the plot

  • Size of your titles
  • Colors of your panels
  • Thickness of your grid lines
  • Placement of legends

Themes are declared using theme() and can be applied to any plot

Complete themes

p +
  theme_grey()

Complete themes

p +
  theme_minimal()

Extension theme

library(ggthemes)

p +
  theme_tufte()

Or this

library(tvthemes)

p +
  theme_parksAndRec()

Tweaking complete themes

p +
  theme_minimal(base_size = 14)

Tweaking complete themes

p +
  theme_minimal(
    base_family = "Roboto",
    header_family = "Roboto Slab"
  )

Ink/paper/accent

p + 
  # Turning off these aesthetics to prevent grouping
  aes(shape = NULL, color = NULL) +
  geom_smooth(method = "lm") +
  theme_bw(
    ink = "#BBBBBB", 
    paper = "#333333", 
    accent = "red"
  )

Why customize themes?

  • Apply a consistent style across multiple plots
  • Create a unique style that reflects your brand or personality
  • Make your plots visually distinctive
  • Improve readability and accessibility of your plots
  • Better communicate the story you want to tell with your data

The anatomy of a ggplot() theme

Theme system

Theme elements

Each element in the plot can be targeted

  • Plot title = plot.title
  • Grid lines = panel.grid
  • Legend background = legend.background

Use special functions to manipulate specific elements

  • Line-based things (axis lines, grid lines) = element_line()
  • Rectangular things (backgrounds) = element_rect()
  • Text-based things = element_text()
  • Default properties of layers = element_geom()
  • Disable element completely = element_blank()

How to learn theme()

  • The theme() function has 147 possible arguments
  • You can get hyper-specific with things like axis.ticks.length.x.bottom
  • The only way to learn how to use theme() is to use it and tinker with it

{gapminder}

library(gapminder)
gapminder
# A tibble: 1,704 × 6
   country     continent  year lifeExp      pop gdpPercap
   <fct>       <fct>     <int>   <dbl>    <int>     <dbl>
 1 Afghanistan Asia       1952    28.8  8425333      779.
 2 Afghanistan Asia       1957    30.3  9240934      821.
 3 Afghanistan Asia       1962    32.0 10267083      853.
 4 Afghanistan Asia       1967    34.0 11537966      836.
 5 Afghanistan Asia       1972    36.1 13079460      740.
 6 Afghanistan Asia       1977    38.4 14880372      786.
 7 Afghanistan Asia       1982    39.9 12881816      978.
 8 Afghanistan Asia       1987    40.8 13867957      852.
 9 Afghanistan Asia       1992    41.7 16317921      649.
10 Afghanistan Asia       1997    41.8 22227415      635.
# ℹ 1,694 more rows

Basic plot

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 = label_currency(scale_cut = cut_short_scale())) +
  # Format with commas
  scale_size_continuous(labels = label_comma()) +
  # Use dark 3
  scale_color_discrete_qualitative(palette = "Dark 3") +
  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(facets = vars(year)) +
  theme_grey()

base_plot

Change the theme

base_plot +
  theme_minimal()

Create a custom theme

# change base font and size
theme_pretty <- function(
  base_family = "Atkinson Hyperlegible",
  base_size = 14,
  ink = "#222222", # Cornell dark gray
  paper = "#FFFFFF", # White
  accent = "#B31B1B", # Carnellian
  ...
) {
  theme_minimal(
    base_family = base_family,
    base_size = base_size,
    ink = ink,
    paper = paper,
    accent = accent,
    ...
  ) +
    theme_sub_panel(
      # Remove minor grid lines
      grid.minor = element_blank(),
      # Add a thin grey border around all the plots to tie in the facet titles
      border = element_rect(color = "grey90", fill = NA)
    ) +
    theme_sub_plot(
      # Bold, bigger title
      title = element_text(face = "bold", size = rel(1.7)),
      # Plain, slightly bigger subtitle that is grey
      subtitle = element_text(
        face = "plain",
        size = rel(1.3),
        color = "grey70"
      ),
      # Italic, smaller, grey caption that is left-aligned
      caption = element_text(
        face = "italic",
        size = rel(0.7),
        color = "grey70",
        hjust = 0
      )
    ) +
    theme_sub_legend(
      # Bold legend titles
      title = element_text(face = "bold")
    ) +
    theme_sub_strip(
      # Bold, slightly larger facet titles that are left-aligned for the sake of repetition
      text = element_text(face = "bold", size = rel(1.1), hjust = 0),
      # Add a light grey background to the facet titles, with no borders
      background = element_rect(fill = "grey90", color = NA)
    ) +
    theme_sub_axis(
      # Bold axis titles
      title = element_text(face = "bold"),
    ) +
    theme_sub_axis_x(
      # Add some space above the x-axis title and make it left-aligned
      title = element_text(margin = margin(t = 10), hjust = 0)
    ) +
    theme_sub_axis_y(
      # Add some space to the right of the y-axis title and make it top-aligned
      title = element_text(margin = margin(r = 10), hjust = 1)
    )
}

Applied to existing plot

base_plot +
  theme_pretty()

Render in a document

base_plot +
  theme_pretty(base_size = 11)

Applied to another plot

ggplot(
  data = drop_na(penguins, sex),
  mapping = aes(x = bill_len, y = body_mass, color = str_to_title(sex))
) +
  geom_point(size = 3, alpha = 0.5) +
  scale_color_discrete_qualitative(palette = "Dark 3") +
  scale_y_continuous(labels = label_comma()) +
  facet_wrap(facets = vars(species)) +
  labs(
    x = "Bill length (mm)", y = "Body mass (g)", color = "Sex",
    title = "Gentoo penguins are the largest",
    subtitle = "But females are typically smaller than males",
    caption = "Here's a caption"
  ) +
  theme_pretty()

A note on custom fonts

Custom fonts in {ggplot2}

  • Rendering text and custom fonts has been historically difficult in R (and many other programming languages)
  • Requires unifying the graphics device, operating system, text rendering, and R
  • Several approaches exist with {ggplot2} to simplify this process

{ragg}

  • {ragg} is a package that provides a consistent way to render fonts across different devices
  • Install fonts directly on system and use {ragg} as the graphics device
  • Many great uses for this workflow! But it requires the ability to directly install fonts on the device…

Can’t install fonts directly on system?

Use {systemfonts} and require_font() to download font automatically from Google Fonts or Font Squirrel.

Ensure graphs are rendered using {ragg}

  • ggsave() automatically uses {ragg} as of {ggplot2} 3.3.4
  • In Quarto, set the dev chunk option
knitr:
  opts_chunk:
    dev: ragg_png

Application exercise

ae-08

Instructions

  • Go to the course GitHub org and find your ae-08 (repo name will be suffixed with your GitHub name).
  • Clone the repo in Positron, run renv::restore() to install the required packages, open the Quarto document in the repo, and follow along and complete the exercises.
  • Render, commit, and push your edits by the AE deadline – end of the day

Wrap up

Wrap up

  • {ggplot2} uses theming to control the visual appearance of charts
  • Use custom themes to apply consistent styles across multiple plots