quarto-dev / quarto-cli

Open-source scientific and technical publishing system built on Pandoc.
https://quarto.org
Other
3.9k stars 322 forks source link

Tabsets generated with R loop don't work with knitr cache on #7602

Closed epruesse closed 11 months ago

epruesse commented 11 months ago

Bug description

Rendering multiple tabsets programmatically (which I find commonly useful to avoid repetition in writing when many plots need to be made available in report) only works with #| cache: false, not with #| cache: true in R code blocks. Since ggplot2 rendering takes quite a while, caching that dozen or more plots would help a lot. As is, all plots within such a tabset have to be rendered over and over with every change to the document.

Steps to reproduce

This works:

::: {.panel-tabset}
```{r tabset-test}
#| results: asis
#| cache: false
for (n in seq(3)) {
    cat(paste0("\n### table ", n))
    knitr::kable(data.frame(a=1:3, b=4:6)) |> print()
}

:::


This does not (only `#| cache: true` changed)

````qmd
::: {.panel-tabset}
```{r tabset-test}
#| results: asis
#| cache: true
for (n in seq(3)) {
    cat(paste0("\n### table ", n))
    knitr::kable(data.frame(a=1:3, b=4:6)) |> print()
}

:::


### Expected behavior

An empty div is rendered when cache is enabled.

### Actual behavior

The tabset should appear when cache is enabled.

### Your environment

- Emacs (poly-quarto with ess)
- Linux

### Quarto check output

```bash

[✓] Checking versions of quarto binary dependencies...
      Pandoc version 3.1.1: OK
      Dart Sass version 1.58.3: OK
[✓] Checking versions of quarto dependencies......OK
[✓] Checking Quarto installation......OK
      Version: 1.3.450
      Path: .../miniconda3/envs/quarto/bin

[✓] Checking basic markdown render....OK

[✓] Checking Python 3 installation....OK
      Version: 3.8.5
      Path: /usr/bin/python3
      Jupyter: (None)

      Jupyter is not available in this Python installation.
      Install with python3 -m pip install jupyter

[✓] Checking R installation...........OK
      Version: 4.3.2
      Path: .../miniconda3/envs/quarto/lib/R
      LibPaths:
        - .../miniconda3/envs/quarto/lib/R/library
      knitr: 1.45
      rmarkdown: 2.25

[✓] Checking Knitr engine render......OK
cderv commented 11 months ago

Thanks for the report. Using cache: true is adding an option that triggers a .cell div addition, which we don't want here in the tabset environment as you are using it.

Though it is working like this (after the fix I'll push), to improve your code you should have a look at knit_child() and knit_expand(). This will support much more complex output (like htmlwidget, or ggplot), because print on kable() works because this is markdown output, but it would not work with gt I believe.

Example: https://examples.quarto.pub/create-tabsets-panel-from-r-code/

For yours, this would be something like

---
title: "Tabset"
format: html
keep-md: true
---

```{r tabset-test}
#| results: asis
#| echo: false
#| panel: tabset
res <- lapply(seq(3), function(n) {
    res <- knitr::knit_child(text = c(
      "### table `r n`",
      "",
      "```{r}", 
      "#| cache: true",
      "knitr::kable(data.frame(a=1:3, b=4:6))", 
      "```"), quiet = TRUE, envir = environment())
})
cat(unlist(res), sep = "\n")

This is basically an iteration with knitr::knit() on a template that looks like 
````markdown
# Table `r n`

```{r}
#| cache: true
knitr::kable(data.frame(a=1:3, b=4:6))


Which the content you want to set in your tabsets as if you had written the all blocks

Hope this helps
cderv commented 11 months ago

Closed by 26987f3

epruesse commented 11 months ago

Thank you @cderv! That was an impressively fast response and fix!

cderv commented 11 months ago

Thanks for the report. We would have not found this issue without a user report. So really thank you !

epruesse commented 11 months ago

Here's a helper based on your code above to make the qmd more readable. In case it helps others.

Having this:

make_tab <- function(title, expr, envir = parent.frame(), cache = TRUE, print = TRUE) {
    text = c(
        paste("###", title),
        "",
        "```{r}",
        if (cache) "#| cache: true",
        deparse(substitute(expr)),
        "```"
    )
    res <- knitr::knit_child(text = text, envir = envir, quiet = TRUE)
    if (print) {
        cat(res)
    } else {
        res
    }
}

We can write:

```{r tabset-test}
#| results: asis
#| panel: tabset
for (n in seq(3)) {
    make_tab("Table `r n`", {
        knitr::kable(data.frame(a=1:3, b=5:7))
    })
}

or 

````qmd
```{r tabset-test-lapply}
#| results: asis
#| panel: tabset
tabs <- lapply(seq(3), \(n) {
    make_tab("Table `r n`", {
        knitr::kable(data.frame(a=1:3, b=5:7))
    }, print = FALSE)
})
cat(unlist(tabs))