R ggplot2 - Faceting (facet_wrap, facet_grid)

Faceting creates small multiples—a series of similar plots using the same scale and axes, allowing you to compare patterns across subsets of your data. Instead of overlaying multiple groups on a...

Key Insights

  • Faceting splits data into multiple subplots based on categorical variables, enabling pattern comparison across groups without manual plot creation
  • facet_wrap() arranges panels in a flexible grid based on one or two variables, while facet_grid() creates a rigid matrix layout with explicit row and column assignments
  • Control panel appearance through scales, space, and labeller arguments to handle varying data ranges and improve readability across facets

Understanding Faceting Fundamentals

Faceting creates small multiples—a series of similar plots using the same scale and axes, allowing you to compare patterns across subsets of your data. Instead of overlaying multiple groups on a single plot or manually creating separate visualizations, faceting automates the process while maintaining visual consistency.

library(ggplot2)
library(dplyr)

# Create sample dataset
sales_data <- data.frame(
  month = rep(month.abb[1:12], 4),
  region = rep(c("North", "South", "East", "West"), each = 12),
  revenue = c(runif(12, 50000, 100000), runif(12, 40000, 90000),
              runif(12, 60000, 110000), runif(12, 45000, 95000)),
  quarter = rep(paste0("Q", rep(1:4, each = 3)), 4)
)

# Basic plot without faceting
ggplot(sales_data, aes(x = month, y = revenue, group = region, color = region)) +
  geom_line() +
  theme_minimal()

This overlapping approach becomes cluttered. Faceting provides clarity.

facet_wrap: Flexible Panel Arrangement

facet_wrap() wraps a 1D sequence of panels into 2D. It’s ideal when you want to facet by one variable or when the combination of two variables doesn’t form a natural grid.

# Single variable faceting
ggplot(sales_data, aes(x = month, y = revenue)) +
  geom_col(fill = "steelblue") +
  facet_wrap(~ region) +
  theme_minimal() +
  theme(axis.text.x = element_text(angle = 45, hjust = 1))

Control the number of rows or columns:

# Specify number of columns
ggplot(sales_data, aes(x = month, y = revenue)) +
  geom_line(color = "darkblue", size = 1) +
  facet_wrap(~ region, ncol = 2) +
  theme_minimal()

# Specify number of rows
ggplot(sales_data, aes(x = month, y = revenue)) +
  geom_line(color = "darkblue", size = 1) +
  facet_wrap(~ region, nrow = 1) +
  theme_minimal()

Facet by multiple variables:

# Two-variable faceting with facet_wrap
ggplot(sales_data, aes(x = month, y = revenue)) +
  geom_point(color = "darkred") +
  facet_wrap(~ region + quarter, ncol = 4) +
  theme_minimal() +
  theme(axis.text.x = element_text(angle = 90, hjust = 1, size = 7))

facet_grid: Matrix Layout

facet_grid() creates a matrix of panels defined by row and column faceting variables. It enforces a strict grid structure where each combination of faceting variables gets its own panel.

# Grid with rows and columns
ggplot(sales_data, aes(x = month, y = revenue)) +
  geom_col(fill = "coral") +
  facet_grid(quarter ~ region) +
  theme_minimal() +
  theme(axis.text.x = element_text(angle = 90, hjust = 1, size = 7))

Single dimension grids:

# Only rows
ggplot(sales_data, aes(x = month, y = revenue)) +
  geom_line(color = "forestgreen") +
  facet_grid(region ~ .) +
  theme_minimal()

# Only columns
ggplot(sales_data, aes(x = month, y = revenue)) +
  geom_line(color = "forestgreen") +
  facet_grid(. ~ region) +
  theme_minimal()

Independent Scales

By default, all facets share the same x and y scales. Use the scales argument to allow independent axes.

# Create data with different ranges
varied_data <- data.frame(
  category = rep(c("A", "B", "C"), each = 20),
  x = c(rnorm(20, 10, 2), rnorm(20, 100, 10), rnorm(20, 1000, 100)),
  y = c(rnorm(20, 5, 1), rnorm(20, 50, 5), rnorm(20, 500, 50))
)

# Fixed scales (default)
ggplot(varied_data, aes(x = x, y = y)) +
  geom_point() +
  facet_wrap(~ category) +
  ggtitle("Fixed Scales") +
  theme_minimal()

# Free scales on both axes
ggplot(varied_data, aes(x = x, y = y)) +
  geom_point() +
  facet_wrap(~ category, scales = "free") +
  ggtitle("Free Scales") +
  theme_minimal()

# Free x-axis only
ggplot(varied_data, aes(x = x, y = y)) +
  geom_point() +
  facet_wrap(~ category, scales = "free_x") +
  ggtitle("Free X Scale") +
  theme_minimal()

For facet_grid(), you can also control panel spacing:

# Free scales with adjusted space
ggplot(varied_data, aes(x = x, y = y)) +
  geom_point() +
  facet_grid(. ~ category, scales = "free_x", space = "free_x") +
  theme_minimal()

The space = "free" argument allocates panel width/height proportional to the range of the scale.

Custom Labels

Improve facet strip labels using labeller functions.

# Create data with abbreviated labels
product_data <- data.frame(
  prod = rep(c("prod_a", "prod_b", "prod_c"), each = 30),
  cust_type = rep(rep(c("b2b", "b2c"), each = 15), 3),
  sales = rnorm(90, 1000, 200)
)

# Custom label names
product_labels <- c(
  prod_a = "Product Alpha",
  prod_b = "Product Beta",
  prod_c = "Product Gamma"
)

customer_labels <- c(
  b2b = "Business Customers",
  b2c = "Consumer Customers"
)

ggplot(product_data, aes(x = sales)) +
  geom_histogram(bins = 15, fill = "purple", alpha = 0.7) +
  facet_grid(cust_type ~ prod, 
             labeller = labeller(prod = product_labels, 
                                cust_type = customer_labels)) +
  theme_minimal()

Use label_both to show both variable name and value:

ggplot(product_data, aes(x = sales)) +
  geom_histogram(bins = 15, fill = "navy", alpha = 0.7) +
  facet_wrap(~ prod + cust_type, labeller = label_both) +
  theme_minimal()

Practical Applications

Time Series Comparison

# Stock price comparison
stocks <- data.frame(
  date = rep(seq.Date(as.Date("2023-01-01"), by = "day", length.out = 90), 4),
  ticker = rep(c("AAPL", "GOOGL", "MSFT", "AMZN"), each = 90),
  price = c(cumsum(rnorm(90, 0.5, 2)) + 150,
            cumsum(rnorm(90, 0.4, 2)) + 120,
            cumsum(rnorm(90, 0.6, 2)) + 300,
            cumsum(rnorm(90, 0.3, 2)) + 130)
)

ggplot(stocks, aes(x = date, y = price)) +
  geom_line(color = "darkblue", size = 0.8) +
  facet_wrap(~ ticker, scales = "free_y", ncol = 2) +
  labs(x = "Date", y = "Price ($)", title = "Stock Price Trends") +
  theme_minimal() +
  theme(strip.background = element_rect(fill = "lightgray"))

Distribution Analysis

# Customer segmentation analysis
customers <- data.frame(
  segment = rep(c("Premium", "Standard", "Basic"), each = 100),
  age_group = rep(rep(c("18-30", "31-50", "51+"), length.out = 100), 3),
  spending = c(rnorm(100, 500, 100), rnorm(100, 250, 50), rnorm(100, 100, 30))
)

ggplot(customers, aes(x = spending, fill = segment)) +
  geom_density(alpha = 0.6) +
  facet_grid(segment ~ age_group, scales = "free_y") +
  scale_fill_brewer(palette = "Set2") +
  labs(x = "Monthly Spending ($)", y = "Density") +
  theme_minimal() +
  theme(legend.position = "none")

Correlation Matrices

# Performance metrics across teams
metrics <- data.frame(
  team = rep(c("Engineering", "Sales", "Marketing"), each = 50),
  efficiency = rep(rnorm(50, 75, 10), 3) + rep(c(5, -3, 2), each = 50),
  satisfaction = rep(rnorm(50, 80, 8), 3) + rep(c(3, 5, -2), each = 50)
)

ggplot(metrics, aes(x = efficiency, y = satisfaction)) +
  geom_point(alpha = 0.6, color = "darkred") +
  geom_smooth(method = "lm", se = TRUE, color = "blue") +
  facet_wrap(~ team) +
  labs(x = "Efficiency Score", y = "Satisfaction Score") +
  theme_minimal()

Performance Considerations

Faceting with large datasets can slow rendering. Optimize by:

# Sample large dataset before plotting
large_data <- data.frame(
  group = sample(LETTERS[1:10], 100000, replace = TRUE),
  x = rnorm(100000),
  y = rnorm(100000)
)

# Sample subset for visualization
sampled <- large_data %>%
  group_by(group) %>%
  slice_sample(n = 500)

ggplot(sampled, aes(x = x, y = y)) +
  geom_point(alpha = 0.4) +
  facet_wrap(~ group) +
  theme_minimal()

Use geom_hex() or geom_bin2d() for dense scatter plots:

ggplot(large_data, aes(x = x, y = y)) +
  geom_hex(bins = 30) +
  facet_wrap(~ group) +
  scale_fill_viridis_c() +
  theme_minimal()

Faceting transforms complex multi-group visualizations into clear, comparable panels. Choose facet_wrap() for flexibility and facet_grid() when you need explicit row-column structure. Adjust scales and labels to match your data characteristics and analytical requirements.

Liked this? There's more.

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