---
title: "Forest Plots"
---
```{r}
#| include: false
library(webforest)
library(dplyr)
```
This chapter covers the core features for building forest plots: data mapping, scales, multiple effects, markers, axis control, interactivity, and exporting.
## Data Mapping
webforest uses a **column-mapping pattern**: you specify which columns contain your data, not the values themselves.
```{r}
# Your data can have any column names
data <- data.frame(
study = c("Smith 2020", "Jones 2021", "Lee 2022", "Chen 2023"),
my_hr = c(0.72, 0.85, 0.91, 0.68),
ci_lo = c(0.55, 0.70, 0.75, 0.52),
ci_hi = c(0.95, 1.03, 1.10, 0.89)
)
# Just map them to the right arguments
forest_plot(data,
point = "my_hr",
lower = "ci_lo",
upper = "ci_hi",
label = "study",
null_value = 1, scale = "log"
)
```
### Required Mappings
Every forest plot needs these three column mappings:
| Argument | What it is | Example |
|----------|------------|---------|
| `point` | Point estimate | Hazard ratio, odds ratio, mean difference |
| `lower` | Lower CI bound | 95% CI lower |
| `upper` | Upper CI bound | 95% CI upper |
The `label` argument is optional but almost always used to identify each row.
### Handling Missing Values (NA)
webforest uses `NA` values strategically for structured layouts:
- **Header rows**: `row_type = "header"` with `NA` for effect estimates
- **Spacer rows**: `row_type = "spacer"` for visual separation
- **Missing estimates**: Data rows where effect couldn't be calculated
```{r}
structured <- data.frame(
label = c("Primary Outcomes", " CV Death", " MI", "", "Secondary"),
hr = c(NA, 0.82, 0.79, NA, NA),
lower = c(NA, 0.72, 0.68, NA, NA),
upper = c(NA, 0.94, 0.92, NA, NA),
rtype = c("header", "data", "data", "spacer", "header"),
rbold = c(TRUE, FALSE, FALSE, FALSE, TRUE)
)
forest_plot(structured,
point = "hr", lower = "lower", upper = "upper", label = "label",
row_type = "rtype", row_bold = "rbold",
scale = "log", null_value = 1
)
```
---
## Scale and Null Value
### Log Scale
Use for **ratios** (odds ratio, hazard ratio, risk ratio) where effects are multiplicative:
```{r}
forest_plot(data,
point = "my_hr", lower = "ci_lo", upper = "ci_hi", label = "study",
scale = "log",
null_value = 1, # Ratio of 1 = no effect
axis_label = "Hazard Ratio (95% CI)"
)
```
::: {.callout-warning}
## Log Scale Requires Positive Values
All values in `point`, `lower`, and `upper` must be **positive**. Zero or negative values will cause errors.
:::
### Linear Scale
Use for **differences** (mean difference, risk difference, SMD) where effects are additive:
```{r}
diff_data <- data.frame(
comparison = c("Treatment A", "Treatment B", "Treatment C"),
mean_diff = c(-2.5, 1.3, -0.8),
lower = c(-4.1, -0.2, -2.1),
upper = c(-0.9, 2.8, 0.5)
)
forest_plot(diff_data,
point = "mean_diff", lower = "lower", upper = "upper",
label = "comparison",
scale = "linear",
null_value = 0, # Difference of 0 = no effect
axis_label = "Mean Difference (95% CI)"
)
```
---
## Multiple Effects
Display multiple effect estimates per row—useful for comparing analyses or outcomes.
```{r}
multi_data <- data.frame(
study = c("PIONEER", "SUMMIT", "HORIZON"),
n = c(2450, 1890, 3200),
# ITT analysis
itt_or = c(0.72, 0.78, 0.65),
itt_lo = c(0.58, 0.64, 0.52),
itt_hi = c(0.89, 0.95, 0.81),
# Per-Protocol analysis
pp_or = c(0.68, 0.73, 0.61),
pp_lo = c(0.53, 0.58, 0.47),
pp_hi = c(0.87, 0.92, 0.79)
)
forest_plot(multi_data,
point = "itt_or", lower = "itt_lo", upper = "itt_hi",
label = "study",
effects = list(
web_effect("itt_or", "itt_lo", "itt_hi", label = "ITT", color = "#2563eb"),
web_effect("pp_or", "pp_lo", "pp_hi", label = "Per-Protocol", color = "#16a34a")
),
columns = list(
col_n("n"),
col_interval("Primary OR")
),
scale = "log", null_value = 1,
title = "Sensitivity Analysis Comparison"
)
```
### web_effect() Arguments
| Argument | Description |
|----------|-------------|
| `point` | Column name for point estimates |
| `lower` | Column name for lower bounds |
| `upper` | Column name for upper bounds |
| `label` | Display label for legend |
| `color` | Override color for this effect |
| `shape` | Override shape: `"square"`, `"circle"`, `"diamond"`, `"triangle"` |
---
## Marker Customization
Customize marker appearance based on data values using formula expressions:
```{r}
marker_data <- data.frame(
study = c("RCT-001", "RCT-002", "OBS-001", "OBS-002"),
design = c("RCT", "RCT", "Observational", "Observational"),
hr = c(0.72, 0.68, 0.82, 0.78),
lower = c(0.58, 0.52, 0.68, 0.64),
upper = c(0.89, 0.89, 0.99, 0.95),
pvalue = c(0.002, 0.001, 0.045, 0.022)
)
forest_plot(marker_data,
point = "hr", lower = "lower", upper = "upper",
label = "study",
# Formula expressions - no pre-computed columns needed
marker_shape = ~ ifelse(design == "RCT", "circle", "square"),
marker_color = ~ ifelse(pvalue < 0.05, "#16a34a", "#94a3b8"),
marker_opacity = ~ ifelse(pvalue < 0.01, 1, 0.7),
scale = "log", null_value = 1,
title = "Marker Styling with Formulas"
)
```
### Marker Arguments
| Argument | Value Type | Effect |
|----------|------------|--------|
| `marker_color` | string | CSS color for marker fill |
| `marker_shape` | string | Shape: `"square"`, `"circle"`, `"diamond"`, `"triangle"` |
| `marker_opacity` | numeric | Transparency (0–1) |
| `marker_size` | numeric | Size multiplier (1 = default) |
All marker arguments accept either a column name or a formula expression (`~ ...`).
::: {.callout-tip}
## Advanced: Pipe-Based Workflows
Marker styling can also be done with `set_marker_style()`. See [The Fluent API](fluent-api.qmd).
:::
---
## Axis Control
### Axis Range
By default, the axis range is auto-calculated. Override with `axis_range`:
```{r}
forest_plot(data,
point = "my_hr", lower = "ci_lo", upper = "ci_hi", label = "study",
scale = "log", null_value = 1,
axis_range = c(0.3, 2.0)
)
```
### Custom Tick Values
Specify exact tick positions with `axis_ticks`:
```{r}
forest_plot(data,
point = "my_hr", lower = "ci_lo", upper = "ci_hi", label = "study",
scale = "log", null_value = 1,
axis_range = c(0.3, 2.0),
axis_ticks = c(0.5, 0.7, 1.0, 1.5, 2.0)
)
```
### Gridlines
Add vertical gridlines at tick positions:
```{r}
forest_plot(data,
point = "my_hr", lower = "ci_lo", upper = "ci_hi", label = "study",
scale = "log", null_value = 1,
axis_gridlines = TRUE
)
```
### Axis Summary
| Parameter | Description | Example |
|-----------|-------------|---------|
| `axis_range` | Min/max axis values | `c(0.3, 2.0)` |
| `axis_ticks` | Custom tick positions | `c(0.5, 1, 2)` |
| `axis_gridlines` | Show vertical gridlines | `TRUE` |
| `axis_label` | X-axis label | `"Odds Ratio"` |
---
## Reference Lines
The `null_value` parameter draws a dashed vertical reference line. Add custom reference lines with `forest_refline()`:
```{r}
forest_plot(data,
point = "my_hr", lower = "ci_lo", upper = "ci_hi", label = "study",
scale = "log", null_value = 1,
annotations = list(
forest_refline(0.8, label = "Threshold", style = "dashed", color = "#e11d48")
),
title = "With Custom Reference Line"
)
```
### forest_refline() Arguments
| Argument | Description |
|----------|-------------|
| `value` | X-axis position |
| `label` | Optional label text |
| `style` | `"solid"`, `"dashed"`, `"dotted"` |
| `color` | Line color |
---
## Titles and Labels
```{r}
forest_plot(data,
point = "my_hr", lower = "ci_lo", upper = "ci_hi", label = "study",
scale = "log", null_value = 1,
title = "Treatment Effect on Primary Outcome",
subtitle = "Randomized controlled trials, 2020-2024",
caption = "HR < 1 favors treatment",
footnote = "Random effects meta-analysis",
axis_label = "Hazard Ratio (95% CI)"
)
```
| Argument | Position |
|----------|----------|
| `title` | Top |
| `subtitle` | Below title |
| `axis_label` | Below axis |
| `caption` | Bottom left |
| `footnote` | Bottom right |
---
## Interactivity
::: {.callout-note}
## Interactive by Default
All webforest plots include these interactions out of the box:
- **Hover highlighting** on rows
- **Click to select** rows
- **Column sorting** via header clicks
- **Column resizing** via drag
- **Group collapsing** via chevron clicks
- **Download button** on hover (top-right)
:::
### Hover Tooltips
Tooltips are **opt-in**. Specify fields to show when hovering over interval markers:
```{r}
tooltip_data <- data.frame(
study = c("EMPA-REG 2015", "CANVAS 2017", "DECLARE 2019"),
hr = c(0.62, 0.86, 0.93),
lower = c(0.49, 0.75, 0.84),
upper = c(0.77, 0.97, 1.03),
n = c(7020, 10142, 17160),
events = c(490, 585, 882),
pvalue = c(0.0001, 0.02, 0.17)
)
forest_plot(tooltip_data,
point = "hr", lower = "lower", upper = "upper",
label = "study",
scale = "log", null_value = 1,
interaction = web_interaction(
tooltip_fields = c("n", "events", "pvalue")
),
footnote = "Hover over markers to see tooltip"
)
```
### Interaction Controls
```r
web_interaction(
tooltip_fields = NULL, # Column names for tooltip
enable_sort = TRUE, # Click headers to sort
enable_collapse = TRUE, # Click group headers to collapse
enable_select = TRUE, # Click rows to select
enable_hover = TRUE, # Highlight on hover
enable_resize = TRUE, # Drag column borders
enable_export = TRUE, # Download button
enable_themes = "default" # Theme selection menu
)
```
### Interaction Presets
| Preset | Description |
|--------|-------------|
| `web_interaction()` | Full interactivity (default) |
| `web_interaction_minimal()` | Hover only—no sorting/selection |
| `web_interaction_publication()` | Fully static (for print) |
```{r}
#| eval: false
# Publication-ready static output
forest_plot(data, ...,
interaction = web_interaction_publication()
)
```
### Zoom & Sizing
| Argument | Options | Description |
|----------|---------|-------------|
| `zoom` | 0.5 to 2.0 | Initial zoom level (default 1.0) |
| `auto_fit` | TRUE/FALSE | Shrink to fit container if too large (default TRUE) |
| `max_width` | pixels or NULL | Maximum container width |
| `max_height` | pixels or NULL | Maximum container height (enables scrolling) |
---
## Exporting Plots
webforest supports exporting to SVG, PNG, and PDF.
### Using save_plot()
```r
# Create a plot
p <- forest_plot(data,
point = "hr", lower = "lower", upper = "upper", label = "study",
scale = "log", null_value = 1
)
# Save as SVG (vector)
save_plot(p, "forest_plot.svg")
# Save as PNG (raster)
save_plot(p, "forest_plot.png", width = 1200)
# Save as PDF
save_plot(p, "forest_plot.pdf", width = 800)
```
### Output Formats
| Format | Extension | Use Case | Dependencies |
|--------|-----------|----------|--------------|
| SVG | `.svg` | Vector graphics, web, editing | None |
| PNG | `.png` | Documents, slides | `rsvg` package |
| PDF | `.pdf` | Print, publications | `rsvg` package |
### Options
| Argument | Description | Default |
|----------|-------------|---------|
| `width` | Canvas width in pixels | `800` |
| `height` | Auto-calculated if `NULL` | `NULL` |
| `scale` | PNG resolution multiplier | `2` |
### Publication-Quality Figures
```r
# High-resolution PNG at 300 DPI equivalent
# 800px × 3 = 2400px for ~8 inch width at 300 DPI
save_plot(p, "figure1.png", width = 800, scale = 3)
```
### Browser Export
Interactive plots include a download button (visible on hover in top-right):
1. Hover over plot to reveal button
2. Click to open format menu
3. Choose SVG or PNG
---
## Data Validation Tips
Before plotting, validate your data:
```{r}
validate_forest_data <- function(data, point, lower, upper, scale = "linear") {
p <- data[[point]]
l <- data[[lower]]
u <- data[[upper]]
valid <- !is.na(p)
issues <- character()
if (scale == "log" && any(p[valid] <= 0 | l[valid] <= 0 | u[valid] <= 0)) {
issues <- c(issues, "Log scale requires all positive values")
}
if (any(l[valid] > p[valid] | p[valid] > u[valid])) {
issues <- c(issues, "CI bounds should satisfy: lower <= point <= upper")
}
if (length(issues) == 0) {
message("Data looks valid!")
} else {
warning(paste(issues, collapse = "\n"))
}
}
```
---
## Working with Meta-Analysis Results
### From metafor
```{r}
#| eval: false
library(metafor)
# Run meta-analysis
res <- rma(yi = log_or, sei = se, data = studies, method = "REML")
# Convert to webforest format
forest_data <- studies |>
mutate(
or = exp(log_or),
lower = exp(log_or - 1.96 * se),
upper = exp(log_or + 1.96 * se)
) |>
bind_rows(
tibble(
study = "Pooled Estimate",
or = exp(res$b),
lower = exp(res$ci.lb),
upper = exp(res$ci.ub),
rtype = "summary",
rbold = TRUE
)
)
forest_plot(forest_data,
point = "or", lower = "lower", upper = "upper",
label = "study",
row_type = "rtype", row_bold = "rbold",
scale = "log", null_value = 1
)
```
---
## See Also
- [Columns](columns.qmd) — Column types and column groups
- [Row Styling](row-styling.qmd) — Headers, summaries, indentation
- [Cell Styling](cell-styling.qmd) — Per-cell conditional formatting
- [Themes](themes.qmd) — Preset and custom themes