How to Create a Faceted Plot in ggplot2

Faceting is one of ggplot2's most powerful features for exploratory data analysis. Instead of cramming multiple groups onto a single plot with different colors or shapes, faceting creates separate...

Key Insights

  • Faceting splits your data into multiple panels based on categorical variables, making it easier to compare patterns across groups than overplotting everything on one chart
  • Use facet_wrap() for single-variable faceting with automatic grid layout, and facet_grid() when you need explicit two-dimensional control with row and column variables
  • Free scales (scales = "free") are essential when facet groups have different value ranges, but use them carefully as they can make cross-panel comparisons misleading

Introduction to Faceting

Faceting is one of ggplot2’s most powerful features for exploratory data analysis. Instead of cramming multiple groups onto a single plot with different colors or shapes, faceting creates separate panels for each subset of your data. This approach eliminates overplotting, makes patterns immediately visible, and allows each group to tell its own story.

ggplot2 provides two primary faceting functions: facet_wrap() and facet_grid(). Use facet_wrap() when you have one categorical variable and want ggplot2 to arrange panels in a space-efficient grid. Use facet_grid() when you need explicit control over a two-dimensional layout based on two categorical variables. The choice between them fundamentally changes how your visualization communicates relationships in your data.

Basic Faceting with facet_wrap()

The facet_wrap() function is your workhorse for single-variable faceting. It takes a categorical variable and creates a separate panel for each level, wrapping them into a grid layout automatically.

Here’s a practical example using the built-in mpg dataset:

library(ggplot2)

# Basic faceted scatter plot
ggplot(mpg, aes(x = displ, y = hwy)) +
  geom_point(alpha = 0.6) +
  facet_wrap(~class) +
  labs(
    title = "Highway MPG vs Engine Displacement by Vehicle Class",
    x = "Engine Displacement (L)",
    y = "Highway MPG"
  ) +
  theme_minimal()

The tilde syntax (~class) tells ggplot2 to facet by the class variable. Each vehicle class gets its own panel, making it trivial to see that SUVs and pickups cluster in the high-displacement, low-MPG region while compacts show the opposite pattern.

By default, facet_wrap() uses the same scales across all panels. This is crucial for accurate comparison—if each panel had different axis ranges, you’d be comparing apples to oranges. However, sometimes you need flexibility, which brings us to scale customization.

Two-Dimensional Faceting with facet_grid()

When your data has natural groupings along two dimensions, facet_grid() provides explicit control over the layout. The formula syntax rows ~ cols determines which variable defines rows and which defines columns.

# Create a subset of mpg data for clearer visualization
mpg_subset <- subset(mpg, year %in% c(1999, 2008) & 
                          class %in% c("compact", "suv", "pickup"))

ggplot(mpg_subset, aes(x = displ, y = hwy)) +
  geom_point(aes(color = factor(cyl)), size = 2) +
  facet_grid(year ~ class) +
  labs(
    title = "Vehicle Efficiency by Class and Year",
    x = "Engine Displacement (L)",
    y = "Highway MPG",
    color = "Cylinders"
  ) +
  theme_bw() +
  theme(legend.position = "bottom")

This creates a 2×3 grid with years as rows and vehicle classes as columns. The layout makes it immediately obvious whether efficiency patterns changed between 1999 and 2008 for each vehicle class. You can also use . as a placeholder: facet_grid(. ~ class) creates a single row, while facet_grid(class ~ .) creates a single column.

The key difference from facet_wrap() is that facet_grid() maintains alignment. Every panel in the same row shares the same y-axis scale, and every panel in the same column shares the same x-axis scale. This alignment is perfect for direct visual comparison but can be restrictive when groups have vastly different ranges.

Customizing Facet Appearance

The default faceting behavior works well, but real-world data often demands customization. The most important parameter is scales, which controls whether axes are fixed or free across panels.

# Free scales example with economics dataset
library(tidyr)

# Reshape economics data for faceting
econ_long <- pivot_longer(
  economics,
  cols = c(unemploy, uempmed, psavert),
  names_to = "metric",
  values_to = "value"
)

ggplot(econ_long, aes(x = date, y = value)) +
  geom_line(color = "steelblue", linewidth = 0.8) +
  facet_wrap(~metric, scales = "free_y", ncol = 1) +
  labs(
    title = "US Economic Indicators Over Time",
    x = NULL,
    y = NULL
  ) +
  theme_minimal() +
  theme(strip.text = element_text(face = "bold"))

Setting scales = "free_y" allows each panel to use its own y-axis range. This is essential here because unemployment numbers, unemployment duration, and savings rates operate on completely different scales. Without free scales, the smaller-valued series would be compressed into illegibility.

Your options for scales are:

  • "fixed" (default): Same scales for all panels
  • "free_x": Free x-axis, fixed y-axis
  • "free_y": Free y-axis, fixed x-axis
  • "free": Both axes free

Custom strip labels improve readability significantly:

# Custom facet labels
metric_labels <- c(
  unemploy = "Total Unemployed (thousands)",
  uempmed = "Median Unemployment Duration (weeks)",
  psavert = "Personal Savings Rate (%)"
)

ggplot(econ_long, aes(x = date, y = value)) +
  geom_line(color = "steelblue", linewidth = 0.8) +
  facet_wrap(
    ~metric, 
    scales = "free_y", 
    ncol = 1,
    labeller = labeller(metric = metric_labels)
  ) +
  labs(title = "US Economic Indicators Over Time", x = NULL, y = NULL) +
  theme_minimal()

The labeller argument accepts named vectors for simple replacements or more complex labeller functions for advanced formatting.

Advanced Faceting Techniques

Faceting becomes more powerful when combined with factor reordering and multiple variables. Control the order of your facets by setting factor levels explicitly:

# Reorder facets by median highway MPG
mpg$class_ordered <- reorder(mpg$class, mpg$hwy, FUN = median)

ggplot(mpg, aes(x = displ, y = hwy)) +
  geom_point(alpha = 0.5) +
  geom_smooth(method = "lm", se = FALSE, color = "red", linewidth = 0.8) +
  facet_wrap(~class_ordered, ncol = 4) +
  labs(
    title = "Efficiency by Vehicle Class (Ordered by Median MPG)",
    x = "Engine Displacement (L)",
    y = "Highway MPG"
  ) +
  theme_minimal()

This orders panels from most to least efficient vehicle classes, creating a natural visual progression that tells a story.

For faceting by multiple variables with facet_wrap(), use the vars() function:

ggplot(mpg, aes(x = displ, y = hwy)) +
  geom_point(alpha = 0.5) +
  facet_wrap(vars(year, drv), ncol = 3) +
  labs(
    title = "Efficiency by Year and Drive Type",
    x = "Engine Displacement (L)",
    y = "Highway MPG"
  ) +
  theme_bw()

This creates panels for every combination of year and drive type, giving you a comprehensive view of how these factors interact.

Practical Use Cases and Best Practices

Faceting excels in time series analysis where you need to compare trends across categories. Here’s a complete real-world example:

# Simulate sales data for multiple products and regions
set.seed(42)
sales_data <- expand.grid(
  date = seq(as.Date("2022-01-01"), as.Date("2023-12-31"), by = "month"),
  product = c("Widget A", "Widget B", "Widget C"),
  region = c("North", "South")
)
sales_data$sales <- rnorm(nrow(sales_data), mean = 1000, sd = 200) +
  as.numeric(sales_data$date - min(sales_data$date)) * 2

ggplot(sales_data, aes(x = date, y = sales)) +
  geom_line(aes(color = product), linewidth = 1) +
  geom_smooth(method = "loess", se = TRUE, alpha = 0.2, color = "black") +
  facet_wrap(~region, ncol = 1, scales = "free_y") +
  scale_color_brewer(palette = "Set1") +
  labs(
    title = "Product Sales Trends by Region",
    subtitle = "Monthly data with LOESS smoothing",
    x = NULL,
    y = "Sales ($)",
    color = "Product"
  ) +
  theme_minimal() +
  theme(
    legend.position = "bottom",
    strip.text = element_text(face = "bold", size = 12),
    panel.grid.minor = element_blank()
  )

This example demonstrates several best practices:

Use free scales judiciously: Here scales = "free_y" accommodates different regional sales volumes while keeping time on a consistent x-axis for temporal comparison.

Limit the number of facets: More than 12-15 panels becomes overwhelming. If you have too many categories, consider filtering to the most important ones or using interactive tools.

Maintain consistent color schemes: When using color within faceted plots, keep the color mapping consistent across panels. Don’t use color for the faceting variable—that’s redundant.

Combine with smoothing: Adding trend lines or smoothers helps viewers extract patterns from noisy data across multiple panels.

Control strip text appearance: Make facet labels bold and appropriately sized so they clearly delineate panels without dominating the visualization.

The biggest pitfall with faceting is using free scales when you shouldn’t. If you’re comparing magnitudes across groups, fixed scales are mandatory. Use free scales only when the absolute values differ so dramatically that fixed scales would compress some groups into illegibility, and you’re primarily interested in within-group patterns rather than between-group comparisons.

Faceting transforms complex multi-group visualizations from cluttered messes into clear, scannable displays. Master these techniques and you’ll produce clearer, more insightful data visualizations that communicate effectively without requiring paragraphs of explanation.

Liked this? There's more.

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