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:
- Full themes like
theme_classic(),theme_bw(),theme_minimal(). - 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)
)
}