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.