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