Common tasks and patterns

Quick recipes for common forest plot tasks. Each recipe is self-contained and can be adapted to your data.

Add a Summary Row

Display an overall effect estimate as a diamond:

Code
meta_data <- data.frame(
  study = c("Smith 2020", "Jones 2021", "Lee 2022", "Overall"),
  hr = c(0.72, 0.85, 0.91, 0.82),
  lower = c(0.55, 0.70, 0.75, 0.70),
  upper = c(0.95, 1.03, 1.10, 0.96),
  rtype = c("data", "data", "data", "summary"),
  rbold = c(FALSE, FALSE, FALSE, TRUE)
)

forest_plot(meta_data,
  point = "hr", lower = "lower", upper = "upper",
  label = "study", null_value = 1, scale = "log",
  row_type = "rtype", row_bold = "rbold",
  theme = web_theme_modern()
)

Customize Colors for JAMA

Adjust the JAMA theme for your journal’s requirements:

Code
jama_custom <- web_theme_jama() |>
  set_colors(
    primary = "#000000",        # Black for main elements
    interval = "#000000",       # Black markers
    interval_line = "#333333"   # Dark gray CI lines
  ) |>
  set_typography(
    font_family = "Arial, sans-serif"  # Common journal font
  )

forest_plot(meta_data[1:3, ],
  point = "hr", lower = "lower", upper = "upper",
  label = "study", null_value = 1, scale = "log",
  theme = jama_custom
)

Show Multiple Effects Per Row

Display ITT, Per-Protocol, and As-Treated analyses on the same row:

Code
multi_data <- data.frame(
  study = c("Trial A", "Trial B", "Trial C"),
  itt_or = c(0.79, 0.85, 0.72),
  itt_lower = c(0.66, 0.71, 0.58),
  itt_upper = c(0.95, 1.02, 0.89),
  pp_or = c(0.75, 0.82, 0.68),
  pp_lower = c(0.62, 0.68, 0.54),
  pp_upper = c(0.91, 0.99, 0.86)
)

forest_plot(multi_data,
  point = "itt_or", lower = "itt_lower", upper = "itt_upper",
  label = "study", null_value = 1, scale = "log",
  effects = list(
    web_effect("itt_or", "itt_lower", "itt_upper",
               label = "ITT", color = "#2563eb"),
    web_effect("pp_or", "pp_lower", "pp_upper",
               label = "Per-Protocol", color = "#16a34a")
  ),
  theme = web_theme_modern(),
  footnote = "Blue = ITT, Green = Per-Protocol"
)

Export High-Resolution PNG

For journal submission at 300 DPI:

spec <- web_spec(data,
  point = "hr", lower = "lower", upper = "upper",
  label = "study", null_value = 1, scale = "log",
  theme = web_theme_jama()
)

# 800px * 3 = 2400px for ~8 inch width at 300 DPI
save_plot(spec, "figure1.png", width = 800, scale = 3)

Format P-values

P-values are displayed with Unicode superscript notation for very small values (e.g., 1.2×10⁻⁵). This keeps the display compact and readable:

Code
pval_data <- data.frame(
  study = c("Alpha", "Beta", "Gamma", "Delta", "Epsilon"),
  or = c(0.72, 0.95, 0.68, 1.02, 0.55),
  lower = c(0.55, 0.78, 0.52, 0.84, 0.42),
  upper = c(0.94, 1.16, 0.89, 1.24, 0.72),
  pval = c(0.000012, 0.42, 0.0008, 0.78, 0.00000035)
)

forest_plot(pval_data,
  point = "or", lower = "lower", upper = "upper",
  label = "study", null_value = 1,
  columns = list(
    col_pvalue("pval", "P-value")
  ),
  theme = web_theme_modern()
)

Customize P-value Precision

Control the number of significant figures and when to switch to exponential notation:

Code
forest_plot(pval_data,
  point = "or", lower = "lower", upper = "upper",
  label = "study", null_value = 1,
  columns = list(
    col_pvalue("pval", "P-value",
      digits = 3,           # 3 significant figures
      exp_threshold = 0.01  # Use exponential below 0.01
    )
  ),
  theme = web_theme_modern()
)

Add Significance Stars

Enable stars for quick visual identification (* p<0.05, ** p<0.01, *** p<0.001):

Code
forest_plot(pval_data,
  point = "or", lower = "lower", upper = "upper",
  label = "study", null_value = 1,
  columns = list(
    col_pvalue("pval", "P-value", stars = TRUE)
  ),
  theme = web_theme_modern()
)

Create Nested Subgroups

Two-level hierarchy with region and country:

Code
nested_data <- data.frame(
  study = c("Site A", "Site B", "Site C", "Site D", "Site E", "Site F"),
  country = c("USA", "USA", "USA", "UK", "UK", "Germany"),
  or = c(0.82, 0.91, 0.78, 0.85, 0.88, 0.79),
  lower = c(0.68, 0.75, 0.62, 0.70, 0.72, 0.64),
  upper = c(0.99, 1.10, 0.98, 1.03, 1.07, 0.98)
)

forest_plot(nested_data,
  point = "or", lower = "lower", upper = "upper",
  label = "study", group = "country",
  null_value = 1, scale = "log",
  theme = web_theme_modern()
)

Add Weight Bars

Show study weights as horizontal bars:

Code
weighted_data <- data.frame(
  study = c("Large RCT", "Medium RCT", "Small RCT"),
  or = c(0.85, 0.78, 0.92),
  lower = c(0.75, 0.62, 0.71),
  upper = c(0.96, 0.98, 1.19),
  weight = c(55, 30, 15)
)

forest_plot(weighted_data,
  point = "or", lower = "lower", upper = "upper",
  label = "study", null_value = 1,
  weight = "weight",  # Scale marker sizes by weight
  columns = list(
    col_bar("weight", "Weight %", width = 100)
  ),
  theme = web_theme_modern()
)

Format as Mean Difference

Use linear scale for continuous outcomes:

Code
md_data <- data.frame(
  study = c("Trial 1", "Trial 2", "Trial 3"),
  md = c(-2.5, -1.8, -3.2),
  lower = c(-4.1, -3.5, -5.0),
  upper = c(-0.9, -0.1, -1.4)
)

forest_plot(md_data,
  point = "md", lower = "lower", upper = "upper",
  label = "study",
  null_value = 0,  # 0 for differences
  scale = "linear",
  axis_label = "Mean Difference (95% CI)",
  theme = web_theme_modern()
)

Create Headers and Sections

Organize a complex meta-analysis with headers:

Code
sectioned_data <- data.frame(
  study = c(
    "Primary Outcomes", "  Mortality", "  CV Events", "  Primary Subtotal",
    "Secondary Outcomes", "  Quality of Life", "  Hospitalizations"
  ),
  hr = c(NA, 0.78, 0.85, 0.82, NA, 0.92, 0.88),
  lower = c(NA, 0.65, 0.72, 0.73, NA, 0.80, 0.76),
  upper = c(NA, 0.94, 1.00, 0.92, NA, 1.06, 1.02),
  rtype = c("header", "data", "data", "summary",
            "header", "data", "data"),
  rbold = c(TRUE, FALSE, FALSE, TRUE, TRUE, FALSE, FALSE),
  rindent = c(0, 1, 1, 1, 0, 1, 1)
)

forest_plot(sectioned_data,
  point = "hr", lower = "lower", upper = "upper",
  label = "study", null_value = 1, scale = "log",
  columns = list(col_interval("HR (95% CI)")),
  row_type = "rtype", row_bold = "rbold", row_indent = "rindent",
  theme = web_theme_modern()
)

Highlight Specific Studies

Use badges and colors to draw attention:

Code
highlight_data <- data.frame(
  study = c("Landmark Trial", "Study B", "Study C", "Study D"),
  or = c(0.72, 0.85, 0.91, 0.88),
  lower = c(0.60, 0.70, 0.75, 0.72),
  upper = c(0.86, 1.03, 1.10, 1.07),
  is_bold = c(TRUE, FALSE, FALSE, FALSE),
  badge = c("Key", NA, NA, NA),
  color = c("#2563eb", NA, NA, NA)
)

forest_plot(highlight_data,
  point = "or", lower = "lower", upper = "upper",
  label = "study", null_value = 1,
  row_bold = "is_bold", row_badge = "badge", row_color = "color",
  theme = web_theme_modern()
)

Color Markers by Significance

Highlight significant results with green, non-significant with gray:

Code
sig_data <- data.frame(
  study = c("Trial A", "Trial B", "Trial C", "Trial D"),
  or = c(0.72, 0.95, 0.68, 1.05),
  lower = c(0.55, 0.78, 0.52, 0.86),
  upper = c(0.94, 1.15, 0.89, 1.28),
  pval = c(0.008, 0.42, 0.002, 0.65)
)

sig_data$marker_color <- ifelse(sig_data$pval < 0.05, "#16a34a", "#94a3b8")

forest_plot(sig_data,
  point = "or", lower = "lower", upper = "upper",
  label = "study", null_value = 1, scale = "log",
  marker_color = "marker_color",
  columns = list(col_pvalue("pval", "P")),
  theme = web_theme_modern(),
  footnote = "Green = significant (p<0.05), Gray = non-significant"
)

Different Shapes by Study Design

Use circles for RCTs and squares for observational studies:

Code
design_data <- data.frame(
  study = c("RCT-001", "RCT-002", "OBS-001", "OBS-002"),
  design = c("RCT", "RCT", "Observational", "Observational"),
  or = c(0.72, 0.78, 0.85, 0.82),
  lower = c(0.55, 0.62, 0.70, 0.68),
  upper = c(0.94, 0.98, 1.03, 0.99)
)

design_data$marker_shape <- ifelse(design_data$design == "RCT", "circle", "square")

forest_plot(design_data,
  point = "or", lower = "lower", upper = "upper",
  label = "study", group = "design",
  null_value = 1, scale = "log",
  marker_shape = "marker_shape",
  theme = web_theme_modern(),
  footnote = "Circles = RCTs, Squares = Observational studies"
)

Add Hover Tooltips

Show additional metadata when hovering over interval markers:

Code
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", null_value = 1, scale = "log",
  interaction = web_interaction(
    tooltip_fields = c("n", "events", "pvalue")
  ),
  theme = web_theme_modern(),
  footnote = "Hover over markers to see tooltip"
)

Tooltips are opt-in and display the row label, estimate with CI, and any additional fields you specify.

Use Dark Theme for Presentations

Code
forest_plot(meta_data[1:3, ],
  point = "hr", lower = "lower", upper = "upper",
  label = "study", null_value = 1, scale = "log",
  theme = web_theme_dark(),
  title = "Treatment Effect",
  subtitle = "For presentation slides"
)

Split by Subgroups

Create separate navigable plots for each subgroup value:

Code
set.seed(42)
subgroup_data <- data.frame(
  study = paste0("Study ", 1:20),
  region = sample(c("Americas", "Europe", "Asia"), 20, replace = TRUE),
  or = exp(rnorm(20, log(0.8), 0.25)),
  lower = NA, upper = NA
)
subgroup_data$lower <- subgroup_data$or * exp(-1.96 * 0.2)
subgroup_data$upper <- subgroup_data$or * exp(1.96 * 0.2)

forest_plot(subgroup_data,
  point = "or", lower = "lower", upper = "upper",
  label = "study",
  split_by = "region",  # Creates sidebar navigation
  scale = "log", null_value = 1,
  theme = web_theme_modern()
)

For hierarchical navigation (e.g., Sex > Age Group):

forest_plot(data,
  point = "or", lower = "lower", upper = "upper",
  label = "study",
  split_by = c("sex", "age_group"),  # Nested tree
  shared_axis = TRUE,  # Same scale across all subgroups
  scale = "log", null_value = 1
)

Export Split Forest to Directory

Save all subgroup plots as separate files:

split_result <- data |>
  web_spec(point = "or", lower = "lower", upper = "upper", label = "study") |>
  split_forest(by = c("sex", "age_group"))

# Creates: output/Male/Male_Young.svg, output/Male/Male_Old.svg, etc.
save_split_forest(split_result, "output", format = "svg")

Apply Styling with the Fluent API

Use set_*() functions to modify plots after creation:

Code
step_data <- data.frame(
  study = c("Trial A", "Trial B", "Trial C", "Overall"),
  hr = c(0.72, 0.85, 0.91, 0.82),
  lower = c(0.55, 0.70, 0.75, 0.68),
  upper = c(0.95, 1.03, 1.10, 0.99),
  is_summary = c(FALSE, FALSE, FALSE, TRUE),
  sig_color = c("#16a34a", "#94a3b8", "#94a3b8", "#2563eb"),
  study_shape = c("circle", "circle", "circle", "diamond")
)

forest_plot(step_data,
  point = "hr", lower = "lower", upper = "upper",
  label = "study", scale = "log", null_value = 1,
  theme = web_theme_modern()
) |>
  set_marker_style(color = "sig_color", shape = "study_shape") |>
  set_row_style(bold = "is_summary")

Create Plot Variants

Apply different styling to the same base plot:

Code
base_plot <- forest_plot(data,
  point = "hr", lower = "lower", upper = "upper",
  label = "study", scale = "log", null_value = 1
)

# Variant 1: Colored by significance
base_plot |> set_marker_style(color = "sig_color")

# Variant 2: Shaped by study type
base_plot |> set_marker_style(shape = "study_shape")

# Variant 3: Both
base_plot |> set_marker_style(color = "sig_color", shape = "study_shape")