Panel tabset from list of plots

library(tidyverse)
library(glue)

# Make some plots
p1 <- ggplot(mpg, aes(x = cyl, y = hwy, color = drv)) +
  geom_point()

p2 <- ggplot(mpg, aes(x = hwy, fill = drv)) +
  geom_density()

p3 <- ggplot(mpg, aes(x = displ, y = hwy, color = drv)) +
  geom_point()

# I find it easiest to work with all these things in a data frame, but it's not necessary
output_stuff <- tribble(
  ~panel_title, ~plot,
  "Plot 1", p1,
  "Plot 2", p2,
  "Plot 3", p3
)

The plots are all in this output_stuff$plot list:

output_stuff$plot[[3]]

For a tabset, the markdown needs to look like this:

::: {.panel-tabset}
### Title

```{r}
output_stuff$plot[[1]]
```

### Another title

```{r}
output_stuff$plot[[2]]
```

:::

So we can build each of these panels in the data frame:

build_panel <- function(panel_title, plot_index) {
  output <- glue("
  ### <<panel_title>>

  ```{r}
  #| echo: false
  output_stuff$plot[[<<plot_index>>]]
  ```", .open = "<<", .close = ">>")
  # ↑ It's best to override glue's {} delimiters because of the ```{r} chunk

  output
}

output_with_markdown <- output_stuff |> 
  mutate(row = row_number()) |> 
  # We could use map2_chr(), but I like using pmap() just in case I need to
  # expand it beyond 2 things
  mutate(markdown = pmap_chr(
    lst(panel_title, row), 
    \(panel_title, row) build_panel(panel_title, plot_index = row)
  ))

Now we have a markdown panel for each plot:

cat(output_with_markdown$markdown[[1]])
### Plot 1

```{r}
#| echo: false
output_stuff$plot[[1]]
```
# We can collapse all these panels into one big character object, wrapped in Quarto's special .panel-tabset div
final_markdown <- paste0(
  "::: {.panel-tabset}\n\n",
  paste0(output_with_markdown$markdown, "\n", collapse = "\n"),
  "\n:::"
)

Check it out!

cat(final_markdown)
::: {.panel-tabset}

### Plot 1

```{r}
#| echo: false
output_stuff$plot[[1]]
```

### Plot 2

```{r}
#| echo: false
output_stuff$plot[[2]]
```

### Plot 3

```{r}
#| echo: false
output_stuff$plot[[3]]
```

:::

However, this won’t render/knit properly, even if we add results="asis" to the chunk options.

But we can feed it to knitr::knit() in an inline chunk:

# This, but in an inline chunk
knitr::knit(text = final_markdown)