Building Quarto Dashboards
A beginner’s guide, step by step, with the Ethiopian Malaria Survey

Author
Affiliation

Yebelay Berehan

Center for Evaluation and Development (C4ED)



r Sys.Date()

Where we are going

We will build a real malaria dashboard one idea at a time. No prior Quarto experience needed.

5,000Households

19.0%Positivity

54.3%Bednet ownership

32.5%Any anemia

Our six modules

  1. What is Quarto / R Markdown?
  2. The Quarto dashboard
  3. Pages, rows & columns
  4. Cards
  5. Value boxes
  6. Tabsets & sidebars
Tip

You don’t type these numbers by hand: the dashboard reads them from data/malaria_survey_ethiopia.csv and recalculates every time the data changes.


01 · What is Quarto / R Markdown?

The old way vs the dynamic way

Imagine writing a malaria report the manual way:

  1. Run your analysis in R, copy a number.
  2. Paste “19% positive” into Word.
  3. Next month new data arrives… and every number, table, and figure must be copied again, by hand.

A dynamic document fixes this: your text, your code, and your results live in one file. Press render, and the numbers update themselves.

Note

This is the single idea behind R Markdown and Quarto: write once, the computer fills in the results.


From R Markdown to Quarto

  • R Markdown (file ending .Rmd) was the original tool for dynamic documents in R. It is excellent and still widely used.

  • Quarto (file ending .qmd) is its next-generation successor. Same idea, but:

    • works with R, Python, and Julia,
    • produces many outputs from one file (web page, PDF, Word, slides, books, dashboards),
    • needs no extra R packages to install: Quarto is its own program.
Tip

If you know R Markdown, you already know 90% of Quarto. If you are brand new, even better: start straight with Quarto.


Anatomy of a .qmd file

Every Quarto file has just three kinds of content:

---
title: "My malaria report"      # 1. YAML header (settings)
format: html
---

## Introduction                 # 2. Markdown text (words)

Malaria positivity was high in lowland regions.

```{r}                          # 3. Code chunk (R that runs)
mean(malaria$malaria_positive == "Yes", na.rm = TRUE)
```
  1. YAML header - settings between the --- lines (title, output format).
  2. Markdown - your ordinary writing, with # for headings, **bold**, lists.
  3. Code chunks - R code between ```{r} fences; its output is inserted automatically.

One source, many outputs

The magic of the YAML format: line: change one word, get a different document from the same .qmd.

format: You get
html A web report
pdf A PDF (via LaTeX)
docx A Word document
revealjs Slides (like these)
dashboard An interactive dashboard ← our goal
Note

How it renders: Quarto runs your code with knitr, then hands the text + results to pandoc, which builds the final file. You just press Render.


02 · The Quarto dashboard

What is a dashboard?

A dashboard is a web page built for at-a-glance monitoring: a few headline numbers, some charts, maybe a table, arranged in a tidy grid.

Think of the one-page summary a malaria program manager wants on a Monday morning: How many tested? What’s the positivity? Which regions are worst? Are nets reaching people?

Tip

A dashboard is not new R. It is a layout for analysis you can already do, the same ggplot and tables, just arranged as cards.


The minimal dashboard

Start a new file malaria_dashboard.qmd. The only thing that makes it a dashboard is one line in the YAML:

---
title: "Ethiopia Malaria Indicator Survey"
format: dashboard
---

Add a chunk that draws something, and it becomes your first card:

```{r}
library(tidyverse)
malaria <- read_csv("data/malaria_survey_ethiopia.csv")
ggplot(malaria, aes(hemoglobin)) + geom_histogram()
```

How to render it

Two commands in the terminal (or the Render button in RStudio / VS Code):

quarto preview malaria_dashboard.qmd   # live: updates as you edit
quarto render  malaria_dashboard.qmd   # one-off: writes the .html
Warning

Run from the right folder. Open a terminal inside the folder that holds your .qmd, or give the full path. “File does not exist” almost always means you are in the wrong directory.


The structure, in one picture

A dashboard is a simple nesting of four things:

# Page          →  a tab in the top navigation bar
## Row          →  a horizontal band on that page
### Column      →  a vertical split inside the row
    code chunk  →  a CARD (holds a plot, table, or value box)

We will meet each one in the next modules, in this exact order: pages → rows/columns → cards → value boxes → tabs/sidebars.


03 · Pages, rows & columns

Headings are the layout

In a dashboard, the number of # signs decides the layout. You do not write HTML; you write headings.

Heading Creates Example
# A page (top-nav tab) # Overview
## A row ## Row
### A column ### Column
Note

Start simple: one page, one or two rows. Add pages later when the dashboard grows.


Rows stack top to bottom

By default each ## Row is a band across the page. Control its share of the height with {height=...}.

## Row {height=25%}

(value boxes go here - a thin band on top)

## Row {height=75%}

(charts go here - the big band below)

Heights are relative: 25% and 75% simply split the space.


Columns sit side by side

Inside a row, each ### Column splits the space left-to-right, sized with {width=...}.

## Row {height=75%}

### Column {width=60%}
(big chart: positivity by region)

### Column {width=40%}
(smaller chart: bednet effect)
Tip

Rule of thumb: rows for bands, columns for side-by-side. You can nest columns inside rows inside pages as deep as you need.


Many pages = top navigation

A # heading starts a brand-new page, shown as a tab in the navbar. Use pages to separate audiences.

# Overview
## Row { ...value boxes... }
## Row { ...charts... }

# Regional detail
## Row { ...tables by region... }

# Prevention & coverage
## Row { ...bednet & IRS charts... }

Our finished malaria_dashboard.qmd has four pages built exactly this way.


04 · Cards

What is a card?

A card is one box in the grid, the basic unit of a dashboard. The simplest way to make a card is to write a code chunk: whatever it produces (a plot, a table) fills the card.

```{r}
ggplot(malaria, aes(hemoglobin)) +
  geom_histogram(binwidth = 0.5, fill = "#047B77")
```

That one chunk is already a card. No extra syntax needed.


Give every card a title

Add #| title: at the top of the chunk. The title becomes the grey header bar on the card.

```{r}
#| title: "Malaria test positivity by region"
malaria |>
  filter(malaria_tested == "Yes") |>
  group_by(region) |>
  summarise(positivity = mean(malaria_positive == "Yes") * 100) |>
  ggplot(aes(reorder(region, positivity), positivity)) +
  geom_col(fill = "#047B77") + coord_flip() +
  labs(x = NULL, y = "Positivity (%)")
```
Note

Lines that begin with #| are chunk options: instructions to Quarto, not R code. title, echo, eval are all set this way.


Cards from text, and manual cards

A card does not have to hold code. Wrap any content in a .card div to make a manual card, for notes, definitions, or instructions.

::: {.card title="How to read this dashboard"}
Positivity = positive RDTs ÷ people tested, shown per region.
Lowland regions (Gambella) carry the highest burden.
:::

Tables are cards too, knitr::kable() or DT::datatable() inside a chunk.


When a card is too tall: scrolling

Big tables or long text can overflow a card. Two fixes:

  • Make the whole dashboard scroll: format: dashboardscrolling: true in the YAML.
  • Make one card/row scroll by adding the class {.card-scrollable} or setting an explicit {height=...}.
format:
  dashboard:
    scrolling: true     # the page scrolls instead of squashing cards
Tip

For a searchable, paged table that never overflows, use DT::datatable(df, options = list(pageLength = 10)).


05 · Value boxes

What is a value box?

A value box is a special card that shows one big number with a label and an icon, perfect for KPIs (Key Performance Indicators) like positivity or bednet coverage.

18.7%Tested

19.0%Positive

54.3%Bednet own.

These are the first things a manager reads, so we put them in a thin row at the top.


The value-box syntax

A value box is a chunk with the option #| content: valuebox. It returns a small list().

```{r}
#| content: valuebox
#| title: "Positive (of tested)"
list(
  icon  = "thermometer-high",   # a Bootstrap icon name
  color = "danger",             # red, for a warning metric
  value = "19.0%"
)
```

Three things to set: title (the label), icon, and value (the number).


Icons and colors

  • Icons come from Bootstrap Icons; use the name, e.g. "house-door", "shield-check", "droplet-half".
  • Colors are theme names: primary, secondary, success, info, warning, danger (or any hex like "#047B77").
Metric Sensible color
Positivity (bad if high) danger (red)
Bednet ownership (good if high) success (green)
Tested / neutral counts primary / info

Dynamic values - the whole point

Never hard-code “19.0%”. Compute it once in a setup chunk, then drop the object into the value box.

```{r}
#| label: setup
#| echo: false
library(tidyverse)
malaria <- read_csv("data/malaria_survey_ethiopia.csv")
tested  <- filter(malaria, malaria_tested == "Yes")
pct_p   <- round(mean(tested$malaria_positive == "Yes") * 100, 1)
```
```{r}
#| content: valuebox
#| title: "Positive (of tested)"
list(icon = "thermometer-high", color = "danger",
     value = paste0(pct_p, "%"))   # <- the live number
```
Tip

Change the data file, re-render, and every value box updates by itself. That is reproducibility.


06 · Tabsets & sidebars

Tabsets: many views, one space

Sometimes two charts deserve the same spot. A tabset stacks them behind clickable tabs. Add {.tabset} to a row or column, then use #### for each tab.

## Row {.tabset}

### Positivity by region
(positivity plot chunk)

### Bednet use
(bednet plot chunk)

The reader clicks Positivity by region / Bednet use to switch. Great for “same space, different angle”.


Global vs page-level sidebars

  • A sidebar declared before the first page (right after the setup) is global: it shows on every page.
  • A sidebar declared inside a page (## {.sidebar} under a #) belongs to that page only.
Note

Static dashboards (just R + knitr) use sidebars for context. To make filters actually change the charts, you add Shiny or Observable JS, a next step once you are comfortable with the static version.


Putting it together

Your finished dashboard

The example malaria_dashboard.qmd combines every module:

  • Setup chunk loads the data and computes the numbers (Module 1, 5).
  • format: dashboard with theme (Module 2).
  • Four pages: Overview, Regional detail, Prevention & coverage, Demographics & health (Module 3).
  • Cards with charts and tables (Module 4).
  • A top row of value boxes on each page (Module 5).
TipYour deliverables
  • malaria_dashboard.qmd - the full working dashboard. Render with quarto render malaria_dashboard.qmd.
  • malaria_dashboard_preview.html - open it now to see the target before you build.

Recap: the path you walked

  1. Quarto / R Markdown - one file holds text + code + results.
  2. format: dashboard - turns that file into a dashboard.
  3. Pages / rows / columns - #, ##, ### build the grid.
  4. Cards - code chunks (and .card divs) become boxes.
  5. Value boxes - #| content: valuebox shows live KPIs.
  6. Tabsets & sidebars - more views and context in the same space.

One .qmd, one quarto render, a dashboard your whole program can read.


Practice: build it yourself

Try these in order, rendering after each step:

  1. New file, format: dashboard, a title.
  2. Add the setup chunk that loads the malaria data.
  3. Add a ## Row of three value boxes (tested, positive, bednet).
  4. Add a ## Row with two columns: a region chart and a table.
  5. Add a second page # Prevention & coverage.
  6. Add a theme: line and re-render.
Note

Stuck on a step? Open malaria_dashboard.qmd and copy the matching block.


Thank you