Forest plots display point estimates with confidence intervals—the standard visualization for meta-analyses and clinical trial results.

TipTwo Ways to Add Forest Columns

With tabviz() + viz_forest() (recommended for control):

tabviz(data, label = "study", columns = list(
  viz_forest(point = "hr", lower = "lo", upper = "hi", scale = "log", null_value = 1)
))

With forest_plot() (quick shortcut):

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.

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)
  ))

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
Code
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:

Code
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)")
  ))
WarningLog 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:

Code
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.

Code
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:

Code
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)
  )
)
TipWhen 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:

Code
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

Axis Range

Override the auto-calculated range:

Code
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:

Code
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:

Code
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():

Code
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

Code
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:

Code
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

Code
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