ggplot2 Facet Wrap in R: A Beginner's Guide

R Published Updated 3 mins read By Leon Wei
ggplot2 Facet Wrap in R: A Beginner's Guide cover image

Quick summary

Summarize this blog with AI

Introduction

facet_wrap() draws a panel for each observed combination of faceting variables and wraps those panels into rows and columns. Use fixed scales to compare magnitudes across panels; free scales emphasize variation within each panel. We will plot the same fictional monthly counts both ways and verify the panel layout.

Tested with R 4.5.1 and ggplot2 4.0.0. Run the code blocks in order. Install the named packages before starting; inputs are supplied below.

Prepare one explicit observation per region and month

The original data retains its missing observation. The plot uses a clearly counted complete subset, rather than silently treating NA as zero.

library(ggplot2)
observations <- data.frame(region = factor(c(rep("North", 4), rep("South", 4), "North"),
                                          levels = c("North", "South", "Inactive")),
                           month = c(rep(1:4, 2), 5),
                           count = c(10, 12, 11, 14, 100, 130, 120, 160, NA))
keep <- complete.cases(observations[, c("region", "month", "count")])
plot_data <- observations[keep, , drop = FALSE]
stopifnot(sum(!keep) == 1L, nrow(plot_data) == 8L)
stopifnot(!anyDuplicated(plot_data[, c("region", "month")]))

If your source has multiple rows per month and region, choose and validate an aggregation before drawing a trend. A wide-to-long reshape is called pivot_longer, not a longer-to-wider operation.

Use fixed scales for a magnitude comparison

The factor levels determine panel order. A line and point both show the monthly values; named labels make the strip text deliberate.

labels <- c(North = "North region", South = "South region", Inactive = "No observations")
base_plot <- ggplot(plot_data, aes(month, count)) +
  geom_line(colour = "#1b6a83", linewidth = 0.7) +
  geom_point(colour = "#1b6a83", size = 2.6) +
  scale_x_continuous(breaks = 1:4) +
  labs(x = "Month", y = "Recorded orders", title = "Fixed scales compare magnitudes") +
  theme_minimal(base_size = 12)
p_fixed <- base_plot + facet_wrap(vars(region), ncol = 2, labeller = as_labeller(labels))
print(p_fixed)
layout <- ggplot_build(p_fixed)$layout$layout
stopifnot(nrow(layout) == 2L, length(unique(layout$SCALE_Y)) == 1L)
Monthly orders with fixed facet scalesNorth counts 10 to 14 and South counts 100 to 160 share one y scale, making the difference in magnitude clear. North region South region 1 2 3 4 1 2 3 4 40 80 120 160 Month Recorded orders Fixed scales compare magnitudes
Fixed y scales: South has substantially larger counts than North.

The South panel is visibly higher on the shared scale. Equal distances on the y-axis mean equal changes in counts across panels.

Use free y scales only when the question needs them

This changes only the facet scales and title, leaving the plotted observations unchanged.

p_free <- base_plot + facet_wrap(vars(region), ncol = 2, scales = "free_y",
                                 labeller = as_labeller(labels)) +
  labs(title = "Free y scales reveal within-region changes")
print(p_free)
free_layout <- ggplot_build(p_free)$layout$layout
stopifnot(length(unique(free_layout$SCALE_Y)) == 2L)
stopifnot(identical(ggplot_build(p_fixed)$data[[2]]$y,
                    ggplot_build(p_free)$data[[2]]$y))
Monthly orders with free y scalesThe same eight observations use separate y ranges in the North and South panels. Both local trends are visible, but screen heights are not comparable. North region South region 1 2 3 4 1 2 3 4 100 120 140 160 10 11 12 13 14 Month Recorded orders Free y scales reveal within-region changes
Free y scales: inspect the different axis labels before comparing panels.

Read the labels: the two panels now have different y ranges. A steeper-looking line or a similar screen height does not imply the same absolute change. The facet_wrap reference distinguishes free scales from free panel space; space="free_y" constrains the layout to one column.

Check empty levels and bar mappings

Unused factor levels disappear by default. Set drop=FALSE when showing a known but empty group helps the reader.

p_empty <- base_plot + facet_wrap(vars(region), drop = FALSE)
stopifnot(nrow(ggplot_build(p_empty)$layout$layout) == 3L)
# Existing numeric heights need both x and y; geom_col does not count rows.
p_bars <- ggplot(plot_data, aes(month, count)) + geom_col() + facet_wrap(vars(region))
stopifnot(nrow(ggplot_build(p_bars)$data[[1]]) == 8L)
stopifnot(sum(ggplot_build(p_bars)$data[[1]]$y) == sum(plot_data$count))

For entirely empty input, report “no observations” before plotting rather than implying zero activity. Choose readable labels and a manageable panel count. Free scales and dropped levels are analytical choices, not automatic visual improvements.

Primary references

Interview Prep

Begin Your SQL, Python, and R Journey

Master 230 interview-style coding questions and build the data skills needed for analyst, scientist, and engineering roles.

Related Articles

All Articles
Comments in R: A Beginner's Guide cover image
r May 2, 2024

Comments in R: A Beginner's Guide

Unlock the secrets of effective commenting in R programming with this comprehensive guide designed for beginners. Learn best practices and tips …