This document is organized in four tiers: Foundations, Intermediate ggplot2, Advanced & Extensions, and Domain-specific / Publication workflows. Each section builds on the last. Work through it top to bottom, or jump to a tier if you’re already comfortable with the basics. Every code chunk is runnable as-is in Quarto (Render button, or Cmd/Ctrl+Shift+K), provided the listed packages are installed and clinic_diabetes_simulated.csv sits in the same folder as this .qmd file.
The dataset
clinic_diabetes_simulated.csv is a fully simulated 900-row dataset modeled loosely on a diabetes-management program across six counties in western Kenya. It is not real patient data — it exists to give this guide variables worth plotting: a date, a few categoricals, several continuous measures, and a binary outcome.
Column
Type
Description
patient_id
character
Unique patient identifier
visit_date
date
Visit date, 2023–2025
region
categorical
County (6 levels)
facility
categorical
Health facility (nested in region)
sex
categorical
Female / Male
age
continuous
Years
bmi
continuous
Body mass index
treatment_group
categorical
4-arm treatment assignment
hba1c
continuous
Glycated hemoglobin (%)
systolic_bp
continuous
Systolic blood pressure (a few missing values, by design)
diastolic_bp
continuous
Diastolic blood pressure
outcome_status
categorical
Controlled / Uncontrolled (HbA1c < 7.0 threshold)
cost_kes
continuous
Simulated visit cost, Kenyan shillings
Code
# Run once if you don't have these installed:# install.packages(c("tidyverse", "scales", "patchwork", "ggrepel",# "ggdist", "gghighlight", "gganimate", "plotly",# "sf", "rnaturalearth", "viridis", "ggthemes"))library(ggplot2)library(dplyr)library(readr)clinic <-read_csv("clinic_diabetes_simulated.csv", show_col_types =FALSE) |>mutate(region =factor(region),treatment_group =factor(treatment_group),outcome_status =factor(outcome_status, levels =c("Uncontrolled", "Controlled")),sex =factor(sex) )glimpse(clinic)
R treats a plot as data + a grammar, not a drawing you click together. That grammar (the “grammar of graphics”, implemented by ggplot2) means you describe what the plot should show, and R figures out how to draw it. This is what makes R plots so reproducible and easy to iterate on.
1.2 Base R plotting (know it, don’t live in it)
Base R plotting is fast for a quick look at data. You’ll see it in other people’s code, so it’s worth recognizing even if you do most of your work in ggplot2.
Code
# Quick scatterplotplot(clinic$bmi, clinic$hba1c,main ="BMI vs HbA1c", xlab ="BMI", ylab ="HbA1c (%)",pch =19, col ="steelblue")
Code
# Quick histogramhist(clinic$age, breaks =15, col ="grey80",main ="Distribution of Patient Age", xlab ="Age")
Code
# Boxplot by groupboxplot(hba1c ~ treatment_group, data = clinic,main ="HbA1c by Treatment Group", xlab ="", ylab ="HbA1c (%)",las =2)
When base R is fine: fast exploratory checks while cleaning data. When to switch to ggplot2: anything you’ll show someone else, iterate on, or reuse.
1.3 The Grammar of Graphics: ggplot2 fundamentals
Every ggplot2 plot has three required pieces:
Data — a data frame
Aesthetic mappings (aes()) — which columns map to which visual properties (x, y, color, size, shape…)
Geometry (geom_*()) — the type of visual mark (points, bars, lines…)
Code
ggplot(data = clinic, aes(x = bmi, y = hba1c)) +geom_point()
Adding more aesthetics
Code
ggplot(clinic, aes(x = bmi, y = hba1c, color = treatment_group, size = age)) +geom_point(alpha =0.6) +labs(title ="HbA1c vs BMI by Treatment",x ="BMI", y ="HbA1c (%)",color ="Treatment", size ="Age")
Make a scatterplot of age vs systolic_bp, colored by sex, with a linear trend line per group.
Show solution
ggplot(clinic, aes(age, systolic_bp, color = sex)) +geom_point(alpha =0.5) +geom_smooth(method ="lm", se =FALSE) +labs(x ="Age", y ="Systolic BP", color ="Sex")
Tier 2 — Intermediate ggplot2
2.1 Facets: small multiples
Facets split one plot into a grid of panels by a categorical variable — often clearer than cramming everything into color/shape.
Code
ggplot(clinic, aes(bmi, hba1c)) +geom_point(alpha =0.4) +facet_wrap(~ region) +labs(title ="HbA1c vs BMI, split by Region")
Code
ggplot(clinic, aes(bmi, hba1c)) +geom_point(alpha =0.4) +facet_grid(sex ~ outcome_status) +labs(title ="facet_grid: two variables at once")
2.2 Scales: controlling how data maps to visuals
Scales control axis breaks, color palettes, and transformations.
Rule of thumb: always check your plot renders sensibly in grayscale/for colorblind viewers. viridis palettes are colorblind-safe by design.
2.4 Themes: polishing the look
Code
p <-ggplot(clinic, aes(bmi, hba1c, color = treatment_group)) +geom_point(size =2, alpha =0.6) +labs(title ="HbA1c vs BMI", subtitle ="By treatment group",x ="BMI", y ="HbA1c (%)", color ="Treatment",caption ="Source: simulated clinic dataset")p +theme_minimal()
Code
# Custom theme tweaks — this is how you build a "house style"p +theme_minimal(base_size =13) +theme(plot.title =element_text(face ="bold"),legend.position ="bottom",panel.grid.minor =element_blank() )
2.5 Coordinate systems
Code
# Flipped bar chart — useful for long category labelsggplot(clinic, aes(x = facility)) +geom_bar(fill ="steelblue") +coord_flip() +labs(title ="coord_flip()", x =NULL, y ="Visits")
Code
# Polar coordinates -> donut (use sparingly; bar charts usually communicate better)df <- clinic |>count(outcome_status)ggplot(df, aes(x =2, y = n, fill = outcome_status)) +geom_col(width =1) +coord_polar(theta ="y") +xlim(0.5, 2.5) +theme_void() +labs(title ="coord_polar(): use sparingly", fill ="Outcome")
Practice exercise (Tier 2)
Facet the bmi vs hba1c scatterplot by treatment_group, apply a viridis color scale mapped to age, and use theme_minimal().
Point estimates without uncertainty are half a story. ggdist makes distributional visualization (posterior draws, confidence bands, bootstrap distributions) straightforward.
Code
library(ggdist)ggplot(clinic, aes(x = treatment_group, y = hba1c, fill = treatment_group)) +stat_halfeye(alpha =0.7) +labs(title ="HbA1c distribution by treatment (stat_halfeye)",subtitle ="Shows full distribution, not just mean ± SE") +theme_minimal() +theme(legend.position ="none",axis.text.x =element_text(angle =30, hjust =1))
3.4 Highlighting subsets with gghighlight
Code
library(gghighlight)ggplot(facility_means, aes(mean_bmi, mean_hba1c)) +geom_point(size =3) +gghighlight(mean_hba1c >6.2, label_key = facility) +labs(title ="Facilities with mean HbA1c above 6.2%")
3.5 Interactivity with plotly
Any ggplot2 object can become an interactive HTML widget:
monthly_summary <- clinic |>mutate(month =as.Date(cut(visit_date, "month"))) |>group_by(month) |>summarise(mean_hba1c =mean(hba1c),se =sd(hba1c) /sqrt(n()), .groups ="drop")ggplot(monthly_summary, aes(month, mean_hba1c)) +geom_ribbon(aes(ymin = mean_hba1c -1.96* se, ymax = mean_hba1c +1.96* se),fill ="steelblue", alpha =0.2) +geom_line(color ="steelblue", linewidth =1) +labs(title ="Program-wide mean HbA1c over time",subtitle ="Shaded band = 95% CI of the monthly mean",x =NULL, y ="Mean HbA1c (%)") +theme_minimal()
4.2 Regional comparison: ordered bar/point charts
Code
region_summary <- clinic |>group_by(region) |>summarise(pct_controlled =mean(outcome_status =="Controlled") *100, .groups ="drop")ggplot(region_summary, aes(x =reorder(region, pct_controlled), y = pct_controlled)) +geom_col(fill ="steelblue") +coord_flip() +labs(title ="Percent of patients with controlled HbA1c, by region",x =NULL, y ="% Controlled") +theme_minimal()
Reordering categories by value (reorder()) rather than leaving them alphabetical is a small change that makes bar charts dramatically easier to read — always do this unless there’s a natural inherent order (e.g., age bands, dates).
4.3 Model diagnostics and marginal effects (conceptual pattern)
For real project work you’d fit a model first — e.g. a logistic GLMM of outcome_status on treatment_group with a random intercept for facility (mirroring your lme4/glmmTMB workflow), then visualize it:
Code
library(lme4)library(DHARMa)library(ggeffects)m <-glmer(outcome_status ~ treatment_group + bmi + age + (1| facility),data = clinic, family = binomial)plot(simulateResiduals(m)) # DHARMa diagnostic panelplot(ggpredict(m, terms ="bmi")) # marginal effect, already a ggplot object
ggeffects output is a ggplot2 object, so every theming/scale trick above applies directly to marginal-effects plots.
4.4 Building and reusing a custom theme (house style)
Once you have a look you like, wrap it in a function so every plot in a project or client report is consistent.
Code
theme_myreport <-function(base_size =12) {theme_minimal(base_size = base_size) +theme(plot.title =element_text(face ="bold", size =rel(1.2)),plot.subtitle =element_text(color ="grey40"),legend.position ="bottom",panel.grid.minor =element_blank(),strip.background =element_rect(fill ="grey90", color =NA) )}ggplot(clinic, aes(bmi, hba1c, color = treatment_group)) +geom_point(size =2, alpha =0.6) +labs(title ="Custom house theme", color ="Treatment") +theme_myreport()
4.5 Exporting for print vs. web
Code
# Vector format for print/publication (scales cleanly, editable in Illustrator/Inkscape)ggsave("figure1.pdf", plot = p, width =7, height =4.5, units ="in")# High-res raster for web/slidesggsave("figure1.png", plot = p, width =7, height =4.5, dpi =300)
In Quarto specifically, set fig-width, fig-height, and fig-dpi in the YAML header (as done at the top of this document) so every figure in the rendered report is consistent without per-chunk overrides.
4.6 Where to go from here
ggplot2 internals: read Hadley Wickham’s ggplot2: Elegant Graphics for Data Analysis (free online) to understand stat_* vs geom_* and writing your own geom/stat.
Spatial extension: swap region in this dataset for real county boundary shapefiles (sf) and map pct_controlled as a choropleth — a natural next step given this data’s geographic structure.
Meta-analysis extension: treat each facility as a “study” and each treatment_group effect as an effect size, then practice forest plots with metafor/netmeta on the aggregated facility_means-style table.
Dashboards: shiny + ggplot2/plotly, or a Quarto dashboard (format: dashboard), to let others filter this dataset by region or treatment interactively.
Reproducible reporting: parameterized Quarto documents (params: in YAML) to regenerate the same report structure across datasets, sites, or clients.
Suggested learning path
Stage
Focus
Time estimate
Tier 1
aes(), core geoms, basic labs/titles
1 week
Tier 2
facets, scales, color, themes, coords
1–2 weeks
Tier 3
patchwork, ggrepel, ggdist, plotly, gganimate
2–3 weeks
Tier 4
time trends, regional comparisons, model-diagnostic plots, custom themes, export pipeline
ongoing, project-driven
Work through each tier with this dataset, then repeat with your own — the concepts transfer immediately once real variables replace the simulated ones.
Source Code
---title: "Data Visualization in R"subtitle: "Using diabetes-clinic dataset"author: "Timothy Achala"format: html: toc: true toc-depth: 4 toc-location: left code-fold: show code-tools: true theme: cosmo fig-width: 7 fig-height: 4.5execute: warning: false message: false---## How to use this guideThis document is organized in four tiers: **Foundations**, **Intermediate ggplot2**,**Advanced & Extensions**, and **Domain-specific / Publication workflows**. Eachsection builds on the last. Work through it top to bottom, or jump to a tier ifyou're already comfortable with the basics. Every code chunk is runnable as-is inQuarto (Render button, or `Cmd/Ctrl+Shift+K`), provided the listed packages areinstalled and `clinic_diabetes_simulated.csv` sits in the same folder as this`.qmd` file.## The dataset`clinic_diabetes_simulated.csv` is a **fully simulated** 900-row datasetmodeled loosely on a diabetes-management program across six counties inwestern Kenya. It is *not* real patient data — it exists to give this guidevariables worth plotting: a date, a few categoricals, several continuousmeasures, and a binary outcome.| Column | Type | Description ||---|---|---|| `patient_id` | character | Unique patient identifier || `visit_date` | date | Visit date, 2023–2025 || `region` | categorical | County (6 levels) || `facility` | categorical | Health facility (nested in region) || `sex` | categorical | Female / Male || `age` | continuous | Years || `bmi` | continuous | Body mass index || `treatment_group` | categorical | 4-arm treatment assignment || `hba1c` | continuous | Glycated hemoglobin (%) || `systolic_bp` | continuous | Systolic blood pressure (a few missing values, by design) || `diastolic_bp` | continuous | Diastolic blood pressure || `outcome_status` | categorical | Controlled / Uncontrolled (HbA1c < 7.0 threshold) || `cost_kes` | continuous | Simulated visit cost, Kenyan shillings |```{r setup, message=FALSE, warning=FALSE}# Run once if you don't have these installed:# install.packages(c("tidyverse", "scales", "patchwork", "ggrepel",# "ggdist", "gghighlight", "gganimate", "plotly",# "sf", "rnaturalearth", "viridis", "ggthemes"))library(ggplot2)library(dplyr)library(readr)clinic <-read_csv("clinic_diabetes_simulated.csv", show_col_types =FALSE) |>mutate(region =factor(region),treatment_group =factor(treatment_group),outcome_status =factor(outcome_status, levels =c("Uncontrolled", "Controlled")),sex =factor(sex) )glimpse(clinic)```---# Tier 1 — Foundations## 1.1 Why R for visualization?R treats a plot as **data + a grammar**, not a drawing you click together. Thatgrammar (the "grammar of graphics", implemented by `ggplot2`) means you describe*what* the plot should show, and R figures out *how* to draw it. This is whatmakes R plots so reproducible and easy to iterate on.## 1.2 Base R plotting (know it, don't live in it)Base R plotting is fast for a quick look at data. You'll see it in otherpeople's code, so it's worth recognizing even if you do most of your work in`ggplot2`.```{r}# Quick scatterplotplot(clinic$bmi, clinic$hba1c,main ="BMI vs HbA1c", xlab ="BMI", ylab ="HbA1c (%)",pch =19, col ="steelblue")# Quick histogramhist(clinic$age, breaks =15, col ="grey80",main ="Distribution of Patient Age", xlab ="Age")# Boxplot by groupboxplot(hba1c ~ treatment_group, data = clinic,main ="HbA1c by Treatment Group", xlab ="", ylab ="HbA1c (%)",las =2)```**When base R is fine:** fast exploratory checks while cleaning data.**When to switch to ggplot2:** anything you'll show someone else, iterate on,or reuse.## 1.3 The Grammar of Graphics: ggplot2 fundamentalsEvery `ggplot2` plot has three required pieces:1. **Data** — a data frame2. **Aesthetic mappings** (`aes()`) — which columns map to which visual properties (x, y, color, size, shape...)3. **Geometry** (`geom_*()`) — the type of visual mark (points, bars, lines...)```{r}ggplot(data = clinic, aes(x = bmi, y = hba1c)) +geom_point()```### Adding more aesthetics```{r}ggplot(clinic, aes(x = bmi, y = hba1c, color = treatment_group, size = age)) +geom_point(alpha =0.6) +labs(title ="HbA1c vs BMI by Treatment",x ="BMI", y ="HbA1c (%)",color ="Treatment", size ="Age")```### The core geoms you'll use constantly```{r}#| fig-height: 6library(patchwork)p1 <-ggplot(clinic, aes(bmi, hba1c)) +geom_point(alpha =0.4) +labs(title ="geom_point")p2 <-ggplot(clinic, aes(region)) +geom_bar() +coord_flip() +labs(title ="geom_bar")p3 <-ggplot(clinic, aes(hba1c)) +geom_histogram(bins =30) +labs(title ="geom_histogram")p4 <-ggplot(clinic, aes(treatment_group, hba1c)) +geom_boxplot() +labs(title ="geom_boxplot") +theme(axis.text.x =element_text(angle =30, hjust =1))p5 <-ggplot(clinic, aes(bmi, hba1c)) +geom_point(alpha =0.3) +geom_smooth(method ="lm") +labs(title ="geom_smooth")p6 <-ggplot(clinic, aes(cost_kes)) +geom_density(fill ="steelblue", alpha =0.5) +labs(title ="geom_density")(p1 | p2 | p3) / (p4 | p5 | p6)```### Practice exercise (Tier 1)> Make a scatterplot of `age` vs `systolic_bp`, colored by `sex`, with a> linear trend line per group.```{r}#| code-fold: true#| code-summary: "Show solution"ggplot(clinic, aes(age, systolic_bp, color = sex)) +geom_point(alpha =0.5) +geom_smooth(method ="lm", se =FALSE) +labs(x ="Age", y ="Systolic BP", color ="Sex")```---# Tier 2 — Intermediate ggplot2## 2.1 Facets: small multiplesFacets split one plot into a grid of panels by a categorical variable — oftenclearer than cramming everything into color/shape.```{r}ggplot(clinic, aes(bmi, hba1c)) +geom_point(alpha =0.4) +facet_wrap(~ region) +labs(title ="HbA1c vs BMI, split by Region")``````{r}ggplot(clinic, aes(bmi, hba1c)) +geom_point(alpha =0.4) +facet_grid(sex ~ outcome_status) +labs(title ="facet_grid: two variables at once")```## 2.2 Scales: controlling how data maps to visualsScales control axis breaks, color palettes, and transformations.```{r}ggplot(clinic, aes(bmi, hba1c, color = age)) +geom_point(size =2) +scale_color_viridis_c(option ="plasma") +scale_x_continuous(breaks =seq(15, 50, 5)) +scale_y_continuous(labels = scales::label_number(suffix ="%"))```Log scales are common for skewed data (cost, income, viral load):```{r}ggplot(clinic, aes(cost_kes)) +geom_histogram(bins =30) +scale_x_log10(labels = scales::label_number(big.mark =",")) +labs(title ="Log-scaled cost distribution", x ="Cost (KES, log scale)")```## 2.3 Color: palettes that communicate- **Sequential** (`viridis`, `scale_color_gradient`) — ordered numeric data- **Diverging** (`scale_color_gradient2`) — data with a meaningful midpoint (e.g., above/below a threshold)- **Qualitative** (`scale_color_brewer`, `ggthemes::scale_color_tableau`) — unordered categories```{r}ggplot(clinic, aes(bmi, hba1c, color = treatment_group)) +geom_point(size =2, alpha =0.6) +scale_color_brewer(palette ="Dark2") +labs(color ="Treatment")```**Rule of thumb:** always check your plot renders sensibly in grayscale/forcolorblind viewers. `viridis` palettes are colorblind-safe by design.## 2.4 Themes: polishing the look```{r}p <-ggplot(clinic, aes(bmi, hba1c, color = treatment_group)) +geom_point(size =2, alpha =0.6) +labs(title ="HbA1c vs BMI", subtitle ="By treatment group",x ="BMI", y ="HbA1c (%)", color ="Treatment",caption ="Source: simulated clinic dataset")p +theme_minimal()``````{r}# Custom theme tweaks — this is how you build a "house style"p +theme_minimal(base_size =13) +theme(plot.title =element_text(face ="bold"),legend.position ="bottom",panel.grid.minor =element_blank() )```## 2.5 Coordinate systems```{r}# Flipped bar chart — useful for long category labelsggplot(clinic, aes(x = facility)) +geom_bar(fill ="steelblue") +coord_flip() +labs(title ="coord_flip()", x =NULL, y ="Visits")``````{r}# Polar coordinates -> donut (use sparingly; bar charts usually communicate better)df <- clinic |>count(outcome_status)ggplot(df, aes(x =2, y = n, fill = outcome_status)) +geom_col(width =1) +coord_polar(theta ="y") +xlim(0.5, 2.5) +theme_void() +labs(title ="coord_polar(): use sparingly", fill ="Outcome")```### Practice exercise (Tier 2)> Facet the `bmi` vs `hba1c` scatterplot by `treatment_group`, apply a> viridis color scale mapped to `age`, and use `theme_minimal()`.```{r}#| code-fold: true#| code-summary: "Show solution"ggplot(clinic, aes(bmi, hba1c, color = age)) +geom_point(size =2, alpha =0.6) +facet_wrap(~treatment_group) +scale_color_viridis_c() +theme_minimal()```---# Tier 3 — Advanced & Extensions## 3.1 Composing multi-panel figures with `patchwork````{r}library(patchwork)a <-ggplot(clinic, aes(bmi, hba1c)) +geom_point(alpha =0.4)b <-ggplot(clinic, aes(treatment_group)) +geom_bar() +theme(axis.text.x =element_text(angle =30, hjust =1))(a | b) +plot_annotation(title ="Combined figure", tag_levels ="A")```## 3.2 Avoiding label overlap with `ggrepel````{r}library(ggrepel)facility_means <- clinic |>group_by(facility) |>summarise(mean_bmi =mean(bmi), mean_hba1c =mean(hba1c), .groups ="drop")ggplot(facility_means, aes(mean_bmi, mean_hba1c, label = facility)) +geom_point() +geom_text_repel(size =3, max.overlaps =15) +labs(title ="Facility-level averages", x ="Mean BMI", y ="Mean HbA1c")```## 3.3 Visualizing uncertainty with `ggdist`Point estimates without uncertainty are half a story. `ggdist` makesdistributional visualization (posterior draws, confidence bands, bootstrapdistributions) straightforward.```{r}library(ggdist)ggplot(clinic, aes(x = treatment_group, y = hba1c, fill = treatment_group)) +stat_halfeye(alpha =0.7) +labs(title ="HbA1c distribution by treatment (stat_halfeye)",subtitle ="Shows full distribution, not just mean ± SE") +theme_minimal() +theme(legend.position ="none",axis.text.x =element_text(angle =30, hjust =1))```## 3.4 Highlighting subsets with `gghighlight````{r}library(gghighlight)ggplot(facility_means, aes(mean_bmi, mean_hba1c)) +geom_point(size =3) +gghighlight(mean_hba1c >6.2, label_key = facility) +labs(title ="Facilities with mean HbA1c above 6.2%")```## 3.5 Interactivity with `plotly`Any `ggplot2` object can become an interactive HTML widget:```{r}library(plotly)p <-ggplot(clinic, aes(bmi, hba1c, color = treatment_group,text =paste(patient_id, "-", region))) +geom_point(size =2, alpha =0.6) +labs(color ="Treatment")ggplotly(p, tooltip =c("text", "bmi", "hba1c"))```Use interactivity for exploratory dashboards or web reports — not for staticprint/PDF output, where it silently falls back to an image.## 3.6 Animation with `gganimate`Useful for time-series or process data — here, average HbA1c drifting downover the study period as treatment effects accumulate.```{r}#| eval: falselibrary(gganimate)monthly <- clinic |>mutate(month =as.Date(cut(visit_date, "month"))) |>group_by(month, treatment_group) |>summarise(mean_hba1c =mean(hba1c), .groups ="drop")ggplot(monthly, aes(month, mean_hba1c, color = treatment_group)) +geom_line(linewidth =1) +geom_point(size =2) +labs(title ="Mean HbA1c over time", x =NULL, y ="Mean HbA1c (%)") +transition_reveal(month)```*(Chunk set to `eval: false` since `gganimate` rendering is slow — flip it onwhen you're ready to render the .gif.)*### Practice exercise (Tier 3)> Take `facility_means` from 3.2, add `gghighlight()` to highlight facilities> with `mean_bmi > 28`, and wrap it in `ggplotly()` for interactivity.```{r}#| code-fold: true#| code-summary: "Show solution"p <-ggplot(facility_means, aes(mean_bmi, mean_hba1c, label = facility)) +geom_point(size =3) +gghighlight(mean_bmi >28)ggplotly(p)```---# Tier 4 — Domain-Specific & Publication Workflows## 4.1 Time trends: line charts done properly```{r}monthly_summary <- clinic |>mutate(month =as.Date(cut(visit_date, "month"))) |>group_by(month) |>summarise(mean_hba1c =mean(hba1c),se =sd(hba1c) /sqrt(n()), .groups ="drop")ggplot(monthly_summary, aes(month, mean_hba1c)) +geom_ribbon(aes(ymin = mean_hba1c -1.96* se, ymax = mean_hba1c +1.96* se),fill ="steelblue", alpha =0.2) +geom_line(color ="steelblue", linewidth =1) +labs(title ="Program-wide mean HbA1c over time",subtitle ="Shaded band = 95% CI of the monthly mean",x =NULL, y ="Mean HbA1c (%)") +theme_minimal()```## 4.2 Regional comparison: ordered bar/point charts```{r}region_summary <- clinic |>group_by(region) |>summarise(pct_controlled =mean(outcome_status =="Controlled") *100, .groups ="drop")ggplot(region_summary, aes(x =reorder(region, pct_controlled), y = pct_controlled)) +geom_col(fill ="steelblue") +coord_flip() +labs(title ="Percent of patients with controlled HbA1c, by region",x =NULL, y ="% Controlled") +theme_minimal()```Reordering categories by value (`reorder()`) rather than leaving themalphabetical is a small change that makes bar charts dramatically easier toread — always do this unless there's a natural inherent order (e.g., agebands, dates).## 4.3 Model diagnostics and marginal effects (conceptual pattern)For real project work you'd fit a model first — e.g. a logistic GLMM of`outcome_status` on `treatment_group` with a random intercept for `facility`(mirroring your `lme4`/`glmmTMB` workflow), then visualize it:```{r}#| eval: falselibrary(lme4)library(DHARMa)library(ggeffects)m <-glmer(outcome_status ~ treatment_group + bmi + age + (1| facility),data = clinic, family = binomial)plot(simulateResiduals(m)) # DHARMa diagnostic panelplot(ggpredict(m, terms ="bmi")) # marginal effect, already a ggplot object````ggeffects` output is a `ggplot2` object, so every theming/scale trick aboveapplies directly to marginal-effects plots.## 4.4 Building and reusing a custom theme (house style)Once you have a look you like, wrap it in a function so every plot in aproject or client report is consistent.```{r}theme_myreport <-function(base_size =12) {theme_minimal(base_size = base_size) +theme(plot.title =element_text(face ="bold", size =rel(1.2)),plot.subtitle =element_text(color ="grey40"),legend.position ="bottom",panel.grid.minor =element_blank(),strip.background =element_rect(fill ="grey90", color =NA) )}ggplot(clinic, aes(bmi, hba1c, color = treatment_group)) +geom_point(size =2, alpha =0.6) +labs(title ="Custom house theme", color ="Treatment") +theme_myreport()```## 4.5 Exporting for print vs. web```{r}#| eval: false# Vector format for print/publication (scales cleanly, editable in Illustrator/Inkscape)ggsave("figure1.pdf", plot = p, width =7, height =4.5, units ="in")# High-res raster for web/slidesggsave("figure1.png", plot = p, width =7, height =4.5, dpi =300)```In Quarto specifically, set `fig-width`, `fig-height`, and `fig-dpi` in theYAML header (as done at the top of this document) so every figure in therendered report is consistent without per-chunk overrides.## 4.6 Where to go from here- **`ggplot2` internals**: read Hadley Wickham's *ggplot2: Elegant Graphics for Data Analysis* (free online) to understand `stat_*` vs `geom_*` and writing your own `geom`/`stat`.- **Spatial extension**: swap `region` in this dataset for real county boundary shapefiles (`sf`) and map `pct_controlled` as a choropleth — a natural next step given this data's geographic structure.- **Meta-analysis extension**: treat each `facility` as a "study" and each`treatment_group` effect as an effect size, then practice forest plots with `metafor`/`netmeta` on the aggregated `facility_means`-style table.- **Dashboards**: `shiny` + `ggplot2`/`plotly`, or a Quarto dashboard (`format: dashboard`), to let others filter this dataset by region or treatment interactively.- **Reproducible reporting**: parameterized Quarto documents (`params:` in YAML) to regenerate the same report structure across datasets, sites, or clients.---## Suggested learning path| Stage | Focus | Time estimate ||---|---|---|| Tier 1 | `aes()`, core geoms, basic labs/titles | 1 week || Tier 2 | facets, scales, color, themes, coords | 1–2 weeks || Tier 3 | patchwork, ggrepel, ggdist, plotly, gganimate | 2–3 weeks || Tier 4 | time trends, regional comparisons, model-diagnostic plots, custom themes, export pipeline | ongoing, project-driven |Work through each tier with **this dataset**, then repeat with your own —the concepts transfer immediately once real variables replace the simulatedones.