; PSY 1903
PSY 1903 Programming for Psychologists

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)
    )
}