Parametrized Nice Reports with Quarto and PDF

UseR 2024

Thomas Vroylandt, Kantiles

whoami

  • Founded a consultancy business

  • Also work with R for the Rest of Us

  • Creator of the pagedreport package

  • Produced 1000+ parametrized reports

Contact me: thomas@kantiles.com

Parametrized Reporting ?

From one report many

PSC Housing

Oregon Voices

IA2030

Child Welfare Program

Why not a dashboard ?

Dashboard

  • Pull approach

  • Bring global & detailed understanding

  • Interactive viz and tables

  • For analysts & data practitioners

  • https://

Reports

  • Push approach

  • Bring local comprehension & awareness

  • Static tailored viz

  • For people on the field & partners

  • Portable Document Format (PDF)

Why not something else ?

  • Infographics
  • Graphs
  • Analysis note
  • Excel files
  • Data API

Why not something else ?

Ask:

  • for what ?
  • for who ?
  • frequency
  • ability to build yourself
  • maintenance

Parameters

Choose your parameters

Areas

Sectors

Units

Combine them but check you have enough data

Put them on the header

---
title: "IA2030 Country Profile"
subtitle: "`r params$country`: 2023"

# params to change here
params:
  country: "Somalia"
---

And re-use them with params$XXX :

  • in code
  • in inline text
  • in your style

Pass the salt

  • CSS
<style>
:root{
  --country-name: "`r params$country`";
}
</style>
  • Typst (can be nested)
---
country_name: '`r params$country`'
---

Tools

Pick up a tool

Text

Computations

PDF engines

Comparison

Typst

  • Quarto format

  • Lightning quick

  • Custom langage for layout

  • Great for simple templates

weasyprint

  • Pandoc engine

  • Quick

  • No preview

  • Best tool for most of the cases

  • French

pagedjs-cli

  • Pandoc engine

  • No direct preview

  • Most complete tool

  • JS based

Design advices

  • Make your parameters explicit

  • Think about the extreme cases like long names in margins

Design advices

  • Make your sections visuals with named pages & colors

Design advices

  • Columns are great but difficult to handle

Create a template with Typst

Set up the format + use partials:

format: 
  typst:
    template-partials: 
      - typst-template.typ
      - typst-show.typ
    font-paths: fonts/
  • typst-show.typ transfer the parameters
  • typst-template.typ is the template

font-paths allow you to use custom fonts by putting them in a folder

Create a template with Typst

typst-show.typ

#show: my-report.with(
  // main params
  $if(title)$
    title: "$title$",
  $endif$
  $if(subtitle)$
    subtitle: "$subtitle$",
  $endif$

  // add params
  $if(country-name)$
    country_name: "$country-name$",
  $endif$
  )

Create a template with Typst

typst-template.typ: all is valid until replaced

// format with custom parameters
#let my-report(
  title: "Title",
  subtitle: none,
  country_name: none
  body,
) = {
  // text default
  set text(
    font: "Calibri",
    size: 11pt,
  )

  // --- Configure first page ---
  set page(
    "us-letter",
    background: place(right, rect(fill: rgb(#123456), width: 30%, height: 100%)),
    margin: (left: 1in, right: 1in, top: 0.7in, bottom: 1in),
  )

  // --- title grid ---
  v(5%)
  // logo
  image("logo.svg", height: 10%)
  v(10%)
  pad(text(16pt, weight: "regular", country_name), right: 30%)
  v(5%)

  pagebreak()

Create a template with Typst

  // --- Configure the rest of the pages ---
  set page(
    "us-letter",
    background: place(top, rect(fill: rgb(#123456), width: 100%, height: 0.5in)),
    footer: {
      line(length: 100%)
    },
    margin: (left: 1in, right: 1in, top: 0.7in, bottom: 1in),
  )

  // --- headings ---
  // level 1 is ## if there is no #
  // level 2 is ###
  // level 3 is ####
  show heading.where(level: 1): it => [
    #set text(17pt, weight: "bold")
    #block(it.body)
  ]

  show heading.where(level: 2): it => [
    #set text(fill: rgb(maincolor), size: 15pt, weight: "regular")
    #block(it.body)
  ]

  show heading.where(level: 3): it => [
    #set text(fill: rgb(maincolor), size: 12pt, weight: "regular")
    #block(it.body)
  ]

  // --- body ---
  body
}

Create a template with CSS tools

  • All the others tools are based on CSS Paged Media

  • Differences but more or less compatible between them

  • I did a full talk about how to template things (in French) for pagedown. See the video

  • Print to PDF:

    • output to HTML + use bash command weasyprint (or pagedjs-cli)
    • output to PDF + use pdf-engine: weasyprint

Quick how-to

  • Add your parameters as CSS vars: fonts, colors, margins, images
:root {
  /* colors */
  --report-navy: #1f4675;
  --report-light-blue: #59b8cd;
  --report-red: #eb5a53;

  /* fonts */
  --main-font: "Calibri";
  --header-font: "Montserrat";

  /* margin units */
  --unit: 0.7in;
  --unit_neg: -0.45in;
}

Quick how-to

  • Create your global style with them :
body {
  font-family: var(--main-font);
}
h1, h2, h3 {
  color: var(--report-red);
}
p {
  font-weight: 400;
  font-size: 12pt;
  color: var(--report-navy);
  padding-left: 0.55in;
}

Vars that have been created in the Quarto file can also be used

Quick how-to

  • Define page size and margins
@page {
  /* page size */
  size: Letter;

  /* margins */
  margin-top: 1in;
  margin-left: 0.3in;
  margin-right: 1in;
  margin-bottom: 1in;

}

Quick how-to

  • Add content in margins
@page {
  /* country name */
  @top-left {
    content: var(--country-name);
  }
   /* logo */
  @top-right {
    content: "";
    width: 3.3in;
    background: url(img/logo_white.svg);
    background-repeat: no-repeat;
    background-size: 3in;
  }
}

Use content: ""; to display the background color or image.

Quick how-to

  • Add content in margins
@page {
  /* numbering */
  @bottom-right {
    content: counter(page);
    width: 7.1in;
    font-weight: bold;
  }
}

To learn more about content in margins

Quick how-to

  • Build title page
@page:first{
  background-image: var(--cover-url);
  background-repeat: no-repeat, no-repeat;
  background-size: 5in, cover;
  background-position: 10% 10%, 50% 50%;
  @bottom-left {
    display: none;
  }
}
  • Don’t forget .title and others

display: none; helps to remove the global content in margins

Quick how-to

  • Define named pages

    • with fenced div :::named1 + :::

    • or classic HTML <div class="named1"> + </div>

    • CSS side :

.named1 {
  page: .namedLayout;
}

@page .namedLayout {
  @top-left {
    background-color: var(--report-navy);
    color: white;
  }
}

Quick how-to

  • And the content inside too
.named1 > h3 {
  color: var(--report-navy);
}

Quick how-to

  • Define utility functions :

    • columns (use CSS Grid first !)
    • img -> remove defaults margins

You can arrange some positions with negative margins and positive paddings. Play with them !

Since page size is fixed, think to use position: absolute; if needed

Presenting quarto.report

  • Quarto extension
  • based on weasyprint
  • Aim at facilitating templating
  • Can be use for parametrized reporting
  • Both R & Python
quarto add kantiles/quarto.report

Presenting quarto.report

Change parameters in the YAML (or _quarto.yml file)

style:
  font:
    header: "Outfit"
    main: "Montserrat"
    mono: "Fira Code"
    size: 12pt
  color:
    font: "#404040"
    font-accent: "#fdfdfd"
    accent: "#123456"
    third: "#987654"
  pagesize:
    width: 210mm
    height: 297mm
  logo: url(logo_url)
  main-img: url(img_url)

quarto.report - typewriter

format:
    quarto.report-pdf+typewriter
  • main image on title page
  • table of contents by default

quarto.report - chalk

format:
    quarto.report-pdf+chalk
  • two colors template
  • table of contents by default

quarto.report - corner

format:
    quarto.report-pdf+corner
  • well suited for parametrised reporting
  • one additional named page by default

Data

Prepare your data

  • Avoid as much as possible computations within the report

  • Pay attention to formats - use scales

  • Make sure your data is unique by parameter

Check missing data

Compute explicit missing data with tidyr::complete

Graphs

Pay attention to limits

  • Your graphs should work in all cases
  • The more granular you go, the more extreme your values will be
  • You should plot your data first

If you got this warning from ggplot2, you probably messed up something with limits

Warning message:
Removed 7 rows containing missing values or values outside the scale range
(`geom_point()`).

Pay attention to limits

Limits can be set dynamically

ggplot(...) +
  ... +
  scale_x_continuous(
    breaks = seq(2021, 2023, 1),
    limits = c(min(df$year) - .1, max(df$year) + .1),
    position = "top",
    labels = c("Baseline", "2022", "2023")
  ) 

Handle the size

  • Output as svg or svglite (with fig-format)
  • Setup fig-width and fig-height for all plots
  • Size can be set dynamically (in inches)
if(params$country == "Somalia") {
  maps_width <- 5
  maps_height <- 4
} else{
  ...
}

Then used in chunks with !expr

```{r}
#| fig-width: !expr maps_width + .5
#| fig-height: !expr maps_height
```

Set up a reference

To the global level

To others

In time

You can pick them all

Set up a reference

Set up a reference

Set up a reference

Declutter & highlight

David talked about all of this at Cascadia R conf

  • use facets
  • don’t label everything
  • hide small values
  • think about labels position
  • highlight what’s important

Text

Stay dynamic

  • Be informative about comparisons :

    • growth
    • difference
  • Compute before display
```{epoxy, .data=txt_data_1_3}
In {year}, measles elimination was <span class = "txt-navy">achieved
</span>in {nb_achieved} countries in the WHO region, while it was
<span class = "txt-yellow">re-established</span> in
{nb_reestablished} countries and <span class = "txt-red">
not achieved</span> in {nb_not_achieved} countries.
```

Highlight

Use :

  • size
  • color
  • weight

<span>...</span>

Generate

Render them all

  • list of parameters
  • map
  • quarto_render

Render them all

map(
  country_list,
  \(x)quarto_render(
    input = "path.qmd",
    output = glue::glue("report_{x}"),
    params = list(country = country)
  ),
  .progress = TRUE
)

Use fs::file_move to move your reports to the right folder (see )

Quality insurance

  • Check the number of pages !
walk(list.files("generated_reports/", full.names = TRUE),
     function(x) {
       pdf_length <- pdftools::pdf_length(x)
       if (pdf_length != 6) {
         print(paste0(x, " reports ERROR"))
       } else{
         print("OK")
       }
     })
  • Check reports with extreme values :

    • long names
    • outliers in data
    • missing data

Thanks !