Render parameterized reports with Quarto

quarto
productivity
R

A quick how-to guide for making reports in Quarto with custom parameters, and a couple fixes for some current limitations.

Author

John Paul Helveston

Published

2023-02-28

Quarto is an open-source scientific and technical publishing system built on Pandoc. Many view it as the “next generation” of RMarkdown, though it is more general in that is supports mutliple languages and is not R-specific.

One of the most common use cases for me is creating parameterized reports. A parameterized report is one that uses a .qmd file with parameters that can be passed to it while rendering, enabling the ability to create different versions of the output report based on the parameters.

BTW, Meghan Hall has a great post on this topic and goes into much more detail on how to customize outputs—take a look!

As a professor, one way I use parameterized reports is in providing my student’s unique feedback for their assignments. Using parameters like student_name, grade, and feedback, I am able to use a single .qmd file and then render a unique report for each student where those parameters are replaced with the appropriate information for each student.

RMarkdown and Quarto use almost identical interfaces for rendering parameterized reports, so most of this post applies directly to RMarkdown. But since Quarto is more general and newer, I’m going to focus on Quarto for this post.

One more side note—these examples only apply when using knitr as the rendering engine (you can also use parameters with Jupyter, which uses a different syntax).

Parameter basics

To render a parameterized output in Quarto, you have to follow two steps:

  1. Add parameters to your .qmd file
  2. Pass the parameter values while rendering.

Adding parameters in .qmd files

In the YAML, you can define any parameters you want using params. For example, if I wanted to make a report with the parameter name that you will replace with a person’s name when rendering, you would add this to the YAML:

---
params:
  name: "John"
---

The value "John" is the default value for the parameter, which will be used if no parameter is passed. This parameter can now be used anywhere in the .qmd file using params$name, which will be replaced with whatever the parameter value is. Note that in a code chunk you can just use params$name directly, but if you want to use it in-line (e.g. in a sentence) you have use an in-line R command, like so:

`r params$name`

You can include as many parameters as you want, just add them to params. For example, here is how you would add a parameter for name and grade:

---
params:
  name: "John"
  grade: "100%"
---

One nice feature about using parameters is that you an preview the output with the default values, that way you can make sure everything looks the way you want before creating different versions of the document.

I tend to save these files as something like “template.qmd”, since it is a template that I will use to render to multiple different versions.

Passing parameters while rendering

Once you have a “template.qmd” file ready with parameters in place, you can pass new parameters to it while rendering. If you prefer to work in the terminal, you can pass parameters in the quarto render command, e.g.:

quarto render template.qmd -P name:'Paul' -P grade:'98%'

If you’re more comfortable working in R than the terminal (like me), you can use the {quarto} R package to render the .qmd file. The main function is quarto::quarto_render(), which takes an input argument for the path to the “template.qmd” file. To pass parameters, you use the execute_params argument, which must be a list of parameters. For example, to render the same output as in the terminal example above, you would use:

quarto::quarto_render(
    input = "template.qmd",
    execute_params = list(
        name = "Paul",
        grade = "98%"
    )
)

Iterative rendering

I tend to have more than one set of parameters I need to pass to my “template.qmd” file (e.g. I need a report for every student in my class). In these cases, I use the quarto::quarto_render() command inside a loop.

For example, imagine that I had a “grades.csv” file with the columns name and grade for each student in my class. I could read in that data file and then iteratively render the “template.qmd” file for each student. Here I have to be careful to make sure I also provide an output_file argument so that each report has a unique name. My code would look something like this:

df <- readr::read_csv("grades.csv")

for (i in 1:nrow(df)) {
    student <- df[i, ] # Each row is a unique student
    quarto::quarto_render(
        input = "template.qmd",
        output_file = paste0("feedback-", student$name, ".pdf"),
        execute_params = list(
            name = student$name,
            grade = student$grade
        )
    )
}

If I ran this code, I would end up with a lot of PDF files in my directory, each with the name “feedback-{name}.pdf”, where “{name}” is replaced with each student’s name (e.g. “feedback-John.pdf”).

Aside for the {purrr} people: Yes I know there are other ways to iterate, but for this specific purpose I prefer loops as I find it easier for passing parameters (especially if there are multiple parameters).

Examples

For a recent GW Coders meetup (which you can watch here), I demonstrated how to use parameterized Quarto files with two simple examples: Grades and Wedding Cards. The code for those demos are available at https://github.com/jhelvy/quarto-pdf-demo.

The grades example is similar to the example I have used thus far in this post for creating unique reports for several students. The Wedding Cards example demonstrates how I could use two different templates and render the appropriate one depending on a condition (in this case these are “thank you” cards that contain a different message depending on whether the gift was money or not).

In each example, I have a “template.qmd” file that defines the content of the parameterized output PDF, and a “make_pdfs.R” file that contains the R code to iteratively render each PDF. I encourage you to download the files and play with them yourself to see how each example works. They are by no means the only (or even best) way to do this, but they provide a working starting point to build upon.

Some challenges

In the demo repo, I have included a third example called “data-frames” that demonstrates some fixes for two challenges I have run into when rendering parameterized reports in Quarto. Those are:

  1. Passing a data frame object as a parameter.
  2. Rendering the output to a different directory.

It is worth mentioning that neither of these are issues when using RMarkdown. They may be addressed more elegantly in the future, but for now here are my workaround solutions.

Passing data frames as parameters

Since Quarto is a separate program from R, it doesn’t know what a data frame is, so if you pass a data frame object as a parameter in execute_params, it will convert it to a list. This issue was posted in the Posit Community forum here.

After posting about the issue in the Fediverse, both Mickaël Canouil and Garrick Aden-Buie suggested using the {jsonlite} package to serialize the data frame to pass it as a parameter and then un-serialize it back to a data frame inside the .qmd file. Turns out this worked perfectly!

The specific functions I use to handle the job are jsonlite::toJSON() and jsonlite::fromJSON(). In the quarto::quarto_render() command, I have to serialize the data frame inside the parameter list like so:

quarto::quarto_render(
    input = "template.qmd",
    execute_params = list(
        df = jsonlite::toJSON(df), # Serialize the data frame
        month = month
    )
)

Then inside my “template.qmd” file I un-serialize it back to a data frame inside a code chunk with the following line:

df <- jsonlite::fromJSON(params$df)

From there on I can use the df object anywhere in my “template.qmd” file as a data frame. The reason this isn’t an issue when using RMarkdown is that RMarkdown runs inside R, so it “knows” what a data frame is throughout the whole process.

In the “data-frames” example, I create monthly summary tables of flight departure and arrival delays by airline using the {nycflights} package.

In this specific example, an easier approach would be to simply pass the “month” as a parameter to the “template.qmd” file and then compute the summary table there (this is in fact my recommended approach if possible). But that requires that the data be accessible from outside the “template.qmd” file (e.g. saved to disc), and that the summary calculations be relatively fast. If, for example, reading in and summarizing the data is computationally expensive, then it may be easier to do what I have done in this example, which is first read in and summarize all the data, then pass along the summary data frame to the “template.qmd” file as a serialized data frame.

Rendering to a different directory

Unfortunately, at least at the moment it appears that quarto::quarto_render() is not capable of rendering an output file to any location other than the current directory. I noted this in the quarto-cli discussion forums here. The best solution for now seems to be to simply render the output and then copy it over to a desired output directory.

In practice, this is a bit cumbersome as there are a number of different conditions to consider that make the copy-pasting not so simple, so my solution was to write my own custom function that works as a wrapper around quarto::quarto_render() and allows the user to provide an optional output_dir for where the output file will be moved post-rendering.

I have put this function inside my person R package {jph}, which you can install if you wish to use it yourself. I named the function quarto_render_move(), which renders and then optionally moves the file to a desired location. The function source is available here.

In practice, it works as a drop-in replacement for quarto::quarto_render(). Here is an example:

jph::quarto_render_move(
    input = "template.qmd",
    output_file = "feedback-student.pdf",
    output_dir = "output_folder",
    execute_params = list(
        name = "Paul",
        grade = "98%"
    )
)

Using this code, the output file would be placed inside a folder called “output_folder”.

Wrap up

Quarto is still quite new, and the user base is still growing. Without a doubt, I expect that most current Quarto users are coming from RMarkdown, which has for years just seemed like total wizardry with how seamlessly it works.

Coming from RMarkdown myself, Quarto has a lot of very nice features that definitely build on the best of what RMarkdown has had to offer. But it’s not perfect, and the fact that it is totally separate from R (i.e. it’s not an R package) has meant giving up some of the conveniences I have enjoyed, like passing data frames around with wreckless abandon. Hopefully the tricks posted here will work for you too if you try to use them. However your Quarto journey goes, let me know with a comment!

Cheers, JP

Back to top