Statistical figures are usually comparisons: condition A against condition B, or the same relationship across several samples. ggplot2 has one idea for each — map the group to an aesthetic, or facet into small multiples — and this lesson uses both.
Step 1 — A grouped dataset
library(ggplot2)
df <- data.frame(
time = rep(1:6, times = 2),
signal = c(2.0, 3.9, 8.1, 15.8, 32.5, 63.0,
2.1, 3.0, 4.4, 6.5, 9.8, 14.6),
condition = rep(c("treated", "control"), each = 6)
)
Step 2 — Map the group to colour
One aesthetic turns a pile of points into a comparison:
ggplot(df, aes(x = time, y = signal, colour = condition)) +
geom_line(linewidth = 0.8) +
geom_point(size = 2) +
labs(x = "Time (h)", y = "Signal", colour = NULL)
ggplot2 splits the data by condition, draws each with its own colour, and builds the legend. colour = NULL in labs() drops the redundant legend title.
Step 3 — Or facet into small multiples
When curves overlap too much to read, give each group its own panel instead:
ggplot(df, aes(x = time, y = signal)) +
geom_line() +
geom_point(size = 1.5) +
facet_wrap(~ condition) +
labs(x = "Time (h)", y = "Signal")
facet_wrap(~ condition) makes one panel per condition with shared axes — the honest way to compare shapes without spaghetti.
Step 4 — Theme it for print
The grey default is fine on screen and noisy on paper. A minimal theme with a readable base size:
ggplot(df, aes(x = time, y = signal, colour = condition)) +
geom_line(linewidth = 0.8) +
geom_point(size = 2) +
labs(x = "Time (h)", y = "Signal", colour = NULL) +
theme_minimal(base_size = 13) +
theme(legend.position = "top")
A legend on top keeps the plotting area full-width — worth it in a single-column figure.
Step 5 — Export or insert
As always: Export / Insert for a vector PDF or a placed figure with a \label, with the script saved as the figure’s source.
Tip: If the y-values span decades, add
scale_y_log10()as one more layer — an exponential that looks like a hockey stick becomes a straight line whose slope readers can actually judge.