Menu

Histogram in R: hist() Explained with Examples

How to make a histogram in R with hist() - choosing breaks, styling bars, switching to the density scale, overlaying a normal curve, and the ggplot2 version.

This page includes runnable editors - edit, run, and see output instantly.

What a Histogram Shows

A histogram answers one question about one numeric variable: how are the values distributed? It chops the range of the data into consecutive intervals (bins), counts how many observations fall into each, and draws a bar per bin whose height is that count. Where the data is dense, bars are tall; where it's sparse, they're short.

That single picture tells you things a mean and standard deviation can't: whether the data has one peak or two, whether it's symmetric or skewed toward one tail, and whether there are stray values far from the rest. It's usually the very first plot to make when you meet a new dataset, right alongside the numbers from descriptive statistics.

In R the function is hist(), and it's one line.

Your First Histogram with hist()

Let's simulate the heights of 200 people - normally distributed around 170 cm with a standard deviation of 10 (see random numbers for what set.seed() and rnorm() do):

set.seed(42)
heights <- rnorm(200, mean = 170, sd = 10)

hist(heights)

R picks the bins itself and draws a bell-shaped stack of bars: short bars near 145 cm, rising through the 160s, peaking around 170, and falling away symmetrically past 190. The vertical axis is labeled "Frequency" - raw counts - and the title and axis label come from the variable name.

The defaults are deliberately plain. The usual dressing-up arguments all work here:

hist(heights,
     main   = "Distribution of heights",
     xlab   = "Height (cm)",
     col    = "steelblue",
     border = "white")

col fills the bars, border colors their outlines - border = "white" gives the clean separated-bar look you see in most publications. Everything the plot() guide says about colors and titles applies unchanged.

Bin Width: the breaks Argument

The most important argument to hist() is breaks, because the bin width decides what story the plot tells. Same data, two settings:

hist(heights, breaks = 5)    # five wide bins: a crude, blocky bell
hist(heights, breaks = 30)   # thirty narrow bins: detail, plus noise

With breaks = 5 the histogram is so coarse that everything looks like a smooth single hump - a second cluster or a gap in the data would be invisible. With breaks = 30 you see fine structure, but random wiggles start masquerading as features. Neither is "correct"; the honest move is to try a few settings and see which features survive.

breaks accepts three forms:

  • A number - breaks = 30 - a suggestion for the bin count. R adjusts it to land on tidy boundaries, so you may get 28 or 33 bins. This surprises everyone once.
  • A vector of cut points - breaks = seq(140, 200, by = 5) - exact bin edges, no negotiation. Use this when bins must align across several histograms you're comparing.
  • A rule name - breaks = "FD" for Freedman–Diaconis, which chooses the width from the data's spread and sample size and behaves well on skewed data.

Density Histograms and the Normal Curve

By default bar heights are counts (freq = TRUE). Setting freq = FALSE rescales the bars so their total area is 1 - the density scale. The shape doesn't change; the vertical axis does. The point of doing this is that a density histogram lives on the same scale as a probability density function, so you can overlay a theoretical curve directly on top of the data:

hist(heights,
     freq = FALSE,
     col = "gray90",
     main = "Heights vs. a normal curve",
     xlab = "Height (cm)")

curve(dnorm(x, mean = mean(heights), sd = sd(heights)),
      add = TRUE, col = "tomato", lwd = 2)

curve() with add = TRUE draws the bell curve over the existing histogram (the x inside dnorm(x, ...) is a placeholder curve() fills in, not a variable of yours). If the bars hug the curve, a normal model is reasonable; where they bulge away from it - a heavy tail, a second peak - the model is missing something. Skip freq = FALSE and the curve will crawl uselessly along the bottom of the plot, because counts and densities are on different scales.

The Numbers Behind the Bars

hist() doesn't just draw - it returns the binning as a list. With plot = FALSE it skips the drawing entirely and just computes, which means you can run this one right here:

h$counts is the histogram as data: each number is the height of one bar. The table(cut(...)) line is the text-mode equivalent - cut() bins the values, table() counts each bin - and it's a decent sanity check to print before or instead of a plot. If you assign without plot = FALSE (h <- hist(heights)), R draws and returns the list; the assignment doesn't suppress the plot.

Histograms with ggplot2

The ggplot2 version trades breaks for binwidth, which is often the more natural dial - you say how wide a bin is in data units instead of how many bins you want:

library(ggplot2)

ggplot(data.frame(heights), aes(x = heights)) +
    geom_histogram(binwidth = 5, fill = "steelblue", color = "white") +
    labs(title = "Distribution of heights", x = "Height (cm)", y = "Count")

binwidth = 5 means every bar covers 5 cm. ggplot2 warns you if you leave binwidth unset and quietly defaults to 30 bins - heed the warning, because the default is rarely the right width for your data. For a quick look at one variable, hist() is less typing; ggplot2 earns its keep when the histogram needs facets, groups, or a consistent theme with the rest of your figures.

What You Take Away

  • A histogram shows the distribution of one numeric variable: hist(x) and you're done.
  • breaks is the argument that matters - a number is only a suggestion, a vector sets exact edges, "FD" picks a defensible width. Always try more than one setting.
  • freq = FALSE switches to the density scale, which is what lets curve(dnorm(...), add = TRUE) overlay a normal curve meaningfully.
  • hist(x, plot = FALSE) returns the bins as data ($breaks, $counts, $mids); table(cut(x, breaks)) is the text-only equivalent.
  • In ggplot2, geom_histogram(binwidth = ...) - and set binwidth yourself.

Next up: the boxplot - the distribution plot that really shines when you're comparing groups side by side.

Frequently Asked Questions

How do you make a histogram in R?

Call hist(x) on a numeric vector. R splits the range of x into bins and draws one bar per bin, with bar height showing how many values fall inside. Add breaks = to control the number of bins and col = to color the bars.

What does the breaks argument do in hist()?

It controls the binning. A single number like breaks = 30 is a suggestion for how many bins to use (R rounds to tidy boundaries), a vector like breaks = seq(140, 200, by = 5) sets the exact cut points, and breaks = "FD" uses the Freedman–Diaconis rule.

How do you overlay a normal curve on a histogram in R?

Draw the histogram on the density scale with freq = FALSE, then add the curve with curve(dnorm(x, mean = mean(data), sd = sd(data)), add = TRUE). On the frequency scale the curve would sit uselessly near zero, so freq = FALSE is essential.

What is the difference between a histogram and a bar chart?

A histogram bins one numeric variable, so the horizontal axis is a continuous scale and the bars touch. A bar chart compares distinct categories, so the bars are separate. In R that's hist() for numbers and barplot() for category counts.

Coddy programming languages illustration

Learn to code with Coddy

GET STARTED