From One Notebook to Many Reports: Automating with Quarto

 

Charlotte Wickham cvwickham

Posit, PBC

cwickham.github.io/one-notebook-many-reports

The Best and Worst Places to Grow Up: How Your Area Compares

Washington State Soil Health Initiative: State of the Soils Assessment

Built with Quarto logo
by Jadey Ryan

From One Notebook to Many Reports with Quarto

The `corvallis.ipynb` notebook open in JupyterNotebook. Notebook contains the heading Corvallis, code cells, markdown output and a plot.

Arrow pointing to the right

The `corvallis.pdf` document rendered by Quarto. Document contains the heading Corvallis, markdown output and a plot.

The `portland.pdf` document rendered by Quarto. Document contains the heading Portland, markdown output and a plot.

The `eugene.pdf` document rendered by Quarto. Document contains the heading Eugene, markdown output and a plot.

What is Quarto?

Quarto is an open-source, command line tool, built by Posit, to…

Turn notebooks into HTML documents

$ quarto render corvallis.ipynb

corvallis.html

The `corvallis.ipynb` notebook rendered by Quarto. Document contains the heading Corvallis, markdown output and a plot.

Turn notebooks into Word documents

$ quarto render corvallis.ipynb --to docx

corvallis.docx

{.border .tight fig-alt=“The corvallis.ipynb notebook rendered by Quarto to docx.}

Turn notebooks into PDF documents

$ quarto render corvallis.ipynb --to typst

corvallis.pdf

The `corvallis.ipynb` notebook rendered by Quarto to `docx`.

Set document options in a header

Raw cell at the top of the notebook:

---
# Options for rendering the document
---

Set document options in a header

Raw cell at the top of the notebook:

---
format: typst
---


quarto render corvallis.ipynb

corvallis.pdf

The `corvallis.ipynb` notebook rendered by Quarto to `docx`.

Set document options in a header

Raw cell at the top of the notebook:

---
format: typst
echo: false # Hide code
---


quarto render corvallis.ipynb

The `corvallis.ipynb` notebook rendered with `echo: false`. There are no code cells shown in the rendered document.

Set document options in a header

Raw cell at the top of the notebook:

---
format: typst
echo: false # Hide code
execute: 
  enabled: true # Run code cells
---


quarto render corvallis.ipynb

The `corvallis.ipynb` notebook rendered with `echo: false`. There are no code cells shown in the rendered document.

Set code cell options as #| comments

E.g. #| include: false to hide the code and output:

#| include: false
tmean.head()

The `corvallis.ipynb` notebook with a code cell that has `#| include: false` in the first line.

The `corvallis.ipynb` notebook rendered with `#| include: false`. There is no output from the cell that produced the `head` of the data.

Quarto is …

A way to avoid copy-and-pasting into a Word document

And more…websites

Screenshot of the nbdev website, which is a Quarto website and builds Quarto doc sites.

And more…books

Screenshot of the book Python for Data Analysis, which is a Quarto book.

And more…presentations

Screenshot of this presentation, which is a Quarto presentation.

And more…publishing

quarto publish slides.qmd github-pages

Parameterized reports in Quarto

Start with a notebook that works for one value:

  1. Turn hardcoded value into a variable

  2. Make the variable a parameter

  3. Render with a different parameter value

  4. Automate rendering for all parameter values

1. Turn hardcoded value into a variable

corvallis.ipynb

city = "Corvallis"


# Corvallis


tmean = tmean_oregon.filter(
    pl.col("city") == "Corvallis",
)


...
+ labs(title = "Corvallis, OR", ...)
...

climate.ipynb

city = "Corvallis"


Markdown(f"# {city}")


tmean = tmean_oregon.filter(
    pl.col("city") == city,
)


...
+ labs(title = f"{city}, OR", ...)
...

2. Make the variable a parameter

Add the tag parameters to the code cell:

The `corvallis.ipynb` notebook with the code cell that contains the `city` variable highlighted. The tag `parameters` is added to the cell.

3. Render with a different parameter value

quarto render climate.ipynb

climate.pdf

3. Render with a different parameter value

quarto render climate.ipynb -P city:Portland

climate.pdf

3. Render with a different parameter value

quarto render climate.ipynb -P city:Portland --output-file portland.pdf

portland.pdf

4. Automate rendering for all cities

cities:

city output_file
Portland portland.pdf
Cottage Grove cottage_grove.pdf
St. Helens st_helens.pdf

gen-reports.py:

from quarto import render

for row in cities.iter_rows(named=True):
    # render() runs `quarto render` 
    # via subprocess.run()
    render(
        "climate.ipynb",
        execute_params={"city": row["city"]},
        output_file=row["output_file"],
    )

From One Notebook to Many Reports with Quarto

The `corvallis.ipynb` notebook open in JupyterNotebook. Notebook contains the heading Corvallis, code cells, markdown output and a plot.

Arrow pointing to the right

The `corvallis.pdf` document rendered by Quarto. Document contains the heading Corvallis, markdown output and a plot.

The `portland.pdf` document rendered by Quarto. Document contains the heading Portland, markdown output and a plot.

The `eugene.pdf` document rendered by Quarto. Document contains the heading Eugene, markdown output and a plot.

Making Pretty Reports

Add brand with Brand.yml

_brand.yml
color:
  palette:
    white: "#FFFFFF"
    forest-green: "#2d5a3d"     
    charcoal-grey: "#555555"    
    orange: "#ff6b35"
  foreground: charcoal-grey    
  background: white            
  primary: forest-green       
  secondary: orange      
typography:
  fonts:
    - family: Montserrat
      source: google
    - family: Open Sans
      source: google
  base:
    family: Open Sans
    weight: 400
  headings:
    family: Montserrat
    weight: 600
    color: forest-green 
logo:
  medium: logo.png

Quarto will detect _brand.yml

More control over logo with options:

---
format: 
  typst:
    logo:
      width: 1in
      location: right-top
---

Use brand-yml package to set brand elements in your code:

from brand_yml import Brand
brand = Brand.from_yaml("")
highlight_color = brand.color.secondary

Brand + Typst = Pretty PDFs

Typst Docs: https://typst.app/docs/

Example: cwickham/one-notebook-many-reports/05-pretty-reports

Tips: Quarto + Typst

The `corvallis.ipynb` notebook rendered by Quarto to `pdf`. The document has dark green header with the city in white text and a map next to it with the location as an orange dot.

PDF Accessibility

Legislation mandating accessible PDFs:

Neither format: typst nor format: pdf currently produce tagged PDFs 😔

Possible solutions:

  • Use format: docx then use Word to export to PDF

  • Don’t use PDF. Use format: html. Quarto ≥v1.8 websites pass axe-core checks by default.

Quarto for parameterized reports

Advantages

  • Manage one notebook

  • Render to one or many formats

  • “You do you” automation

… so much more

Thank you

Big thanks to Jadey Ryan for the inspiring example and talk .

.qmd

Consider using the Quarto document format *.qmd

quarto convert corvallis.ipynb 
  • Plain text, easier version control, copy-paste examples

  • No output from cells in file, forced reproducibility

  • Header and markdown unadorned

  • Code cells inside {python} code blocks:

    ```{python}
    
    ```

corvallis.qmd

---
format: typst
echo: false
title: Corvallis
jupyter: python3
---

```{python}
import polars as pl
from plotnine import *
from datetime import date
from calendar import month_name, month_abbr
from IPython.display import Markdown
```

```{python}
this_month = date(2025, 5, 1)
highlight_color = "#FF5733" 
```

```{python}
tmean_oregon = pl.read_csv("data/tmean-oregon.csv", schema_overrides={"date": pl.Date})
tmean = tmean_oregon.filter(
    pl.col("city") == "Corvallis",
)
```

```{python}
#| include: false
tmean.head()
```

```{python}
this = tmean.filter(pl.col("date") == this_month).row(0, named=True)
Markdown(f"{month_name[this['month']]} {this['year']} was {abs(this['tmean_diff']):.1f}°C {this['tmean_direction']} than usual.")
```

```{python}
(
    ggplot(tmean, aes(x="month", y="tmean"))
    + geom_line(aes(group="year"), alpha=0.2)
    + geom_line(aes(y = "tmean_normal"))
    + geom_line(data=tmean.filter(pl.col("year") == 2025), color=highlight_color)
    + geom_point(
        data=tmean.filter(pl.col("date") == this_month), color=highlight_color
    )
    + scale_x_continuous(breaks=list(range(1, 13)), labels=list(month_abbr[1:]))
    + labs(title = "Corvallis, OR", x="", y="Mean Temperature (°C)")
    + theme_bw() 
    + theme(figure_size = (8, 4))
)
```

Typst + Quarto

Typst + Quarto: keep the intermediate .typ file

In document header of climate.ipynb:

---
format: typst
keep-typ: true
---

quarto render climate.ipynb

Output:

  • climate.pdf: the final PDF result
  • climate.typ: the intermediate Typst file. Useful for examining and debugging.

Typst + Quarto: include raw Typst code

Write Typst code in a raw cell with ```{=typst} syntax

```{=typst}
#set figure(numbering: none)
#show figure.caption: set align(left)
```

Typst docs: https://typst.app/docs/

This raw syntax works to inject code into other formats too e.g. =html and =latex.

Typst + Quarto: wrap elements in Typst functions

Add the typst-function Quarto extension.

climate.ipynb:

---
filters: [typst-function]
functions: [place]
---


#| label: map
oregon_map("Corvallis")


::: {.place arguments='bottom + left, dy:-0.25in'}

{{< contents map >}}

:::

climate.typ:

#place(bottom + left, dy:-0.25in)[
  // `oregon_map("Corvallis")`
]