; PSY 1903
PSY 1903 Programming for Psychologists

Suggestion Box

Spot an error or have suggestions for improvement on these notes? Let us know!

Custom Themes in ggplot2

Custom themes let us control the overall look of our plots in a consistent way. Instead of rewriting the same theme(...) call in every figure, we can define a theme once, save it as an object or function, and then reuse it across many plots.

In this notes set, we focus on:

  • how themes work under the hood
  • how to build our own theme object
  • how to turn that into a reusable theme function
  • how to control spacing and layout so figures look clean and readable

We assume you already know how to create basic ggplot figures and how to use built-in themes like theme_classic().


1. What is a theme in ggplot2?

In ggplot, a theme controls the non-data parts of the plot:

  • background color and borders
  • gridlines
  • axis text and titles
  • legend position and appearance
  • spacing around and inside the plot

Themes do not change the data, the geoms, or the mappings. They only change how the plot looks.

A theme is just another layer you add with +:

ggplot(npt_data, aes(x = focus_group, y = mean_rt_overall)) +
  geom_col() +
  theme_classic()

2. How themes combine

There are two main ways we work with themes:

  1. Full themes like theme_classic(), theme_bw(), theme_minimal().
  2. Theme adjustments using theme(...).

When we write:

theme_classic() +
theme(axis.title = element_text(size = 14))

the base comes from theme_classic(), and theme(...) updates only the parts we mention.


3. Building a simple reusable theme object

theme_npt <- theme_classic() +
  theme(
    plot.title   = element_text(size = 16, face = "bold"),
    axis.title   = element_text(size = 14),
    axis.text    = element_text(size = 12),
    legend.title = element_text(size = 12),
    legend.text  = element_text(size = 11),
    legend.position = "bottom"
  )

Use it like this:

ggplot(npt_data, aes(x = focus_group, y = mean_rt_overall)) +
  geom_col(fill = "steelblue") +
  labs(title = "Mean RT by Focus Group",
       x = "Focus Group",
       y = "Mean RT (ms)") +
  theme_npt

4. Turning a custom theme into a function

theme_npt <- function(base_size = 12, base_family = "") {
  theme_classic(base_size = base_size, base_family = base_family) +
    theme(
      plot.title   = element_text(size = base_size + 4, face = "bold"),
      axis.title   = element_text(size = base_size + 2),
      axis.text    = element_text(size = base_size),
      legend.title = element_text(size = base_size),
      legend.text  = element_text(size = base_size - 1),
      legend.position = "bottom"
    )
}

5. Tweaking common components inside a theme

theme(
  plot.title = element_text(
    size  = 16,
    face  = "bold",
    hjust = 0.5
  ),
  axis.title.x = element_text(
    margin = margin(t = 8)
  ),
  axis.title.y = element_text(
    margin = margin(r = 8)
  ),
  panel.grid.major = element_line(
    color = "gray85",
    linewidth = 0.3
  ),
  panel.grid.minor = element_blank()
)

6. Using your theme across a whole report

```{r}
theme_set(theme_npt())
```

Override per plot:

+ theme_bw()

7. Spacing and layout

Spacing controls how much room different parts of the plot take up. This can make the difference between a cramped figure and a clean, readable one.

7.1 plot.margin

theme(plot.margin = margin(10, 20, 10, 20))

margin(top, right, bottom, left)

7.2 Axis title and text spacing

theme(
  axis.title.x = element_text(margin = margin(t = 8)),
  axis.title.y = element_text(margin = margin(r = 8))
)

7.3 Facet spacing

theme(panel.spacing = grid::unit(1, "lines"))

7.4 Legend layout

theme(
  legend.position = "bottom",
  legend.margin   = margin(t = 4, b = 4),
  legend.box.spacing = unit(4, "pt")
)

8. Putting it all together

theme_npt <- function(base_size = 12) {
  theme_classic(base_size = base_size) +
    theme(
      plot.title   = element_text(size = base_size + 4, face = "bold", hjust = 0.5),
      axis.title   = element_text(size = base_size + 2),
      axis.text    = element_text(size = base_size),
      axis.title.x = element_text(margin = margin(t = 8)),
      axis.title.y = element_text(margin = margin(r = 8)),
      legend.position = "bottom",
      legend.title    = element_text(size = base_size),
      legend.text     = element_text(size = base_size - 1),
      plot.margin     = margin(10, 20, 10, 20)
    )
}