All guides
histogramRggplot2how tostatistics

How to Make a Histogram in R (Base R and ggplot2)

Create histograms in R using base R's hist() function and ggplot2. Includes bin count control, density overlay, and publication-ready styling.

By ChartsMakers Editorial Team·4 min read·Published 2026-05-18·Reviewed 2026-09-23

R is a natural fit for histograms: base R can draw one with a single hist() call, and ggplot2 gives you more control over themes, facets, and layered statistics. Here's how to use both, and when each makes sense.

Base R histogram

The hist() function is the fastest way to inspect a distribution:

data <- c(62, 65, 68, 71, 73, 75, 78, 81, 85, 87, 42, 95, 88, 76, 73)

hist(data,
     main = "Test Score Distribution",
     xlab = "Score",
     col  = "#0F766E",
     border = "white")

hist() automatically chooses breaks when you do not specify them. The result is quick — no packages required.

Controlling the number of bins

  • breaks as integer — breaks = 10 suggests 10 bins; R may adjust slightly for clean boundaries
  • breaks as vector — breaks = seq(40, 100, by = 10) sets exact bin boundaries from 40 to 100 in steps of 10
  • breaks = "FD" — Freedman-Diaconis rule, better for skewed data with outliers
  • breaks = "Scott" — Scott's rule, useful when the distribution is roughly smooth and not too skewed
# Exact bin boundaries
hist(data,
     breaks = seq(40, 100, by = 10),
     main   = "Test Scores (10-point bins)",
     xlab   = "Score",
     col    = "#0F766E",
     border = "white")

Y-axis: frequency vs density

# Frequency (count) — default
hist(data, freq = TRUE)

# Density — area of all bars sums to 1
hist(data, freq = FALSE)

Use freq = FALSE when you want to overlay a density curve.

Adding a density curve overlay

hist(data,
     freq   = FALSE,
     col    = "#ccece6",
     border = "white",
     main   = "Distribution with Density Curve",
     xlab   = "Score")

lines(density(data), col = "#0F766E", lwd = 2)

This combination — histogram bars plus a smoothed density curve — is common in exploratory data analysis and statistics courses.

ggplot2 histogram

For publication-ready charts with more styling control, use ggplot2:

library(ggplot2)

data_df <- data.frame(score = c(62, 65, 68, 71, 73, 75, 78,
                                 81, 85, 87, 42, 95, 88, 76, 73))

ggplot(data_df, aes(x = score)) +
  geom_histogram(binwidth = 10, fill = "#0F766E", color = "white") +
  labs(title = "Test Score Distribution",
       x = "Score",
       y = "Count") +
  theme_minimal()

Key ggplot2 parameters:

  • binwidth — width of each bin in data units (preferred over bins for interpretability)
  • bins — number of bins (ggplot2 defaults to 30 when neither bins nor binwidth is set)
  • fill — bar fill color
  • color — bar border color

ggplot2 with density Y-axis

ggplot(data_df, aes(x = score)) +
  geom_histogram(aes(y = after_stat(density)),
                 binwidth = 10,
                 fill = "#0F766E", color = "white", alpha = 0.7) +
  geom_density(color = "#0D5C55", linewidth = 1) +
  labs(title = "Distribution with Density Overlay",
       x = "Score", y = "Density") +
  theme_minimal()

after_stat(density) replaces the deprecated ..density.. syntax in recent ggplot2 versions.

Faceted histograms for group comparison

library(ggplot2)

df <- data.frame(
  score = c(65, 70, 75, 80, 85, 55, 60, 70, 80, 90),
  class = c(rep("Class A", 5), rep("Class B", 5))
)

ggplot(df, aes(x = score, fill = class)) +
  geom_histogram(binwidth = 10, color = "white", alpha = 0.8) +
  facet_wrap(~class) +
  scale_fill_manual(values = c("#0F766E", "#6366f1")) +
  theme_minimal() +
  theme(legend.position = "none")

facet_wrap(~class) creates one panel per group — cleaner than overlapping histograms.

FAQ

Q

Should I use hist() or ggplot2?

Use hist() for quick exploratory analysis in your R console — it's one line and quickly shows the distribution. Use ggplot2 when you need a polished chart for a report, paper, or presentation — the theming and faceting options are more flexible.

Q

How do I save the histogram to a file?

For base R: wrap in png("histogram.png"); hist(data); dev.off(). For ggplot2: use ggsave("histogram.png", width = 8, height = 5, dpi = 300) after your ggplot call. For SVG: use ggsave("histogram.svg").

Q

Why does hist() produce a different number of bins than I specified?

The breaks integer is a suggestion, not a hard requirement. R rounds bin boundaries to "pretty" numbers, which may result in slightly more or fewer bins than specified. Use breaks = seq(min, max, by = width) to set exact boundaries.

Q

How do I add a normal distribution curve to my histogram?

In base R with freq = FALSE: after the hist() call, add curve(dnorm(x, mean(data), sd(data)), add = TRUE, col = "red", lwd = 2). This overlays a normal density curve fitted to your data's mean and standard deviation.

References

How this guide is reviewed

This guide is maintained by the ChartsMakers Editorial Team and reviewed for statistical accuracy, product behavior, and clarity. Sources used for factual checks are listed in the References section. See our editorial policy.

Want to try it with your data?

Paste your data into the Histogram Maker.