How to Create a Histogram in ggplot2

• Bin width selection fundamentally changes histogram interpretation—default bins rarely tell the full story, so always experiment with multiple bin configurations before drawing conclusions

Key Insights

• Bin width selection fundamentally changes histogram interpretation—default bins rarely tell the full story, so always experiment with multiple bin configurations before drawing conclusions • Use geom_histogram() with binwidth rather than bins when working with measurements that have meaningful units (like miles per gallon or dollars) • Overlaying density curves and statistical markers transforms histograms from simple frequency displays into analytical tools that reveal distribution shape and central tendencies

Introduction to Histograms and ggplot2

Histograms are the workhorse visualization for understanding data distribution. Unlike bar charts that display categorical frequencies, histograms bin continuous numerical data into intervals and show how many observations fall within each range. This makes them essential for identifying skewness, detecting outliers, and assessing whether your data follows expected patterns like normal distribution.

ggplot2 has become the de facto standard for data visualization in R, and for good reason. Its grammar of graphics approach makes it intuitive to layer visual elements, customize aesthetics, and create publication-ready plots with minimal code. The geom_histogram() function provides extensive control over how you represent distributions while maintaining consistency with ggplot2’s overall syntax.

Basic Histogram Syntax

Creating a histogram in ggplot2 requires only two components: your data and the variable you want to visualize. The geom_histogram() function handles the binning and counting automatically.

library(ggplot2)

# Basic histogram using mtcars dataset
ggplot(mtcars, aes(x = mpg)) +
  geom_histogram()

This produces a functional histogram, though ggplot2 will warn you that it’s using bins = 30 by default. The x-axis shows miles per gallon values, while the y-axis displays the count of cars in each bin. Notice that you only specify the x aesthetic—histograms calculate the y-axis (count) automatically.

The default gray fill with black borders works for exploratory analysis, but you’ll want more control for presentations or publications.

Customizing Bins and Binwidth

The single most important decision when creating a histogram is choosing bin width. Too few bins obscure important patterns; too many create noise that prevents pattern recognition.

ggplot2 offers two parameters: bins specifies the number of bins to create, while binwidth sets the width of each bin in the units of your data. Use binwidth when your measurements have meaningful units.

library(gridExtra)  # For side-by-side comparison

# Create three histograms with different bin settings
p1 <- ggplot(mtcars, aes(x = mpg)) +
  geom_histogram(bins = 10, fill = "steelblue", color = "white") +
  labs(title = "bins = 10") +
  theme_minimal()

p2 <- ggplot(mtcars, aes(x = mpg)) +
  geom_histogram(bins = 30, fill = "steelblue", color = "white") +
  labs(title = "bins = 30") +
  theme_minimal()

p3 <- ggplot(mtcars, aes(x = mpg)) +
  geom_histogram(binwidth = 5, fill = "steelblue", color = "white") +
  labs(title = "binwidth = 5") +
  theme_minimal()

grid.arrange(p1, p2, p3, ncol = 3)

With 10 bins, you see the general bimodal pattern in the mtcars data. With 30 bins, individual variations become apparent but the overall pattern gets harder to discern. The binwidth = 5 approach groups cars into 5 mpg intervals (10-15, 15-20, etc.), which aligns with how people actually think about fuel efficiency.

For the mtcars dataset, a binwidth of 2-3 mpg typically provides the best balance. Start with the Freedman-Diaconis rule (binwidth = 2 * IQR / n^(1/3)) as a baseline, then adjust based on what patterns you need to reveal.

Styling and Aesthetics

Raw histograms convey information but lack visual polish. ggplot2 provides extensive styling options to create professional visualizations.

ggplot(mtcars, aes(x = mpg)) +
  geom_histogram(binwidth = 2.5, 
                 fill = "#2E86AB", 
                 color = "#023047", 
                 alpha = 0.8) +
  labs(title = "Distribution of Fuel Efficiency",
       subtitle = "1974 Motor Trend Car Road Tests",
       x = "Miles per Gallon",
       y = "Number of Vehicles") +
  theme_minimal(base_size = 14) +
  theme(plot.title = element_text(face = "bold"),
        panel.grid.minor = element_blank(),
        panel.grid.major.x = element_blank())

This example demonstrates several key improvements:

  • fill controls the interior color of bars
  • color sets the border color (use contrasting shades of the same hue)
  • alpha adds transparency (0.7-0.9 works well for most cases)
  • theme_minimal() removes the gray background and simplifies gridlines
  • Custom theme adjustments remove visual clutter

The result is a histogram that looks professional while remaining readable and informative.

Adding Statistical Overlays

Histograms become analytical tools when you overlay statistical information. Density curves show the smoothed distribution shape, while reference lines highlight important values like means or thresholds.

# Calculate mean for vertical line
mean_mpg <- mean(mtcars$mpg)

ggplot(mtcars, aes(x = mpg)) +
  geom_histogram(aes(y = after_stat(density)), 
                 binwidth = 2.5,
                 fill = "#2E86AB", 
                 color = "#023047", 
                 alpha = 0.7) +
  geom_density(color = "#A23B72", 
               linewidth = 1.2) +
  geom_vline(xintercept = mean_mpg, 
             color = "#F18F01", 
             linetype = "dashed", 
             linewidth = 1) +
  annotate("text", 
           x = mean_mpg + 2, 
           y = 0.05, 
           label = paste("Mean =", round(mean_mpg, 1)),
           color = "#F18F01") +
  labs(title = "Fuel Efficiency Distribution with Density Overlay",
       x = "Miles per Gallon",
       y = "Density") +
  theme_minimal()

Critical detail: when adding density curves, change the histogram’s y-axis to density using aes(y = after_stat(density)). Otherwise, the histogram shows counts while the density curve uses proportions, creating a scaling mismatch.

The geom_vline() adds a vertical reference line at the mean, making it easy to see how the distribution centers. The annotate() function labels this line with the actual mean value.

Faceting and Grouped Histograms

Comparing distributions across categories reveals patterns that single histograms miss. ggplot2 provides two approaches: faceting creates separate panels, while grouped histograms overlay distributions.

# Faceted histograms by transmission type
ggplot(mtcars, aes(x = mpg)) +
  geom_histogram(binwidth = 2.5, 
                 fill = "#2E86AB", 
                 color = "white") +
  facet_wrap(~ am, 
             labeller = labeller(am = c("0" = "Automatic", 
                                       "1" = "Manual"))) +
  labs(title = "Fuel Efficiency by Transmission Type",
       x = "Miles per Gallon",
       y = "Count") +
  theme_minimal() +
  theme(strip.background = element_rect(fill = "#E8E8E8"),
        strip.text = element_text(face = "bold"))

# Overlapping histograms (use with caution)
ggplot(mtcars, aes(x = mpg, fill = factor(am))) +
  geom_histogram(binwidth = 2.5, 
                 alpha = 0.6, 
                 position = "identity") +
  scale_fill_manual(values = c("#2E86AB", "#A23B72"),
                    labels = c("Automatic", "Manual"),
                    name = "Transmission") +
  labs(title = "Fuel Efficiency by Transmission Type",
       x = "Miles per Gallon",
       y = "Count") +
  theme_minimal()

Faceting works better for comparing distributions because it eliminates visual overlap. Use position = "identity" with overlapping histograms rather than the default position = "stack", which creates misleading stacked bars.

Common Pitfalls and Best Practices

The most frequent mistake is accepting default bin settings without investigation. Always create histograms with at least three different bin widths to ensure you’re not missing important patterns or creating artificial ones.

# Poor bin selection - hides bimodal distribution
poor_bins <- ggplot(mtcars, aes(x = mpg)) +
  geom_histogram(bins = 5, fill = "steelblue") +
  labs(title = "Poor: bins = 5 (pattern hidden)") +
  theme_minimal()

# Optimal bin selection - reveals bimodal pattern
optimal_bins <- ggplot(mtcars, aes(x = mpg)) +
  geom_histogram(binwidth = 2, fill = "steelblue") +
  labs(title = "Better: binwidth = 2 (pattern visible)") +
  theme_minimal()

grid.arrange(poor_bins, optimal_bins, ncol = 2)

With only 5 bins, the mtcars data appears roughly uniform. With a 2 mpg binwidth, the bimodal distribution becomes apparent—there’s a cluster of fuel-efficient cars and another cluster of gas guzzlers.

Additional best practices:

Always label your axes with units. “mpg” is better than “x”, and “Miles per Gallon” is better still.

Consider alternatives when histograms fail. With fewer than 20 observations, use a strip plot or dot plot instead. For comparing multiple distributions, violin plots or ridge plots often communicate more clearly than overlapping histograms.

Use consistent bin widths when comparing histograms. If you’re showing before/after distributions or comparing groups, identical binning ensures fair comparison.

Avoid 3D histograms and unnecessary embellishments. They reduce readability without adding information.

Histograms are deceptively simple—creating one takes seconds, but creating one that accurately represents your data and communicates clearly requires thought. ggplot2 gives you the tools; understanding your data and choosing appropriate parameters makes the difference between decoration and insight.

Liked this? There's more.

Every week: one practical technique, explained simply, with code you can use immediately.