rggplot2mappingpurrrfacet-wrap

How to dynamically create nested facet_wrap with different numbers of columns and rows?


Beginner in task automation, I would like to dynamically generate nested facet_wraps for many analytes. I know this is possible using purrr::map, but the problem is that my variables in the facet_wrap2 function are not constant. In other words, the variables defining the number of columns (qc: different levels of quality controls) and rows (site: different laboratories where measurements are performed) vary depending on the analyte.

Naively, I grouped by analytes and created two new columns, nqc (number of distinct QC levels per analyte) and nsite (number of distinct sites per analyte), hoping to dynamically map these variables. Unfortunately, the ncol and nrow arguments of facet_wrap2 require integers or NULL values, not vectors ("Error in map(): (...) Caused by error in facet_wrap(): ! nrow must be a whole number or NULL, not an integer vector").

Examples of 3 analytes (see data df0 below):
Analyte AAA : df0: 2 qc measured in 1 site, thus the desired facet_wrap plot should have 2 rows and 1 column
Analyte BBB: df0: 3 qc measured in 3 sites, thus the current facet_wrap plot is ok (3 rows and 3 columns)
Analyte CCC: df0: 4 qc measured in 2 site, thus the desired facet_wrap plot should have 4 rows and 2 columns

I can't find a similar case, at least not one that's explicitly solved. Is this possible using purrr::map, lapply or others?

Thanks

Example plot of analyte AAA: Unwanted graph (left) and desired graph (right) enter image description here

Code (unsuccessful attempt):

library(tidyverse)
library(ggh4x)
library(scales)

# mutate numbers of qc and site (perhaps unnecessary)
df0 <-
  df0 |>
  group_by(analyte) |> 
  mutate(
    nsite = n_distinct(site),
    nqc = n_distinct(qc)) |> 
  ungroup()

# list plots
list_plot <- df0 |>
  group_split(analyte) |> 
  map(~ggplot(., aes(x = year_month, y = cv, group = analyzer)) +
        facet_wrap2(
          vars(qc, site),
          # nrow = df0$nqc,  (error)
          drop = FALSE,
          scales = "free_y",
          strip = strip_nested()
        ) +
        scale_x_discrete(expand = expansion(mult = 0.05)) +
        scale_y_continuous(breaks = breaks_pretty(n=4)) +
        geom_line(linewidth = 1) +
        theme(legend.position = "none")
      )

# display each plot
list_plot[[1]]
list_plot[[2]]
list_plot[[3]]

Data:

df0 <-
structure(list(site = c("SX", "SX", "SX", "SX", "SX", "SX", "SX", 
"SX", "SX", "SX", "SX", "SX", "SX", "SX", "SX", "SX", "SX", "SX", 
"SX", "SX", "SX", "SX", "SX", "SX", "SX", "SX", "SX", "SX", "SX", 
"SX", "SX", "SX", "SX", "SX", "SX", "SX", "SX", "SX", "SX", "SX", 
"SX", "SX", "SX", "SX", "SX", "SX", "SX", "SX", "SX", "SX", "SX", 
"SX", "SX", "SX", "SX", "SX", "SX", "SX", "SX", "SX", "SX", "SX", 
"SX", "SX", "SX", "SX", "SX", "SX", "SX", "SX", "SX", "SX", "SX", 
"SX", "SX", "SX", "SX", "SX", "SX", "SX", "SX", "SX", "SX", "SX", 
"SX", "SX", "SX", "SY", "SY", "SY", "SY", "SY", "SY", "SY", "SY", 
"SY", "SY", "SY", "SY", "SY", "SY", "SY", "SY", "SY", "SY", "SZ", 
"SZ", "SZ", "SZ", "SZ", "SZ", "SZ", "SZ", "SZ", "SZ", "SZ", "SZ", 
"SZ", "SZ", "SZ", "SZ", "SZ", "SZ", "SZ", "SZ", "SZ", "SZ", "SZ", 
"SZ", "SZ", "SZ", "SZ", "SZ", "SZ", "SZ", "SZ", "SZ", "SZ", "SZ", 
"SZ", "SZ", "SZ", "SZ", "SZ", "SZ", "SZ", "SZ", "SZ", "SZ", "SZ", 
"SZ", "SZ", "SZ", "SZ", "SZ", "SZ", "SZ", "SZ", "SZ", "SZ", "SZ", 
"SZ", "SZ", "SZ", "SZ", "SZ", "SZ", "SZ", "SZ", "SZ", "SZ"), 
    analyte = c("AAA", "AAA", "AAA", "AAA", "AAA", "AAA", "AAA", 
    "AAA", "AAA", "AAA", "AAA", "AAA", "CCC", "CCC", "CCC", "CCC", 
    "CCC", "CCC", "CCC", "CCC", "CCC", "CCC", "CCC", "CCC", "CCC", 
    "CCC", "CCC", "CCC", "CCC", "CCC", "CCC", "CCC", "CCC", "CCC", 
    "CCC", "CCC", "CCC", "CCC", "CCC", "CCC", "CCC", "CCC", "CCC", 
    "CCC", "CCC", "CCC", "CCC", "CCC", "CCC", "CCC", "CCC", "CCC", 
    "CCC", "CCC", "CCC", "CCC", "CCC", "CCC", "CCC", "CCC", "BBB", 
    "BBB", "BBB", "BBB", "BBB", "BBB", "BBB", "BBB", "BBB", "BBB", 
    "BBB", "BBB", "BBB", "BBB", "BBB", "BBB", "BBB", "BBB", "BBB", 
    "BBB", "BBB", "BBB", "BBB", "BBB", "BBB", "BBB", "BBB", "BBB", 
    "BBB", "BBB", "BBB", "BBB", "BBB", "BBB", "BBB", "BBB", "BBB", 
    "BBB", "BBB", "BBB", "BBB", "BBB", "BBB", "BBB", "BBB", "CCC", 
    "CCC", "CCC", "CCC", "CCC", "CCC", "CCC", "CCC", "CCC", "CCC", 
    "CCC", "CCC", "CCC", "CCC", "CCC", "CCC", "CCC", "CCC", "CCC", 
    "CCC", "CCC", "CCC", "CCC", "CCC", "CCC", "CCC", "CCC", "CCC", 
    "CCC", "CCC", "CCC", "CCC", "CCC", "CCC", "CCC", "CCC", "CCC", 
    "CCC", "CCC", "CCC", "CCC", "CCC", "CCC", "CCC", "CCC", "CCC", 
    "CCC", "CCC", "BBB", "BBB", "BBB", "BBB", "BBB", "BBB", "BBB", 
    "BBB", "BBB", "BBB", "BBB", "BBB", "BBB", "BBB", "BBB", "BBB", 
    "BBB", "BBB"), qc = c("QC_A1", "QC_A2", "QC_A1", "QC_A2", 
    "QC_A1", "QC_A2", "QC_A1", "QC_A2", "QC_A1", "QC_A2", "QC_A1", 
    "QC_A2", "QC_C1", "QC_C2", "QC_C3", "QC_C4", "QC_C1", "QC_C2", 
    "QC_C3", "QC_C4", "QC_C1", "QC_C2", "QC_C3", "QC_C4", "QC_C1", 
    "QC_C2", "QC_C3", "QC_C4", "QC_C1", "QC_C2", "QC_C3", "QC_C4", 
    "QC_C1", "QC_C2", "QC_C3", "QC_C4", "QC_C1", "QC_C2", "QC_C3", 
    "QC_C4", "QC_C1", "QC_C2", "QC_C3", "QC_C4", "QC_C1", "QC_C2", 
    "QC_C3", "QC_C4", "QC_C1", "QC_C2", "QC_C3", "QC_C4", "QC_C1", 
    "QC_C2", "QC_C3", "QC_C4", "QC_C1", "QC_C2", "QC_C3", "QC_C4", 
    "QC_B1", "QC_B2", "QC_B3", "QC_B1", "QC_B2", "QC_B3", "QC_B1", 
    "QC_B2", "QC_B3", "QC_B1", "QC_B2", "QC_B3", "QC_B1", "QC_B2", 
    "QC_B3", "QC_B1", "QC_B2", "QC_B3", "QC_B1", "QC_B2", "QC_B3", 
    "QC_B1", "QC_B2", "QC_B3", "QC_B1", "QC_B2", "QC_B3", "QC_B1", 
    "QC_B2", "QC_B3", "QC_B1", "QC_B2", "QC_B3", "QC_B1", "QC_B2", 
    "QC_B3", "QC_B1", "QC_B2", "QC_B3", "QC_B1", "QC_B2", "QC_B3", 
    "QC_B1", "QC_B2", "QC_B3", "QC_C1", "QC_C2", "QC_C3", "QC_C4", 
    "QC_C1", "QC_C2", "QC_C3", "QC_C4", "QC_C1", "QC_C2", "QC_C3", 
    "QC_C4", "QC_C1", "QC_C2", "QC_C3", "QC_C4", "QC_C1", "QC_C2", 
    "QC_C3", "QC_C4", "QC_C1", "QC_C2", "QC_C3", "QC_C4", "QC_C1", 
    "QC_C2", "QC_C3", "QC_C4", "QC_C1", "QC_C2", "QC_C3", "QC_C4", 
    "QC_C1", "QC_C2", "QC_C3", "QC_C4", "QC_C1", "QC_C2", "QC_C3", 
    "QC_C4", "QC_C1", "QC_C2", "QC_C3", "QC_C4", "QC_C1", "QC_C2", 
    "QC_C3", "QC_C4", "QC_B1", "QC_B2", "QC_B3", "QC_B1", "QC_B2", 
    "QC_B3", "QC_B1", "QC_B2", "QC_B3", "QC_B1", "QC_B2", "QC_B3", 
    "QC_B1", "QC_B2", "QC_B3", "QC_B1", "QC_B2", "QC_B3"), analyzer = c("X1_C", 
    "X1_C", "X3_C", "X3_C", "X1_C", "X1_C", "X3_C", "X3_C", "X1_C", 
    "X1_C", "X3_C", "X3_C", "X1_B1", "X1_B1", "X1_B1", "X1_B1", 
    "X1_B2", "X1_B2", "X1_B2", "X1_B2", "X2_B", "X2_B", "X2_B", 
    "X2_B", "X3_B", "X3_B", "X3_B", "X3_B", "X1_B1", "X1_B1", 
    "X1_B1", "X1_B1", "X1_B2", "X1_B2", "X1_B2", "X1_B2", "X2_B", 
    "X2_B", "X2_B", "X2_B", "X3_B", "X3_B", "X3_B", "X3_B", "X1_B1", 
    "X1_B1", "X1_B1", "X1_B1", "X1_B2", "X1_B2", "X1_B2", "X1_B2", 
    "X2_B", "X2_B", "X2_B", "X2_B", "X3_B", "X3_B", "X3_B", "X3_B", 
    "X1_C", "X1_C", "X1_C", "X2_C", "X2_C", "X2_C", "X3_C", "X3_C", 
    "X3_C", "X1_C", "X1_C", "X1_C", "X2_C", "X2_C", "X2_C", "X3_C", 
    "X3_C", "X3_C", "X1_C", "X1_C", "X1_C", "X2_C", "X2_C", "X2_C", 
    "X3_C", "X3_C", "X3_C", "Y1_C", "Y1_C", "Y1_C", "Y2_C", "Y2_C", 
    "Y2_C", "Y1_C", "Y1_C", "Y1_C", "Y2_C", "Y2_C", "Y2_C", "Y1_C", 
    "Y1_C", "Y1_C", "Y2_C", "Y2_C", "Y2_C", "Z1_B1", "Z1_B1", 
    "Z1_B1", "Z1_B1", "Z1_B2", "Z1_B2", "Z1_B2", "Z1_B2", "Z2_B1", 
    "Z2_B1", "Z2_B1", "Z2_B1", "Z2_B2", "Z2_B2", "Z2_B2", "Z2_B2", 
    "Z1_B1", "Z1_B1", "Z1_B1", "Z1_B1", "Z1_B2", "Z1_B2", "Z1_B2", 
    "Z1_B2", "Z2_B1", "Z2_B1", "Z2_B1", "Z2_B1", "Z2_B2", "Z2_B2", 
    "Z2_B2", "Z2_B2", "Z1_B1", "Z1_B1", "Z1_B1", "Z1_B1", "Z1_B2", 
    "Z1_B2", "Z1_B2", "Z1_B2", "Z2_B1", "Z2_B1", "Z2_B1", "Z2_B1", 
    "Z2_B2", "Z2_B2", "Z2_B2", "Z2_B2", "Z1_C", "Z1_C", "Z1_C", 
    "Z2_C", "Z2_C", "Z2_C", "Z1_C", "Z1_C", "Z1_C", "Z2_C", "Z2_C", 
    "Z2_C", "Z1_C", "Z1_C", "Z1_C", "Z2_C", "Z2_C", "Z2_C"), 
    year_month = c("2025-09", "2025-09", "2025-09", "2025-09", 
    "2025-10", "2025-10", "2025-10", "2025-10", "2025-11", "2025-11", 
    "2025-11", "2025-11", "2025-09", "2025-09", "2025-09", "2025-09", 
    "2025-09", "2025-09", "2025-09", "2025-09", "2025-09", "2025-09", 
    "2025-09", "2025-09", "2025-09", "2025-09", "2025-09", "2025-09", 
    "2025-10", "2025-10", "2025-10", "2025-10", "2025-10", "2025-10", 
    "2025-10", "2025-10", "2025-10", "2025-10", "2025-10", "2025-10", 
    "2025-10", "2025-10", "2025-10", "2025-10", "2025-11", "2025-11", 
    "2025-11", "2025-11", "2025-11", "2025-11", "2025-11", "2025-11", 
    "2025-11", "2025-11", "2025-11", "2025-11", "2025-11", "2025-11", 
    "2025-11", "2025-11", "2025-09", "2025-09", "2025-09", "2025-09", 
    "2025-09", "2025-09", "2025-09", "2025-09", "2025-09", "2025-10", 
    "2025-10", "2025-10", "2025-10", "2025-10", "2025-10", "2025-10", 
    "2025-10", "2025-10", "2025-11", "2025-11", "2025-11", "2025-11", 
    "2025-11", "2025-11", "2025-11", "2025-11", "2025-11", "2025-09", 
    "2025-09", "2025-09", "2025-09", "2025-09", "2025-09", "2025-10", 
    "2025-10", "2025-10", "2025-10", "2025-10", "2025-10", "2025-11", 
    "2025-11", "2025-11", "2025-11", "2025-11", "2025-11", "2025-09", 
    "2025-09", "2025-09", "2025-09", "2025-09", "2025-09", "2025-09", 
    "2025-09", "2025-09", "2025-09", "2025-09", "2025-09", "2025-09", 
    "2025-09", "2025-09", "2025-09", "2025-10", "2025-10", "2025-10", 
    "2025-10", "2025-10", "2025-10", "2025-10", "2025-10", "2025-10", 
    "2025-10", "2025-10", "2025-10", "2025-10", "2025-10", "2025-10", 
    "2025-10", "2025-11", "2025-11", "2025-11", "2025-11", "2025-11", 
    "2025-11", "2025-11", "2025-11", "2025-11", "2025-11", "2025-11", 
    "2025-11", "2025-11", "2025-11", "2025-11", "2025-11", "2025-09", 
    "2025-09", "2025-09", "2025-09", "2025-09", "2025-09", "2025-10", 
    "2025-10", "2025-10", "2025-10", "2025-10", "2025-10", "2025-11", 
    "2025-11", "2025-11", "2025-11", "2025-11", "2025-11"), cv = c(2.64, 
    3.83, 2.66, 3.11, 1.63, 1.82, 3.74, 3.81, 1.46, 2.07, 3.78, 
    3.17, 1.27, 1.05, 1.82, 2.84, 1.12, 0.97, 1.36, 2.81, 1.16, 
    2.51, 1.66, 3.24, 1.03, 0.78, 1.11, 2.92, 1.82, 0.87, 1.46, 
    2.56, 1.61, 1.24, 1.3, 2.45, 1.68, 1.69, 1.33, 2.24, 2.11, 
    0.74, 1.38, 2.33, 1.14, 0.96, 1.23, 3.05, 1.34, 1.01, 1.09, 
    2.35, 1.35, 1.06, 1.08, 2.2, 2.22, 0.68, 1.09, 2.56, 3.92, 
    2.66, 2.59, 2.38, 2.83, 1.95, 2.26, 1.58, 1.68, 2.59, 1.61, 
    1.56, 5.78, 2.05, 1.87, 3.68, 3.65, 1.21, 4.99, 1.23, 1.02, 
    2.63, 1.79, 1.13, 2.21, 1.22, 1.37, 4.99, 4.02, 4.12, 3.09, 
    1.32, 1.39, 4.52, 2.11, 2.97, 5.48, 2.31, 2.85, 2.89, 1.41, 
    1.31, 2.79, 1.49, 1.51, 1.35, 1.37, 1.48, 2.91, 1.21, 1.25, 
    1.25, 2.93, 1.31, 1.55, 1.41, 3.19, 1.34, 4.05, 1.36, 2.64, 
    1.24, 1.46, 1.42, 2.82, 1.78, 1.41, 1.52, 3.01, 2.24, 1.37, 
    1.84, 3.95, 1.53, 1.21, 1.21, 3.76, 2.49, 1.55, 1.26, 2.93, 
    2.39, 1.56, 1.26, 2.52, 2.06, 0.94, 1.19, 2.25, 1.73, 1.02, 
    1.31, 2.72, 2.24, 1.08, 2.09, 5.57, 3.65, 3.56, 3.19, 2.45, 
    2.14, 3.2, 1.8, 2.13, 2.56, 1.23, 1.21, 2.62, 1.44, 1.37)), row.names = c(NA, 
-171L), class = c("tbl_df", "tbl", "data.frame")) 

Solution

  • I may have misunderstood, but do you get your desired outcome if you use nrow = unique(.$nqc) instead of nrow = df0$nqc? E.g.

    Libraries/data:

    library(tidyverse)
    library(ggh4x)
    library(scales)
    
    df0 <- structure(list(site = c("SX", "SX", "SX", "SX", "SX", "SX", "SX", 
                              "SX", "SX", "SX", "SX", "SX", "SX", "SX", "SX", "SX", "SX", "SX", 
                              "SX", "SX", "SX", "SX", "SX", "SX", "SX", "SX", "SX", "SX", "SX", 
                              "SX", "SX", "SX", "SX", "SX", "SX", "SX", "SX", "SX", "SX", "SX", 
                              "SX", "SX", "SX", "SX", "SX", "SX", "SX", "SX", "SX", "SX", "SX", 
                              "SX", "SX", "SX", "SX", "SX", "SX", "SX", "SX", "SX", "SX", "SX", 
                              "SX", "SX", "SX", "SX", "SX", "SX", "SX", "SX", "SX", "SX", "SX", 
                              "SX", "SX", "SX", "SX", "SX", "SX", "SX", "SX", "SX", "SX", "SX", 
                              "SX", "SX", "SX", "SY", "SY", "SY", "SY", "SY", "SY", "SY", "SY", 
                              "SY", "SY", "SY", "SY", "SY", "SY", "SY", "SY", "SY", "SY", "SZ", 
                              "SZ", "SZ", "SZ", "SZ", "SZ", "SZ", "SZ", "SZ", "SZ", "SZ", "SZ", 
                              "SZ", "SZ", "SZ", "SZ", "SZ", "SZ", "SZ", "SZ", "SZ", "SZ", "SZ", 
                              "SZ", "SZ", "SZ", "SZ", "SZ", "SZ", "SZ", "SZ", "SZ", "SZ", "SZ", 
                              "SZ", "SZ", "SZ", "SZ", "SZ", "SZ", "SZ", "SZ", "SZ", "SZ", "SZ", 
                              "SZ", "SZ", "SZ", "SZ", "SZ", "SZ", "SZ", "SZ", "SZ", "SZ", "SZ", 
                              "SZ", "SZ", "SZ", "SZ", "SZ", "SZ", "SZ", "SZ", "SZ", "SZ"), 
                     analyte = c("AAA", "AAA", "AAA", "AAA", "AAA", "AAA", "AAA", 
                                 "AAA", "AAA", "AAA", "AAA", "AAA", "CCC", "CCC", "CCC", "CCC", 
                                 "CCC", "CCC", "CCC", "CCC", "CCC", "CCC", "CCC", "CCC", "CCC", 
                                 "CCC", "CCC", "CCC", "CCC", "CCC", "CCC", "CCC", "CCC", "CCC", 
                                 "CCC", "CCC", "CCC", "CCC", "CCC", "CCC", "CCC", "CCC", "CCC", 
                                 "CCC", "CCC", "CCC", "CCC", "CCC", "CCC", "CCC", "CCC", "CCC", 
                                 "CCC", "CCC", "CCC", "CCC", "CCC", "CCC", "CCC", "CCC", "BBB", 
                                 "BBB", "BBB", "BBB", "BBB", "BBB", "BBB", "BBB", "BBB", "BBB", 
                                 "BBB", "BBB", "BBB", "BBB", "BBB", "BBB", "BBB", "BBB", "BBB", 
                                 "BBB", "BBB", "BBB", "BBB", "BBB", "BBB", "BBB", "BBB", "BBB", 
                                 "BBB", "BBB", "BBB", "BBB", "BBB", "BBB", "BBB", "BBB", "BBB", 
                                 "BBB", "BBB", "BBB", "BBB", "BBB", "BBB", "BBB", "BBB", "CCC", 
                                 "CCC", "CCC", "CCC", "CCC", "CCC", "CCC", "CCC", "CCC", "CCC", 
                                 "CCC", "CCC", "CCC", "CCC", "CCC", "CCC", "CCC", "CCC", "CCC", 
                                 "CCC", "CCC", "CCC", "CCC", "CCC", "CCC", "CCC", "CCC", "CCC", 
                                 "CCC", "CCC", "CCC", "CCC", "CCC", "CCC", "CCC", "CCC", "CCC", 
                                 "CCC", "CCC", "CCC", "CCC", "CCC", "CCC", "CCC", "CCC", "CCC", 
                                 "CCC", "CCC", "BBB", "BBB", "BBB", "BBB", "BBB", "BBB", "BBB", 
                                 "BBB", "BBB", "BBB", "BBB", "BBB", "BBB", "BBB", "BBB", "BBB", 
                                 "BBB", "BBB"), qc = c("QC_A1", "QC_A2", "QC_A1", "QC_A2", 
                                                       "QC_A1", "QC_A2", "QC_A1", "QC_A2", "QC_A1", "QC_A2", "QC_A1", 
                                                       "QC_A2", "QC_C1", "QC_C2", "QC_C3", "QC_C4", "QC_C1", "QC_C2", 
                                                       "QC_C3", "QC_C4", "QC_C1", "QC_C2", "QC_C3", "QC_C4", "QC_C1", 
                                                       "QC_C2", "QC_C3", "QC_C4", "QC_C1", "QC_C2", "QC_C3", "QC_C4", 
                                                       "QC_C1", "QC_C2", "QC_C3", "QC_C4", "QC_C1", "QC_C2", "QC_C3", 
                                                       "QC_C4", "QC_C1", "QC_C2", "QC_C3", "QC_C4", "QC_C1", "QC_C2", 
                                                       "QC_C3", "QC_C4", "QC_C1", "QC_C2", "QC_C3", "QC_C4", "QC_C1", 
                                                       "QC_C2", "QC_C3", "QC_C4", "QC_C1", "QC_C2", "QC_C3", "QC_C4", 
                                                       "QC_B1", "QC_B2", "QC_B3", "QC_B1", "QC_B2", "QC_B3", "QC_B1", 
                                                       "QC_B2", "QC_B3", "QC_B1", "QC_B2", "QC_B3", "QC_B1", "QC_B2", 
                                                       "QC_B3", "QC_B1", "QC_B2", "QC_B3", "QC_B1", "QC_B2", "QC_B3", 
                                                       "QC_B1", "QC_B2", "QC_B3", "QC_B1", "QC_B2", "QC_B3", "QC_B1", 
                                                       "QC_B2", "QC_B3", "QC_B1", "QC_B2", "QC_B3", "QC_B1", "QC_B2", 
                                                       "QC_B3", "QC_B1", "QC_B2", "QC_B3", "QC_B1", "QC_B2", "QC_B3", 
                                                       "QC_B1", "QC_B2", "QC_B3", "QC_C1", "QC_C2", "QC_C3", "QC_C4", 
                                                       "QC_C1", "QC_C2", "QC_C3", "QC_C4", "QC_C1", "QC_C2", "QC_C3", 
                                                       "QC_C4", "QC_C1", "QC_C2", "QC_C3", "QC_C4", "QC_C1", "QC_C2", 
                                                       "QC_C3", "QC_C4", "QC_C1", "QC_C2", "QC_C3", "QC_C4", "QC_C1", 
                                                       "QC_C2", "QC_C3", "QC_C4", "QC_C1", "QC_C2", "QC_C3", "QC_C4", 
                                                       "QC_C1", "QC_C2", "QC_C3", "QC_C4", "QC_C1", "QC_C2", "QC_C3", 
                                                       "QC_C4", "QC_C1", "QC_C2", "QC_C3", "QC_C4", "QC_C1", "QC_C2", 
                                                       "QC_C3", "QC_C4", "QC_B1", "QC_B2", "QC_B3", "QC_B1", "QC_B2", 
                                                       "QC_B3", "QC_B1", "QC_B2", "QC_B3", "QC_B1", "QC_B2", "QC_B3", 
                                                       "QC_B1", "QC_B2", "QC_B3", "QC_B1", "QC_B2", "QC_B3"), analyzer = c("X1_C", 
                                                                                                                           "X1_C", "X3_C", "X3_C", "X1_C", "X1_C", "X3_C", "X3_C", "X1_C", 
                                                                                                                           "X1_C", "X3_C", "X3_C", "X1_B1", "X1_B1", "X1_B1", "X1_B1", 
                                                                                                                           "X1_B2", "X1_B2", "X1_B2", "X1_B2", "X2_B", "X2_B", "X2_B", 
                                                                                                                           "X2_B", "X3_B", "X3_B", "X3_B", "X3_B", "X1_B1", "X1_B1", 
                                                                                                                           "X1_B1", "X1_B1", "X1_B2", "X1_B2", "X1_B2", "X1_B2", "X2_B", 
                                                                                                                           "X2_B", "X2_B", "X2_B", "X3_B", "X3_B", "X3_B", "X3_B", "X1_B1", 
                                                                                                                           "X1_B1", "X1_B1", "X1_B1", "X1_B2", "X1_B2", "X1_B2", "X1_B2", 
                                                                                                                           "X2_B", "X2_B", "X2_B", "X2_B", "X3_B", "X3_B", "X3_B", "X3_B", 
                                                                                                                           "X1_C", "X1_C", "X1_C", "X2_C", "X2_C", "X2_C", "X3_C", "X3_C", 
                                                                                                                           "X3_C", "X1_C", "X1_C", "X1_C", "X2_C", "X2_C", "X2_C", "X3_C", 
                                                                                                                           "X3_C", "X3_C", "X1_C", "X1_C", "X1_C", "X2_C", "X2_C", "X2_C", 
                                                                                                                           "X3_C", "X3_C", "X3_C", "Y1_C", "Y1_C", "Y1_C", "Y2_C", "Y2_C", 
                                                                                                                           "Y2_C", "Y1_C", "Y1_C", "Y1_C", "Y2_C", "Y2_C", "Y2_C", "Y1_C", 
                                                                                                                           "Y1_C", "Y1_C", "Y2_C", "Y2_C", "Y2_C", "Z1_B1", "Z1_B1", 
                                                                                                                           "Z1_B1", "Z1_B1", "Z1_B2", "Z1_B2", "Z1_B2", "Z1_B2", "Z2_B1", 
                                                                                                                           "Z2_B1", "Z2_B1", "Z2_B1", "Z2_B2", "Z2_B2", "Z2_B2", "Z2_B2", 
                                                                                                                           "Z1_B1", "Z1_B1", "Z1_B1", "Z1_B1", "Z1_B2", "Z1_B2", "Z1_B2", 
                                                                                                                           "Z1_B2", "Z2_B1", "Z2_B1", "Z2_B1", "Z2_B1", "Z2_B2", "Z2_B2", 
                                                                                                                           "Z2_B2", "Z2_B2", "Z1_B1", "Z1_B1", "Z1_B1", "Z1_B1", "Z1_B2", 
                                                                                                                           "Z1_B2", "Z1_B2", "Z1_B2", "Z2_B1", "Z2_B1", "Z2_B1", "Z2_B1", 
                                                                                                                           "Z2_B2", "Z2_B2", "Z2_B2", "Z2_B2", "Z1_C", "Z1_C", "Z1_C", 
                                                                                                                           "Z2_C", "Z2_C", "Z2_C", "Z1_C", "Z1_C", "Z1_C", "Z2_C", "Z2_C", 
                                                                                                                           "Z2_C", "Z1_C", "Z1_C", "Z1_C", "Z2_C", "Z2_C", "Z2_C"), 
                     year_month = c("2025-09", "2025-09", "2025-09", "2025-09", 
                                    "2025-10", "2025-10", "2025-10", "2025-10", "2025-11", "2025-11", 
                                    "2025-11", "2025-11", "2025-09", "2025-09", "2025-09", "2025-09", 
                                    "2025-09", "2025-09", "2025-09", "2025-09", "2025-09", "2025-09", 
                                    "2025-09", "2025-09", "2025-09", "2025-09", "2025-09", "2025-09", 
                                    "2025-10", "2025-10", "2025-10", "2025-10", "2025-10", "2025-10", 
                                    "2025-10", "2025-10", "2025-10", "2025-10", "2025-10", "2025-10", 
                                    "2025-10", "2025-10", "2025-10", "2025-10", "2025-11", "2025-11", 
                                    "2025-11", "2025-11", "2025-11", "2025-11", "2025-11", "2025-11", 
                                    "2025-11", "2025-11", "2025-11", "2025-11", "2025-11", "2025-11", 
                                    "2025-11", "2025-11", "2025-09", "2025-09", "2025-09", "2025-09", 
                                    "2025-09", "2025-09", "2025-09", "2025-09", "2025-09", "2025-10", 
                                    "2025-10", "2025-10", "2025-10", "2025-10", "2025-10", "2025-10", 
                                    "2025-10", "2025-10", "2025-11", "2025-11", "2025-11", "2025-11", 
                                    "2025-11", "2025-11", "2025-11", "2025-11", "2025-11", "2025-09", 
                                    "2025-09", "2025-09", "2025-09", "2025-09", "2025-09", "2025-10", 
                                    "2025-10", "2025-10", "2025-10", "2025-10", "2025-10", "2025-11", 
                                    "2025-11", "2025-11", "2025-11", "2025-11", "2025-11", "2025-09", 
                                    "2025-09", "2025-09", "2025-09", "2025-09", "2025-09", "2025-09", 
                                    "2025-09", "2025-09", "2025-09", "2025-09", "2025-09", "2025-09", 
                                    "2025-09", "2025-09", "2025-09", "2025-10", "2025-10", "2025-10", 
                                    "2025-10", "2025-10", "2025-10", "2025-10", "2025-10", "2025-10", 
                                    "2025-10", "2025-10", "2025-10", "2025-10", "2025-10", "2025-10", 
                                    "2025-10", "2025-11", "2025-11", "2025-11", "2025-11", "2025-11", 
                                    "2025-11", "2025-11", "2025-11", "2025-11", "2025-11", "2025-11", 
                                    "2025-11", "2025-11", "2025-11", "2025-11", "2025-11", "2025-09", 
                                    "2025-09", "2025-09", "2025-09", "2025-09", "2025-09", "2025-10", 
                                    "2025-10", "2025-10", "2025-10", "2025-10", "2025-10", "2025-11", 
                                    "2025-11", "2025-11", "2025-11", "2025-11", "2025-11"), cv = c(2.64, 
                                                                                                   3.83, 2.66, 3.11, 1.63, 1.82, 3.74, 3.81, 1.46, 2.07, 3.78, 
                                                                                                   3.17, 1.27, 1.05, 1.82, 2.84, 1.12, 0.97, 1.36, 2.81, 1.16, 
                                                                                                   2.51, 1.66, 3.24, 1.03, 0.78, 1.11, 2.92, 1.82, 0.87, 1.46, 
                                                                                                   2.56, 1.61, 1.24, 1.3, 2.45, 1.68, 1.69, 1.33, 2.24, 2.11, 
                                                                                                   0.74, 1.38, 2.33, 1.14, 0.96, 1.23, 3.05, 1.34, 1.01, 1.09, 
                                                                                                   2.35, 1.35, 1.06, 1.08, 2.2, 2.22, 0.68, 1.09, 2.56, 3.92, 
                                                                                                   2.66, 2.59, 2.38, 2.83, 1.95, 2.26, 1.58, 1.68, 2.59, 1.61, 
                                                                                                   1.56, 5.78, 2.05, 1.87, 3.68, 3.65, 1.21, 4.99, 1.23, 1.02, 
                                                                                                   2.63, 1.79, 1.13, 2.21, 1.22, 1.37, 4.99, 4.02, 4.12, 3.09, 
                                                                                                   1.32, 1.39, 4.52, 2.11, 2.97, 5.48, 2.31, 2.85, 2.89, 1.41, 
                                                                                                   1.31, 2.79, 1.49, 1.51, 1.35, 1.37, 1.48, 2.91, 1.21, 1.25, 
                                                                                                   1.25, 2.93, 1.31, 1.55, 1.41, 3.19, 1.34, 4.05, 1.36, 2.64, 
                                                                                                   1.24, 1.46, 1.42, 2.82, 1.78, 1.41, 1.52, 3.01, 2.24, 1.37, 
                                                                                                   1.84, 3.95, 1.53, 1.21, 1.21, 3.76, 2.49, 1.55, 1.26, 2.93, 
                                                                                                   2.39, 1.56, 1.26, 2.52, 2.06, 0.94, 1.19, 2.25, 1.73, 1.02, 
                                                                                                   1.31, 2.72, 2.24, 1.08, 2.09, 5.57, 3.65, 3.56, 3.19, 2.45, 
                                                                                                   2.14, 3.2, 1.8, 2.13, 2.56, 1.23, 1.21, 2.62, 1.44, 1.37)), row.names = c(NA, 
                                                                                                                                                                             -171L), class = c("tbl_df", "tbl", "data.frame"))
    
    

    Code:

    # mutate numbers of qc and site
    df0 <-
      df0 |>
      group_by(analyte) |> 
      mutate(
        nsite = n_distinct(site),
        nqc = n_distinct(qc)) |> 
      ungroup()
    
    # list plots
    list_plot <- df0 |>
      group_split(analyte) |> 
      map(~ggplot(., aes(x = year_month, y = cv, group = analyzer)) +
            facet_wrap2(
              vars(qc, site),
              nrow = unique(.$nqc),
              drop = FALSE,
              scales = "free_y",
              strip = strip_nested()
            ) +
            scale_x_discrete(expand = expansion(mult = 0.05)) +
            scale_y_continuous(breaks = breaks_pretty(n=4)) +
            geom_line(linewidth = 1) +
            theme(legend.position = "none")
      )
    
    # display each plot
    list_plot[[1]]
    

    plot1

    list_plot[[2]]
    

    plot2

    list_plot[[3]]
    

    plot3

    Created on 2026-01-02 with reprex v2.1.1