Parameterized Reporting
with Quarto

Jadey Ryan | January 18, 2024
R-Ladies Washington DC

Acknowledgements

R-Ladies Washington DC Logo

License

Opinions expressed are solely my own and do not express the views of my employer or any organizations I am associated with.

This work is licensed under Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International (CC BY-NC-SA).

Jadey Ryan

Data scientist at WA Dept of Agriculture

Business owner of The Coding Cats

jadeyryan.com

@jadeynryan

linkedin.com/in/jadey-ryan

jadeynryan

thecodingcats.etsy.com

Three snowshoe siamese cats in loaf mode. From left to right: Tai, Mai, and Skye

Jadey collecting a bulk density soil sample in a field of wheat stubble.

Jadey standing in a field of wheat stubble holding a 3 foot long soil sampling probe over her shoulder.

Jadey standing in a grazed wildflower meadow with two colleagues collecting soil samples.

Photo credit: @leslie.mmichel

Logistics

  • Login to Posit Cloud: bit.ly/rladies-parameterized-quarto.

  • If Posit Cloud doesn’t work, download materials locally:

    usethis::use_course(
      "https://github.com/jadeynryan/rladies-parameterized-quarto/raw/main/exercises/exercises.zip")
  • Ask questions in the public Zoom chat or raise hand ✋.

    Zoom reactions panel with options for emojis and raising hand.

  • Workshop structure: presentation, exercises, questions to answer in chat [💬 Chat:], and demos.

💪🏼 Exercise 0

  1. Raise/lower your hand on Zoom.

  2. Use your favorite reaction.

  3. 💬 Chat: share your name, where you’re calling from, and one thing you’ve made that you’re proud of.

01:00

Learning objectives

  1. Understand what parameterized reporting is and when it is useful.


  1. Learn how to convert a report into a parameterized template.


  1. Render all variations of the report at once using {quarto} and {purrr}.


  1. Generate multiple format outputs from the same template file with conditional content and conditional code execution.

Syntax and RStudio aside

Pipes

  • 2014+ magrittr pipe %>%

  • 2021+ (R \(\geq\) 4.1.0) native R pipe |>

Isabella Velásquez’s blog post Understanding the native R pipe |> (2022)

do_something(arg1, arg2, arg3, ...)

arg1 |>  
  do_something(arg2, arg3)
mean(0:10)

0:10 |> 
  mean()


To change Ctrl + Shift + M shortcut to the native pipe:

ToolsGlobal OptionsCodeEditingUse Native Pipe Operator

Namespacing

package::function()

dplyr::select()

  • tells R explicitly to use the function select from the package dplyr

  • helps avoid name conflicts (e.g., MASS::select())

  • does not require library(dplyr)

library(dplyr)

mtcars |>  
  select(mpg, cyl) 
# library(dplyr) not needed

mtcars |>  
  dplyr::select(mpg, cyl) 

RStudio options

ToolsGlobal Options

  • Fussy YAML indentation:

    CodeDisplayIndentation guides:Rainbow lines


  • Match parentheses:

    CodeDisplayIndentation guides: → Check Use rainbow parentheses


  • Matching divs:

    R MarkdownAdvanced → Check Use rainbow fenced divs

R Markdown → Quarto


💬 Chat: what’s your comfort level / experience with R Markdown and Quarto?

R Markdown

Hex logos for various packages from the R Markdown ecosystem.

Figure from “Hello, Quarto” keynote by Julia Lowndes and Mine Çetinkaya-Rundel, presented at RStudio::Conf(2022).

Quarto

A schematic representing the multi-language input (e.g. Python, R, Observable, Julia) and multi-format output (e.g. PDF, html, Word documents, and more) versatility of Quarto.

Artwork from “Hello, Quarto” keynote by Julia Lowndes and Mine Çetinkaya-Rundel, presented at RStudio::Conf(2022). Illustrated by Allison Horst.

A schematic representing rendering of Quarto documents from .qmd, to knitr or jupyter, to plain text markdown, then converted by pandoc into any number of output types including html, PDF, or Word document.

R Markdown vs Quarto

R Markdown

  • Vast R Markdown ecosystem

  • Dependent on R

Quarto

  • Command line interface (CLI)

  • Expands R Markdown ecosystem

  • “Batteries included”

  • Multi-language and multi-engine


R Markdown will still be maintained but likely no new features (Xie 2022).

Differences with parameters

No Quarto equivalent to .Rmd Knit with Parameters GUI built with Shiny {miniUI}.

Input parameter values interactively for parameterized reports.

Figure from R Markdown: The Definitive Guide (Xie et al. 2023)

  • Workaround: build webapp to get input, serialize to YAML, pass to Quarto render.

  • More info: GitHub discussion

Convert .Rmd.qmd*

  1. Change file extension from .Rmd.qmd
  2. Change YAML header (output:format:)
  3. Convert chunk headers with knitr::convert_chunk_header()

*If you want.

R Markdown

```{r, label=rmarkdown, eval=FALSE}
# code
```

Quarto

```{r}
#| label: quarto
#| eval: false

# code
```

Resources for R Markdown users

Parameterized reports

Many use cases

Different audiences, different reports

Show code for technical staff and hide code for everyone else.

See this StackOverflow question and answer for an example.


💬 Chat: what kinds of reports would you like to parameterize? What would the parameters be?

Like a custom function

File with the word '.qmd' inside and the word 'Function' above.

An arrow points from 'Input' with 'params$year' to the previous image with 'Function' and '.qmd' file.

In addition to the previous two images, arrows point to five reports with years 2019 through 2023 on them in a flow chart.

What makes a report “parameterized”?

  • YAML header with params key-value pairs

    • Yet Another Markdown Language or YAML Ain’t Markup Language 🤷🏻‍♀️
  • Use those params in your report to create different variations

Important

Valid parameter values are strings, numbers, or Boolean.

Must serialize a dataframe to pass it as a parameter, then un-serialize it back to a dataframe within the .qmd content.

See Christophe Dervieux’s answer in Posit Community to understand why.

See John Paul Helveston’s blog post to learn how to use {jsonlite} as a workaround.

Workflow

  1. Write report template with default values hard-coded, and then render & review.

  2. Set default params key-value pairs in YAML. Replace hard-coded values with the params variables.

  3. Render the single report and review.

  4. Render extreme cases and review.

    • Parameter values with barely any data and with tons of data.
  5. Render all variations of the report at once.

💪🏼 Exercise 1

Explore a report without parameters and see where we could add them.

  1. Open ex-1-swiss-cats.qmd.

  2. Click the Quarto render button in RStudio Render button.

  3. Look at the source markdown and code and the rendered report.

  4. Update your name as the author. Re-render.

  5. 💬 Chat: what variables could we set as parameters?

05:00

Set params in YAML header

---
title: "Swiss Cats"                 # Metadata
format:                             # Set format types
  html: default                                     
  docx: default                           
params:                             # Set default parameter key-value pairs
  fave_breed: "Snowshoe"                                
---
    
Report content goes here.           # Write content

Important

Your default params key-value pairs must be found in your dataset.

Access params

Run any line or chunk to add params to your environment.

params object is a list.

str(params)
List of 1
 $ fave_breed: chr "Snowshoe"


Access with $ notation.

params$fave_breed
[1] "Snowshoe"


For inline code in YAML or report content, enclose the expression in `r `.

My favorite cat breed is the **`r params$fave_breed`**.

My favorite cat breed is the Snowshoe.

Replace hard-coded values with params

Ctrl + F to find where to replace hard-coded values with params


Use inline R code for markdown

Text textity text **`r params$___`**.


Use paste() for plot and table titles and labels

# ggplot code +
labs(title = paste(params$fave_breed, "population"))

💃🏻 Demo

Modify ex-1-swiss-cats.qmd to add pet_type and fave_breed parameters.

Rendering reports

Multiple ways to render

  1. Quarto render button in RStudio Render button in RStudio or Ctrl + Shift + K keyboard shortcut

  2. Check Render on Save and Ctrl + S keyboard shortcut

  3. Quarto CLI

    Terminal
    quarto render ex-2-quarto-render.qmd -P pet_type:'cats' -P fave_breed:'Snowshoe'
  4. quarto::quarto_render()

    Console or R script
    quarto::quarto_render(
      input = here::here("ex-2-quarto-render.qmd"), # Input Quarto file
      execute_params = list(                        # Named list of params
        pet_type = "cats",
        fave_breed = "Snowshoe"))

💪🏼 Exercise 2

Change parameters and render using 1) Render button and 2) quarto_render().

  1. Look at the unique pet breeds and pick your favorite.

    # Run in the console
    pets |>
       dplyr::distinct(pet_type, breed) |>
       View()
  2. In ex-2-quarto-render.qmd Change the default parameters in the YAML to your favorite pet type and breed. Render using the button.

  3. Now render with quarto::quarto_render(). Replace ___ with different parameters:

    # Run in the console
     quarto::quarto_render(
       input = here::here("ex-2-quarto-render.qmd"),
       execute_params = list(
         pet_type = "___",
         fave_breed = "___"))
  4. 💬 Chat: what’s your fave breed and do you have any pets? 🐈🐕🐹🐍🐠

07:00

Render all 538 reports

One HTML report for each cat breed and each dog breed.

pets <- readr::read_rds(here::here("data", "pets.RDS"))

pets |>
  dplyr::distinct(pet_type, breed) |>
  dplyr::count(pet_type) |>
  janitor::adorn_totals()
pet_type n
cats 104
dogs 434
Total 538
  1. Change the default params in the YAML.

  2. Render button in RStudio or Ctrl + Shift + K keyboard shortcut.

  3. Change the file name to add the parameter.

    output-file: YAML option doesn’t seem to work with inline R code.

  4. Repeat 537 times.

quarto::quarto_render(
  input = here::here("ex-2-quarto-render.qmd"),
  output_file = "dogs-affenpinscher-report.html",
  execute_params = list(
    pet_type = "dogs",
    fave_breed = "Affenpinscher"))

quarto::quarto_render(
  input = here::here("ex-2-quarto-render.qmd"),
  output_file = "dogs-afghan-hound-report.html",
  execute_params = list(
    pet_type = "dogs",
    fave_breed = "Afghan Hound"))

quarto::quarto_render(
  input = here::here("ex-2-quarto-render.qmd"),
  output_file = "dogs-aidi-chien-de-montagne-de-l-atlas-report.html",
  execute_params = list(
    pet_type = "dogs",
    fave_breed = "Aidi Chien De Montagne De L Atlas"))

quarto::quarto_render(
  input = here::here("ex-2-quarto-render.qmd"),
  output_file = "dogs-akita-report.html",
  execute_params = list(
    pet_type = "dogs",
    fave_breed = "Akita"))

# + 534 more times... 

Create a dataframe with three columns that match quarto_render() args:

  • output_format: file type (html, revealjs, pdf, docx, etc.)

  • output_file: file name with extension

  • execute_params: named list of parameters

Map over each row:

  • purrr::pwalk(dataframe, quarto_render, quarto_render_args)

Create dataframe to iterate over

pet_reports <- pets |>
  dplyr::distinct(pet_type, breed) |>   # Get distinct pet/breed combos
  dplyr::mutate(
    output_format = "html",             # Make output_format column
    output_file = paste(                # Make output_file column
      tolower(pet_type),
      tolower(gsub(" ", "-", breed)),
      "report.html",
      sep = "-"
    ),
    execute_params = purrr::map2(       # Make execute_params column
      pet_type,
      breed,
      \(pet_type, breed) list(pet_type = pet_type, breed = breed)))

Subset to first 2 cat/dog breeds

pet_reports_subset <- pet_reports |>
  dplyr::slice_head(n = 2, by = pet_type) |>
  dplyr::select(output_file, execute_params)

pet_reports_subset
output_file execute_params
cats-abyssiniane-report.html cats , Abyssiniane
cats-aegean-cat-report.html cats , Aegean Cat
dogs-affenpinscher-report.html dogs , Affenpinscher
dogs-afghan-hound-report.html dogs , Afghan Hound

Map over each row

  • purrr::pwalk() iterates over multiple arguments simultaneously.

  • First .l argument is a list of vectors.

    • Dataframe is a special case of .l that iterates over rows.
purrr::pwalk(
  .l = pet_reports_subset,                      # Dataframe to map over
  .f = quarto::quarto_render,                   # Function we are applying to each row
  input = here::here("ex-2-quarto-render.qmd"), # Arguments of .f
  .progress = TRUE                              # Show a progress bar :)
)

Limitations


  • If using embed-resources: true YAML option, .qmd can’t be in subfolder, otherwise:

💃🏻 Demo

Demo ex-3-render-reports.R.

Multiple formats

Add to the format: YAML option to render additional output formats from the same .qmd file.

---
format:
  html: 
    embed-resources: true   # Makes the report self-contained
  pdf: default              # If not using any additional format options,
  docx: default             # set value to `default`  
---

Note: the Render button now has a dropdown!

Screenshot of Quarto file with the Render dropdown showing options for HTML, PDF, and MS Word formats.

Quarto docs on multiple formats

💪🏼 Exercise 3

Add another format by updating 1) the YAML, 2) the pet_reports dataframe, and 3) the regexp argument.

  1. Modify the YAML of ex-3-render-reports.qmd to add a new format (see all the options).

  2. Modify ex-3-render-reports.R to:

    • Add this new format to the pet_reports dataframe used in pwalk()

    • Include “.html” OR “.docx” OR “.pdf” in the regexp argument in dir_ls()

    • Hint: use the | pipe OR operator.

  3. 💬 Chat: were any of these functions from the .qmd or .R files new to you?

10:00

Conditional content

Control content visibility

Special .content-visible and .content-hidden classes with when-format="___" and unless-format="___" attributes can be applied to:

::: {.content-visible when-format="html"}

Will only appear in HTML.

:::
```{.python .content-visible when-format="html"}
# code shown only in HTML
2 + 2
```
Some text
[in HTML.]{.content-visible when-format="html"}
[in PDF.]{.content-visible when-format="pdf"}

Useful for static/interactive features

Pairs well with {{< include >}} shortcodes to re-use content without copying/pasting.

Use an underscore (_) prefix for included files so they are automatically ignored by a Quarto render of a project (Includes Quarto Docs).

Use .content-visible unless-format="html"

:::: {.content-visible unless-format="html"}

## Cats

{{< include _cats.qmd >}}

## Dogs

{{< include _dogs.qmd >}}

::::

Use .content-visible when-format="html"

:::: {.content-visible when-format="html"}
::: panel-tabset

## Cats

{{< include _cats.qmd >}}

## Dogs

{{< include _dogs.qmd >}}

:::
::::

💪🏼 Exercise 4

Use conditional content divs to control when tabsets are shown.

  1. Modify ex-4-conditional-content.qmd so that the panel-tabset visible for .html reports and hidden for .pdf & .docx reports.

Options:

  • {.content-visible when-format=“___“}

  • {.content-visible unless-format=“___“}

  • {.content-hidden unless-format=“___“}

  • {.content-hidden unless-format=“___“}

  1. 💬 Chat: what other kinds of content might you want to make visible for only a certain format?
05:00

Conditional code execution

  • More efficient to not execute code that generates interactive outputs for static reports.

  • Useful for executing interactive plot code for HTML reports and static ggplot2 code for all other formats.

  • Not currently a feature of Quarto v1.3, but a Quarto dev thinks it could be possible in v1.4 according to this GitHub discussion. Granted, that comment was from 2023-01-02…

Workaround for conditional R code

Include in the setup chunk of your .qmd file.

Get the format of the Pandoc output:

```{r}
#| label: setup

# Get output format
format <- knitr::opts_knit$get("rmarkdown.pandoc.to")
```

Use eval: !expr chunk option

```{r}
#| eval: !expr format %in% c("latex", "docx")

# code to create static {ggplot2}
```
```{r}
#| eval: !expr format == "html"

# code to create interactive {plotly}
```


Can even use params in !expr:

#| eval: !expr params$fave_breed == "Snowshoe"

💪🏼 Exercise 5

Set a format variable and use it in the eval: chunk option to execute ggplot2 code only for static reports and plotly code only for HTML documents.

  1. Modify ex-5-conditional-code.qmd, get the output format in a variable in the setup chunk.
# Get output format in setup chunk:
format <- knitr::opts_knit$get("rmarkdown.pandoc.to")
  1. Set chunk options for ggplot code blocks and plotly code blocks.
# Insert in chunk options:
#| eval: !expr format == "html"
# or
#| eval: !expr format %in% c("latex", "docx")
  1. 💬 Chat: what is something you learned from this workshop that you’re excited to apply to your own reports and projects?
05:00

Summary

Learning objective 1

Understand what parameterized reporting is and when it is useful.

Like very fancy custom functions:

  • Function → .qmd template

  • Input → parameters

  • Output → rendered reports

Useful for creating variations of the same report:

  • country, state, county, or city

  • time periods

  • breeds, species, diseases, trials, etc.

Note

We only covered reports, but you can also parameterize revealjs presentations! See this Jumping Rivers blog post about it.

Learning objective 2

Learn how to convert a report into a parameterized template.

  • Include default params: in YAML

  • Replace hard-coded values with params$pet_type

    • YAML:

      ---
      title: "Report about `r params$pet_type`"
      params:
        pet_type: "cats"
      ---
    • Inline R code:

      I like **`r params$pet_type`**.
    • Code chunks:

      pets |> 
          dplyr::filter(pet_type == params$pet_type)

Learning objective 3

Render all variations of the report at once using {quarto} and {purrr}.

  1. Get all unique parameter combinations into a dataframe:
head(pet_reports, 1)
pet_type breed output_format output_file execute_params
cats Abyssiniane html cats-abyssiniane-report.html cats , Abyssiniane


  1. Use dataframe in pwalk() with quarto_render():
purrr::pwalk(
  pet_reports,
  quarto::quarto_render,
  input = here::here("pet_template.qmd"),
  .progress = TRUE
)

Learning objective 4

Generate multiple format outputs from the same template with conditional content and code.

Content visibility

  • {.content-visible when-format=“___“}

  • {.content-visible unless-format=“___“}

  • {.content-hidden unless-format=“___“}

  • {.content-hidden unless-format=“___“}

Code execution

#| label: setup

format <- knitr::opts_knit$get("rmarkdown.pandoc.to")
#| label: interactive-plot
#| eval: !expr format == "html"

# plotly code
#| label: static-plot
#| eval: !expr format %in% c("pdf", "docx")

# ggplot2 code

Thank you!

🙏🏼 Please let me know how I did in this short survey


🏡 Home for all workshop materials: https://jadeyryan.quarto.pub/rladies-dc-quarto-params/

From left to right, Mai, Tai, and Skye. Three snowshoe cats cuddling in their warming beds.