How to shade every other y-axis label row (including labels + points) in ggplot?

I’m working with several plots where I compare “Pre” and “Post” slopes for different cities. For one of them (retail), I’ve already added alternating shaded bands behind the points using geom_rect().

Example (simplified):

bg_retail <- data.frame(
  ymin = seq(0.5, max(df_retail_long$city_num), by = 2),
  ymax = seq(1.5, max(df_retail_long$city_num) + 1, by = 2)
)

p_retail <- ggplot(df_retail_long, aes(x = slope, y = city_num, group = city)) +
  geom_rect(data = bg_retail,
            aes(xmin = -Inf, xmax = Inf, ymin = ymin, ymax = ymax),
            inherit.aes = FALSE,
            fill = "lightgrey", alpha = 0.2) +
  geom_line(color = "lightgrey", linewidth = 1, alpha = 0.7) +
  geom_point(aes(color = period), size = 4) +
  scale_y_continuous(
    breaks = unique(df_retail_long$city_num),
    labels = unique(df_retail_long$city),
    expand = expansion(add = c(0.5, 0.5))
  )

This works fine for shading alternating rows in the plot panel, but what I’d really like is to also shade the y-axis labels themselves (so that the label text and its corresponding row of points are highlighted together).

How can I do this in ggplot?

Full code (including my dataset):

pacman::p_load(ggplot2, patchwork, dplyr, stringr)

# airport data
df_airport <- data.frame(
  city = c("Brisbane, Australia", "Delhi, India", "London, UK", "Manchester, UK", 
           "Shenzhen, China", "Guangzhou, China", "Los Angeles, USA", "Melbourne, Australia",
           "Pune, India", "Mumbai, India", "New York, USA", "Santiago, Chile",
           "Cairo, Egypt", "Milan, Italy", "Almaty, Kazakhstan", "Nairobi, Kenya",
           "Amsterdam, Netherlands", "Lahore, Pakistan", "Jeddah, Saudi Arabia", 
           "Riyadh, Saudi Arabia", "Cape Town, South Africa", "Madrid, Spain",
           "Abu Dhabi, UAE", "Dubai, UAE", "Sydney, Australia", "Hong Kong, China"),
  pre_slope = c(-0.550, 0.0405, 0.263, 0.424, 0.331, -0.786, 0.187, -0.0562,
                0.0187, 0.168, 0.0392, 0.0225, 0.0329, -0.0152, 0.174, -0.0931,
                -0.121, -0.246, 0.294, 0.865, -0.503, 0.0466, 0.524, 0.983, 0.0440, -0.295),
  post_slope = c(-0.393, 0.00300, 0.00839, -0.642, -0.595, -0.447, -0.0372, -0.0993,
                 -0.0426, -1.94, 0.00842, -0.903, -0.0127, -0.0468, 1.29, -0.337,
                 -0.435, -0.00608, -0.305, 0.203, 0.193, -0.202, -0.0637, 0.564, -0.0916, 0.768)
)

# industrial data
df_industrial <- data.frame(
  city = c("Beijing, China", "Brisbane, Australia", "Chicago, USA", "Dallas, USA",
           "Delhi, India", "London, UK", "Manchester, UK", "Shenzhen, China",
           "Guangzhou, China", "Wuhan, China", "Los Angeles, USA", "Melbourne, Australia",
           "Pune, India", "Mumbai, India", "New York, USA", "Buenos Aires, Argentina",
           "Vienna, Austria", "Baku, Azerbaijan", "Santiago, Chile", "Cairo, Egypt",
           "Paris, France", "Berlin, Germany", "Frankfurt, Germany", "Munich, Germany",
           "Athens, Greece", "Rome, Italy", "Milan, Italy", "Almaty, Kazakhstan",
           "Nairobi, Kenya", "Mexico City, Mexico", "Amsterdam, Netherlands", "Lahore, Pakistan",
           "Lima, Peru", "Jeddah, Saudi Arabia", "Riyadh, Saudi Arabia", "Johannesburg, South Africa",
           "Cape Town, South Africa", "Madrid, Spain", "Istanbul, Turkey", "Abu Dhabi, UAE",
           "Dubai, UAE", "Caracas, Venezuela", "Rio de Janeiro, Brazil", "Shanghai, China",
           "Sao Paulo, Brazil", "Sydney, Australia", "Toronto, Canada", "Washington DC, USA",
           "Hong Kong, China"),
  pre_slope = c(-0.00621, -0.851, -0.378, 0.0846, -0.0133, 0.361, -0.276, 0.175,
                0.0299, -0.0127, 0.0874, -0.0666, 0.0245, 0.285, 0.0524, -0.0150,
                -0.220, -0.137, 0.444, -0.0354, -0.00491, -0.0300, -0.816, -0.507,
                -0.176, -0.237, -0.0117, 0.325, -0.110, 0.122, -2.45, -0.125,
                0.126, -0.570, -0.590, -0.0271, -0.170, 0.0690, -0.158, -0.120,
                0.310, -0.0893, -0.528, 0.647, 0.000298, 0.0735, 0.236, 0.0237, -0.521),
  post_slope = c(0.0395, 0.594, 0.322, 0.248, 0.0337, 0.00941, -0.502, 0.154,
                 0.789, -0.0532, 0.0400, 0.0439, 0.0249, -1.14, -0.00410, 0.0205,
                 -0.821, 0.142, 0.219, -0.00623, -0.0432, -0.0191, -0.370, -0.328,
                 0.577, 0.0164, -0.00493, 0.841, 0.0101, -0.000736, 0.717, 0.00221,
                 -0.245, 0.0487, 0.363, -0.000446, -0.0949, -0.218, 0.0188, 0.356,
                 0.545, 1.21, -0.0900, -0.209, 0.212, 0.0787, -0.129, -0.587, 1.03)
)

# retail data
df_retail <- data.frame(
  city = c("Brisbane, Australia", "Chicago, USA", "Dallas, USA", "Manchester, UK", 
           "Wuhan, China", "Los Angeles, USA", "Melbourne, Australia", "New York, USA",
           "Buenos Aires, Argentina", "Baku, Azerbaijan", "Paris, France", "Rome, Italy",
           "Milan, Italy", "Almaty, Kazakhstan", "Mexico City, Mexico", "Amsterdam, Netherlands",
           "Lima, Peru", "Warsaw, Poland", "Riyadh, Saudi Arabia", "Johannesburg, South Africa",
           "Madrid, Spain", "Caracas, Venezuela", "Sao Paulo, Brazil", "Sydney, Australia",
           "Toronto, Canada"),
  pre_slope = c(-0.321, -0.934, 0.831, -0.359, 0.0154, 0.0113, -0.100, 0.0510,
                0.00658, 0.00571, -0.0320, -0.512, -0.00924, 0.0852, 0.154, 0.179,
                0.151, -0.217, -0.798, -0.0394, 0.0503, 0.475, -0.0377, -0.0110, 0.438),
  post_slope = c(-0.404, 0.391, 0.119, -1.05, -0.138, 0.0592, 0.0834, -0.0451,
                 -0.0296, 0.170, -0.112, 0.150, -0.0557, 0.114, -0.0217, 0.642,
                 -0.376, -0.0210, 0.663, -0.00313, -0.425, 1.45, 0.233, -0.0950, -0.686)
)

# prep data for plotting
prepare_data <- function(df) {
  df$city_num <- 1:nrow(df)
  df_long <- data.frame(
    city = rep(df$city, 2),
    city_num = rep(df$city_num, 2),
    slope = c(df$pre_slope, df$post_slope),
    period = rep(c("Pre", "Post"), each = nrow(df))
  )
  return(df_long)
}

df_airport_long <- prepare_data(df_airport)
df_industrial_long <- prepare_data(df_industrial)
df_retail_long <- prepare_data(df_retail)

# airport
p_airport <- ggplot(df_airport_long, aes(x = slope, y = city_num, group = city)) +
  geom_line(color = "lightgrey", linewidth = 1, alpha = 0.7) +
  geom_point(aes(color = period), size = 4) +
  geom_vline(xintercept = 0, linetype = "dashed", color = "dark grey") +
  scale_color_manual(values = c("Pre" = "#18685D", "Post" = "#B0280B"),
                     breaks = c("Pre", "Post")) +
  scale_y_continuous(
    breaks = unique(df_airport_long$city_num),
    labels = unique(df_airport_long$city),
    expand = expansion(add = c(0.5, 0.5))
  ) +
  # ggtitle("Airport") +
  theme_minimal(base_size = 18) +
  theme(
    panel.grid = element_blank(),
    axis.line.x.bottom = element_line(color = "black", linewidth = .7),
    axis.line.y.left = element_line(color = "black", linewidth = .7),
    axis.title = element_blank(),
    legend.position = "none"
  )

# industrial
p_industrial <- ggplot(df_industrial_long, aes(x = slope, y = city_num, group = city)) +
  geom_line(color = "lightgrey", linewidth = 1, alpha = 0.7) +
  geom_point(aes(color = period), size = 4) +
  geom_vline(xintercept = 0, linetype = "dashed", color = "dark grey") +
  scale_color_manual(values = c("Pre" = "#18685D", "Post" = "#B0280B"),
                     breaks = c("Pre", "Post")) +
  scale_y_continuous(
    breaks = unique(df_industrial_long$city_num),
    labels = unique(df_industrial_long$city),
    expand = expansion(add = c(0.5, 0.5))
  ) +
  # ggtitle("Industrial") +
  theme_minimal(base_size = 18) +
  theme(
    panel.grid = element_blank(),
    axis.line.x.bottom = element_line(color = "black", linewidth = .7),
    axis.line.y.left = element_line(color = "black", linewidth = .7),
    axis.title = element_blank(),
    legend.title = element_blank(),
    legend.position = "bottom",
    legend.direction = "horizontal",
    legend.spacing.y = unit(0, "cm"),
    legend.margin = margin(t = -5, unit = "pt")
  )

# retail
bg_retail <- data.frame(
  ymin = seq(0.5, max(df_retail_long$city_num), by = 2),
  ymax = seq(1.5, max(df_retail_long$city_num) + 1, by = 2)
)

p_retail <- ggplot(df_retail_long, aes(x = slope, y = city_num, group = city)) +
  geom_rect(data = bg_retail,
            aes(xmin = -Inf, xmax = Inf, ymin = ymin, ymax = ymax),
            inherit.aes = FALSE,
            fill = "lightgrey", alpha = 0.2) +
  geom_line(color = "lightgrey", linewidth = 1, alpha = 0.7) +
  geom_point(aes(color = period), size = 4) +
  geom_vline(xintercept = 0, linetype = "dashed", color = "dark grey") +
  scale_color_manual(values = c("Pre" = "#18685D", "Post" = "#B0280B"),
                     breaks = c("Pre", "Post")) +
  scale_y_continuous(
    breaks = unique(df_retail_long$city_num),
    labels = unique(df_retail_long$city),
    expand = expansion(add = c(0.5, 0.5))
  ) +
  # ggtitle("Retail") +
  theme_minimal(base_size = 18) +
  theme(
    panel.grid = element_blank(),
    axis.line.x.bottom = element_line(color = "black", linewidth = .7),
    axis.line.y.left = element_line(color = "black", linewidth = .7),
    axis.title = element_blank(),
    legend.position = "none"
  )

# Combine plots
p_airport + p_industrial + p_retail + plot_layout(ncol = 3)
sessionInfo()
R version 4.5.1 (2025-06-13 ucrt)
Platform: x86_64-w64-mingw32/x64
Running under: Windows 11 x64 (build 26100)

Matrix products: default
  LAPACK version 3.12.1

locale:
[1] LC_COLLATE=English_United States.utf8  LC_CTYPE=English_United States.utf8    LC_MONETARY=English_United States.utf8
[4] LC_NUMERIC=C                           LC_TIME=English_United States.utf8    

time zone: Europe/Bucharest
tzcode source: internal

attached base packages:
[1] stats     graphics  grDevices utils     datasets  methods   base     

other attached packages:
[1] ggtext_0.1.2    patchwork_1.3.2 ggplot2_4.0.0   tidyplots_0.3.1 stringr_1.5.2   dplyr_1.1.4     sf_1.0-21      

loaded via a namespace (and not attached):
 [1] gtable_0.3.6       compiler_4.5.1     tidyselect_1.2.1   Rcpp_1.1.0         xml2_1.4.0         dichromat_2.0-0.1  systemfonts_1.3.1 
 [8] scales_1.4.0       textshaping_1.0.3  R6_2.6.1           labeling_0.4.3     generics_0.1.4     classInt_0.4-11    tibble_3.3.0      
[15] units_0.8-7        DBI_1.2.3          svglite_2.2.1      pillar_1.11.1      RColorBrewer_1.1-3 rlang_1.1.6        stringi_1.8.7     
[22] S7_0.2.0           cli_3.6.5          withr_3.0.2        magrittr_2.0.4     class_7.3-23       gridtext_0.1.5     grid_4.5.1        
[29] rstudioapi_0.17.1  lifecycle_1.0.4    vctrs_0.6.5        KernSmooth_2.23-26 proxy_0.4-27       glue_1.8.0         farver_2.1.2      
[36] ragg_1.5.0         e1071_1.7-16       pacman_0.5.1       purrr_1.1.0        tools_4.5.1        pkgconfig_2.0.3

The solution (pasted from a response to my question on Stackoverflow):

Another option would be to use faceting. By making each y-axis category its own facet panel, we can add the labels as strip text and automatically gain the option to specify a background fill colour. To achieve alternating fill colours, I combine this approach with ggh4x. This approach also works well when combining multiple plots using patchwork. However, the major downside of this approach is that it is much less efficient, as having a panel for each category takes longer to render the plot.

library(tidyverse)
library(patchwork)
library(ggh4x)

plot_fun <- function(.data, title) {
  n <- length(unique(.data$city))

  bg_rect <- data.frame(
    city = sort(unique(.data$city))[seq(1, n, by = 2)],
    ymin = .5,
    ymax = 1.5
  )
  
  strip_text_fill <- c(alpha("lightgrey", .2), "white")
  strip_text_color <- rep("transparent", 2)
  
  ggplot(.data, aes(x = slope, y = city)) +
    geom_rect(
      data = bg_rect,
      aes(xmin = -Inf, xmax = Inf, ymin = ymin, ymax = ymax),
      inherit.aes = FALSE,
      fill = "lightgrey", alpha = 0.2
    ) +
    geom_line(color = "lightgrey", linewidth = 1, alpha = 0.7) +
    geom_point(aes(color = period), size = 2) +
    geom_vline(xintercept = 0, linetype = "dashed", color = "dark grey") +
    scale_color_manual(
      values = c("Pre" = "#18685D", "Post" = "#B0280B"),
      breaks = c("Pre", "Post")
    ) +
    scale_y_discrete(
      breaks = NULL,
      expand = expansion(add = .5)
    ) +
    labs(title = title, color = NULL) +
    theme_minimal(base_size = 10) +
    theme(
      panel.grid = element_blank(),
      axis.line = element_line(color = "black", linewidth = .7),
      axis.title = element_blank(),
      strip.text.y.left = element_text(angle = 0, hjust = 1)
    ) +
    ggh4x::facet_wrap2(~city,
      ncol = 1,
      scales = "free_y",
      strip.position = "left",
      strip = ggh4x::strip_themed(
        background_y = ggh4x::elem_list_rect(
          fill = strip_text_fill,
          color = strip_text_color
        )
      )
    )
}

list(
  Airport = df_airport,
  Industrial = df_industrial,
  Retail = df_retail
) |>
  lapply(prepare_data) |>
  imap(plot_fun) |>
  wrap_plots(
    ncol = 3,
    guides = "collect"
  ) &
  theme(
    panel.spacing.y = unit(0, "pt"),
    strip.placement = "outside"
  )