Building Quarto Dashboards
A beginner’s guide, step by step, with the Ethiopian Malaria Survey
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
- What is Quarto / R Markdown?
- The Quarto dashboard
- Pages, rows & columns
- Cards
- Value boxes
- Tabsets & sidebars
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:
- Run your analysis in R, copy a number.
- Paste “19% positive” into Word.
- 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.
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.
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)
```- YAML header - settings between the
---lines (title, output format). - Markdown - your ordinary writing, with
#for headings,**bold**, lists. - 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 |
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?
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 .htmlRun 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 |
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)Rule of thumb: rows for bands, columns for side-by-side. You can nest columns inside rows inside pages as deep as you need.
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 (%)")
```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: dashboard→scrolling: truein 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 cardsFor 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
```Change the data file, re-render, and every value box updates by itself. That is reproducibility.
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: dashboardwiththeme(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).
malaria_dashboard.qmd- the full working dashboard. Render withquarto render malaria_dashboard.qmd.malaria_dashboard_preview.html- open it now to see the target before you build.
Recap: the path you walked
- Quarto / R Markdown - one file holds text + code + results.
format: dashboard- turns that file into a dashboard.- Pages / rows / columns -
#,##,###build the grid. - Cards - code chunks (and
.carddivs) become boxes. - Value boxes -
#| content: valueboxshows live KPIs. - Tabsets & sidebars - more views and context in the same space.
One
.qmd, onequarto render, a dashboard your whole program can read.
Practice: build it yourself
Try these in order, rendering after each step:
- New file,
format: dashboard, a title. - Add the setup chunk that loads the malaria data.
- Add a
## Rowof three value boxes (tested, positive, bednet). - Add a
## Rowwith two columns: a region chart and a table. - Add a second page
# Prevention & coverage. - Add a
theme:line and re-render.
Stuck on a step? Open malaria_dashboard.qmd and copy the matching block.