Code
data(effect_sizes)
effect_sizes |>
head(4) |>
tabviz(label = "study", columns = list(
viz_forest(point = "hr", lower = "lower", upper = "upper",
scale = "log", null_value = 1)
))Forest plots display point estimates with confidence intervals—the standard visualization for meta-analyses and clinical trial results.
With tabviz() + viz_forest() (recommended for control):
With forest_plot() (quick shortcut):
tabviz uses a column-mapping pattern: specify which columns contain your data, not the values themselves.
| 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 |
tabviz uses NA values strategically for structured layouts:
row_type = "header" with NA for effect estimatesrow_type = "spacer" for visual separationUse for ratios (odds ratio, hazard ratio, risk ratio) where effects are multiplicative:
All values in point, lower, and upper must be positive. Zero or negative values will cause errors.
Use for differences (mean difference, risk difference, SMD) where effects are additive:
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)
)
tabviz(diff_data, label = "comparison", columns = list(
viz_forest(point = "mean_diff", lower = "lower", upper = "upper",
scale = "linear", null_value = 0, axis_label = "Mean Difference (95% CI)")
))Display multiple effect estimates per row—useful for comparing analyses or outcomes.
multi_data <- data.frame(
study = c("PIONEER", "SUMMIT", "HORIZON"),
n = c(2450, 1890, 3200),
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),
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)
)
tabviz(multi_data, label = "study", title = "Sensitivity Analysis Comparison",
columns = list(
col_n("n"),
col_interval(point = "itt_or", lower = "itt_lo", upper = "itt_hi", header = "Primary OR"),
viz_forest(
scale = "log", null_value = 1,
effects = list(
effect_forest("itt_or", "itt_lo", "itt_hi", label = "ITT", color = "#2563eb"),
effect_forest("pp_or", "pp_lo", "pp_hi", label = "Per-Protocol", color = "#16a34a")
))
)
)| 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" |
When comparing entirely different outcomes or analyses, multiple viz_forest() columns can be more effective:
comparison_data <- data.frame(
study = c("Cardiovascular", "Mortality", "Hospitalization", "Composite"),
na_hr = c(0.72, 0.85, 0.78, 0.76),
na_lo = c(0.58, 0.71, 0.64, 0.65),
na_hi = c(0.89, 1.02, 0.95, 0.89),
eu_hr = c(0.68, 0.79, 0.82, 0.74),
eu_lo = c(0.52, 0.62, 0.67, 0.61),
eu_hi = c(0.89, 1.01, 1.00, 0.90)
)
tabviz(comparison_data, label = "study",
title = "Regional Comparison: North America vs Europe",
columns = list(
viz_forest(point = "na_hr", lower = "na_lo", upper = "na_hi",
header = "North America", scale = "log", null_value = 1, width = 180),
viz_forest(point = "eu_hr", lower = "eu_lo", upper = "eu_hi",
header = "Europe", scale = "log", null_value = 1, width = 180)
)
)Use side-by-side viz_forest() columns when:
Use multi-effect overlays (effect_forest()) when:
Customize marker appearance based on data values using formula expressions:
effect_sizes |>
filter(outcome == "Primary") |>
head(6) |>
tabviz(label = "study", title = "Marker Styling with Formulas",
marker_shape = ~ ifelse(phase == "Phase 3", "circle", "square"),
marker_color = ~ ifelse(significant, "#16a34a", "#94a3b8"),
marker_opacity = ~ ifelse(n > 200, 1, 0.7),
columns = list(
col_text("phase", "Phase"),
viz_forest(point = "hr", lower = "lower", upper = "upper",
scale = "log", null_value = 1)
)
)| 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 (~ ...).
Override the auto-calculated range:
Specify exact tick positions:
Add vertical gridlines at tick positions:
| 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" |
The null_value parameter draws a dashed vertical reference line. Add custom reference lines with refline():
tabviz(axis_demo, label = "study", title = "Clinical Thresholds",
columns = list(
viz_forest(point = "hr", lower = "lower", upper = "upper",
scale = "log", null_value = 1,
annotations = list(
refline(0.5, label = "Strong", style = "solid", color = "#16a34a"),
refline(0.8, label = "Moderate", style = "dashed", color = "#f59e0b")
))
)
)| Argument | Description |
|---|---|
value |
X-axis position |
label |
Optional label text |
style |
"solid", "dashed", "dotted" |
color |
Line color |
width |
Line width (default: 1.5) |
opacity |
Line opacity 0-1 (default: 0.6) |
Before plotting, validate your data:
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"))
}
}library(metafor)
# Run meta-analysis
res <- rma(yi = log_or, sei = se, data = studies, method = "REML")
# Convert to tabviz 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),
row_type = "summary",
row_bold = TRUE
)
)
tabviz(forest_data, label = "study", row_type = "row_type", row_bold = "row_bold",
columns = list(
viz_forest(point = "or", lower = "lower", upper = "upper",
scale = "log", null_value = 1)
)
)---
title: "Forest Plots"
---
```{r}
#| include: false
library(tabviz)
library(dplyr)
library(tidyr)
```
Forest plots display point estimates with confidence intervals—the standard visualization for meta-analyses and clinical trial results.
::: {.callout-tip}
## Two Ways to Add Forest Columns
**With `tabviz()` + `viz_forest()`** (recommended for control):
```r
tabviz(data, label = "study", columns = list(
viz_forest(point = "hr", lower = "lo", upper = "hi", scale = "log", null_value = 1)
))
```
**With `forest_plot()`** (quick shortcut):
```r
forest_plot(data, point = "hr", lower = "lo", upper = "hi", label = "study",
scale = "log", null_value = 1)
```
:::
## Data Mapping
tabviz uses a **column-mapping pattern**: specify which columns contain your data, not the values themselves.
```{r}
data(effect_sizes)
effect_sizes |>
head(4) |>
tabviz(label = "study", columns = list(
viz_forest(point = "hr", lower = "lower", upper = "upper",
scale = "log", null_value = 1)
))
```
### Required 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 |
### Handling Missing Values (NA)
tabviz 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}
data(glp1_trials)
glp1_trials |>
filter(group == "Main Trials") |>
head(5) |>
tabviz(label = "study", row_type = "row_type", row_bold = "row_bold",
columns = list(
viz_forest(point = "hr", lower = "lower", upper = "upper",
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}
effect_sizes |>
filter(outcome == "Primary") |>
head(4) |>
tabviz(label = "study", columns = list(
viz_forest(point = "hr", lower = "lower", upper = "upper",
scale = "log", null_value = 1, 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)
)
tabviz(diff_data, label = "comparison", columns = list(
viz_forest(point = "mean_diff", lower = "lower", upper = "upper",
scale = "linear", null_value = 0, 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_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),
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)
)
tabviz(multi_data, label = "study", title = "Sensitivity Analysis Comparison",
columns = list(
col_n("n"),
col_interval(point = "itt_or", lower = "itt_lo", upper = "itt_hi", header = "Primary OR"),
viz_forest(
scale = "log", null_value = 1,
effects = list(
effect_forest("itt_or", "itt_lo", "itt_hi", label = "ITT", color = "#2563eb"),
effect_forest("pp_or", "pp_lo", "pp_hi", label = "Per-Protocol", color = "#16a34a")
))
)
)
```
### effect_forest() 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"` |
### Side-by-Side Forest Columns
When comparing entirely different outcomes or analyses, multiple `viz_forest()` columns can be more effective:
```{r}
comparison_data <- data.frame(
study = c("Cardiovascular", "Mortality", "Hospitalization", "Composite"),
na_hr = c(0.72, 0.85, 0.78, 0.76),
na_lo = c(0.58, 0.71, 0.64, 0.65),
na_hi = c(0.89, 1.02, 0.95, 0.89),
eu_hr = c(0.68, 0.79, 0.82, 0.74),
eu_lo = c(0.52, 0.62, 0.67, 0.61),
eu_hi = c(0.89, 1.01, 1.00, 0.90)
)
tabviz(comparison_data, label = "study",
title = "Regional Comparison: North America vs Europe",
columns = list(
viz_forest(point = "na_hr", lower = "na_lo", upper = "na_hi",
header = "North America", scale = "log", null_value = 1, width = 180),
viz_forest(point = "eu_hr", lower = "eu_lo", upper = "eu_hi",
header = "Europe", scale = "log", null_value = 1, width = 180)
)
)
```
::: {.callout-tip}
## When to Use Side-by-Side vs Multi-Effect
**Use side-by-side `viz_forest()` columns when:**
- Comparing different outcomes (primary vs secondary endpoint)
- Showing different populations (ITT vs as-treated)
- Outcomes have different clinical meaning
**Use multi-effect overlays (`effect_forest()`) when:**
- Comparing the same outcome across different analyses
- Sensitivity analyses (e.g., with/without imputation)
- Effects should be directly visually compared on the same scale
:::
---
## Marker Customization
Customize marker appearance based on data values using formula expressions:
```{r}
effect_sizes |>
filter(outcome == "Primary") |>
head(6) |>
tabviz(label = "study", title = "Marker Styling with Formulas",
marker_shape = ~ ifelse(phase == "Phase 3", "circle", "square"),
marker_color = ~ ifelse(significant, "#16a34a", "#94a3b8"),
marker_opacity = ~ ifelse(n > 200, 1, 0.7),
columns = list(
col_text("phase", "Phase"),
viz_forest(point = "hr", lower = "lower", upper = "upper",
scale = "log", null_value = 1)
)
)
```
### 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 (`~ ...`).
---
## Axis Control
```{r}
#| include: false
axis_demo <- effect_sizes |>
filter(outcome == "Primary") |>
head(4)
```
### Axis Range
Override the auto-calculated range:
```{r}
tabviz(axis_demo, label = "study", columns = list(
viz_forest(point = "hr", lower = "lower", upper = "upper",
scale = "log", null_value = 1, axis_range = c(0.3, 2.0))
))
```
### Custom Tick Values
Specify exact tick positions:
```{r}
tabviz(axis_demo, label = "study", columns = list(
viz_forest(point = "hr", lower = "lower", upper = "upper",
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}
tabviz(axis_demo, label = "study", columns = list(
viz_forest(point = "hr", lower = "lower", upper = "upper",
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 `refline()`:
```{r}
tabviz(axis_demo, label = "study", title = "With Custom Reference Line",
columns = list(
viz_forest(point = "hr", lower = "lower", upper = "upper",
scale = "log", null_value = 1,
annotations = list(
refline(0.8, label = "Threshold", style = "dashed", color = "#e11d48")
))
)
)
```
### Multiple Reference Lines
```{r}
tabviz(axis_demo, label = "study", title = "Clinical Thresholds",
columns = list(
viz_forest(point = "hr", lower = "lower", upper = "upper",
scale = "log", null_value = 1,
annotations = list(
refline(0.5, label = "Strong", style = "solid", color = "#16a34a"),
refline(0.8, label = "Moderate", style = "dashed", color = "#f59e0b")
))
)
)
```
### refline() Arguments
| Argument | Description |
|----------|-------------|
| `value` | X-axis position |
| `label` | Optional label text |
| `style` | `"solid"`, `"dashed"`, `"dotted"` |
| `color` | Line color |
| `width` | Line width (default: 1.5) |
| `opacity` | Line opacity 0-1 (default: 0.6) |
---
## Data Validation
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 tabviz 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),
row_type = "summary",
row_bold = TRUE
)
)
tabviz(forest_data, label = "study", row_type = "row_type", row_bold = "row_bold",
columns = list(
viz_forest(point = "or", lower = "lower", upper = "upper",
scale = "log", null_value = 1)
)
)
```
---
## See Also
- [Visualizations Overview](index.qmd) — Interactivity, tooltips, export
- [Titles & Labels](../titles.qmd) — Adding context to your plots
- [Row Styling](../row-styling.qmd) — Headers, summaries, indentation
- [Themes](../themes.qmd) — Visual styling