Search code examples
tidyversegtquarto

Iterating to create tabs with gt in quarto


Something that is very handy is to iterate through a variable and then dynamically create tabs based on values of that variable (here homeworld). This works well with the results: asis chunk option. I can make that work but there is some strange interaction with the {gt} package whereby I can only make gt work with purrr::walk if I use gt::as_raw_html. However if I just produce a single table outside of purrr::walk I don't need gt::as_raw_html. Here is the error message I get {gt} does not work:

Error running filter /Applications/quarto/share/filters/quarto-pre/quarto-pre.lua: ...lications/quarto/share/filters/quarto-pre/quarto-pre.lua:2410: attempt to concatenate a nil value (local 'v') stack traceback: ...lications/quarto/share/filters/quarto-pre/quarto-pre.lua:2417: in function <...lications/quarto/share/filters/quarto-pre/quarto-pre.lua:2415>

Here is the quarto (quarto version 1.1.175) code to reproduce:

---
title: "Untitled"
format: html
execute:
  warning: false
---

```{r r-pkgs}
library(dplyr)
library(glue)
library(gt)
library(purrr)

## just to simplify
starwars <- starwars %>% 
  filter(!is.na(sex))
```

# Does work

::: {.panel-tabset}

```{r}
#| results: asis

walk(
  unique(starwars$sex), \(hw) {
    cat(glue("## {hw} \n\n"))
    
    starwars %>% 
      filter(sex == hw) %>% 
      count(homeworld) %>% 
      head() %>% 
      gt() %>% 
      as_raw_html() %>% 
      print()
    
    cat("\n\n")
  }
)
```
:::


# Does not work

::: {.panel-tabset}

```{r}
#| results: asis
#| eval: false

walk(
  unique(starwars$sex), \(hw) {
    cat(glue("## {hw} \n\n"))
    
    starwars %>% 
      filter(sex == hw) %>% 
      count(homeworld) %>% 
      head() %>% 
      gt() %>% 
      print()
    
    cat("\n\n")
  }
)
```
:::

## single does work
```{r}
#| results: asis
starwars %>% 
  count(homeworld) %>% 
  head() %>% 
  gt()
```

Solution

  • Posting back answer from the duplicate issue in quarto-dev/quarto-cli#2370

    About the issue

    The behavior you see has to do with the print method used, and the iteration with walk()

    When you use print() after gt() or after gt() %>% as_raw_html() it will not have the same effect, as the print method used will not be the same. In the context of knitr, this matters.

    Using as_raw_html() makes sense to include table as raw HTML in such document, and it probably will have the same result as when gt object are printing in knitting to HTML table (though the use of htmltools). When you use gt() %>% print(), it will not use the correct printing method that is used when just gt() is in a chunk (the knit_print() method more on that here for advanced understanding.

    More on how to create content dynamically with knitr

    Let me add some context about knitr and dynamically created content.

    Iterating to dynamically create content in knitr require to use the correct print method (usually knit_print()), and it is better to iterate on child content with knitr::knit_child() function that will correctly handle the printed output specific to sewing result in the document. We have some resource about that in R Markdown Cookbook that would apply to Quarto as well.

    As an example, this is how we recommend to dynamically create content when using knitr content so that R code result is correctly mixed with other content, specifically when the content to dynamically create is a mix of raw markdown, and R code results.

    ---
    title: "Untitled"
    format: html
    execute:
      warning: false
    keep-md: true
    ---
    
    ```{r r-pkgs}
    library(dplyr)
    library(glue)
    library(gt)
    library(purrr)
    
    ## just to simplify
    starwars <- starwars %>% 
      filter(!is.na(sex))
    ```
    
    # Tables
    
    ::: {.panel-tabset}
    
    ```{r}
    #| output: asis
    res <- purrr::map_chr(unique(starwars$sex), \(hw) {
        knitr::knit_child(text = c(
          "## `r hw`",
          "", 
          "```{r}",
          "#| echo: false",
          "starwars %>%",
          "  filter(sex == hw) %>%",
          "  count(homeworld) %>%",
          "  head() %>%",
          "  gt()",
          "```",
          "",
          ""
        ), envir = environment(), quiet = TRUE)
      })
    
    cat(res, sep = '\n')
    ```
    :::
    
    
    

    You could also put the child content in a separate file for easier writing as in the Cookbook, by writing what you would need as document for one value you are iterating

    ## `r hw`
     
    ```{r}
    #| echo: false
    starwars %>%
      filter(sex == hw) %>%
      count(homeworld) %>%
      head() %>%
      gt()
    ```
    

    This is the safest way to mix markdown content (like ##) with R code output ( like Tables or Htmlwidgets) which are content you can't easily just cat() into the file.

    See more in the issue