Before a mean-and-error-bar figure, look at the shape: skew, outliers, or two overlapping populations change what summary is honest. This lesson plots distributions properly in ggplot2.
Step 1 — Data with a shape worth seeing
Two groups, one of them subtly bimodal:
library(ggplot2)
set.seed(42)
df <- data.frame(
group = rep(c("A", "B"), each = 150),
value = c(rnorm(150, 10, 1),
c(rnorm(75, 9, 0.7), rnorm(75, 12, 0.7)))
)
Step 2 — A histogram, with deliberate bins
ggplot(df, aes(x = value)) +
geom_histogram(binwidth = 0.5, fill = "grey70", colour = "white") +
labs(x = "Value", y = "Count")
Always set binwidth yourself. The default 30-bin split depends on your data’s range, so two datasets get different bins and stop being comparable; a chosen width (here 0.5 units) means the same thing in every figure of the paper.
Step 3 — Overlay the groups
Fill by group, made translucent so overlap stays visible — identity position, or the bars stack and lie:
ggplot(df, aes(x = value, fill = group)) +
geom_histogram(binwidth = 0.5, alpha = 0.55, position = "identity",
colour = NA) +
labs(x = "Value", y = "Count", fill = NULL)
Group B’s two humps — invisible in a mean ± sd summary — are now the most obvious thing in the figure.
Step 4 — Or draw densities
When counts don’t matter and shape does, smooth densities read cleaner:
ggplot(df, aes(x = value, colour = group)) +
geom_density(linewidth = 0.9) +
labs(x = "Value", y = "Density", colour = NULL) +
theme_minimal(base_size = 13)
Density smoothing has a bandwidth just like histograms have bins — if a bump survives both the histogram and the density, it’s real.
Step 5 — Export or insert
Export / Insert — vector PDF or into the paper with a \label, script saved as the source.
Tip: When groups have very different sizes, add
aes(y = after_stat(density))to the histogram so each group is area-normalised — otherwise the bigger group just looks taller everywhere.