Skip to contents

Introduction

The forest_plot() function creates a forest plot using the ggplot2 graphics package. The function returns both a plot and the ggplot2 code used to create the plot. In RStudio, the code used to create the plot will be shown in the Viewer pane (see [Plot code example] for an example).

Basic usage

Supply a data frame of estimates (by default, assumed to be log hazard ratios) and standard errors to the forest_plot() function:

my_results <- data.frame(
  subgroup   = c("men", "women", "35_49", "50_64", "65_79"),
  est        = c( 0.45,    0.58,    0.09,    0.35,     0.6),
  se         = c( 0.07,    0.06,    0.06,    0.05,    0.08)
)

forest_plot(my_results)

Use col.est and col.stderr to set the columns that contain estimates and standard errors. By default, the function will look for columns with names estimate/est/beta/loghr and stderr/std.err/se. If you want to supply confidence interval limits, set col.lci and col.uci.

If your estimates are not on the log scale, then set exponentiate=FALSE.

Row labels

Set col.key to identify the rows of the forest plot.

forest_plot(my_results, col.key = "subgroup")

my_row_labels <- data.frame(
  subgroup = c("men", "women", "35_49", "50_64", "65_79"),
  label    = c("Men", "Women", "35 - 49", "50 - 64", "65 - 79")
)

forest_plot(my_results,
            col.key    = "subgroup",
            row.labels = my_row_labels)

To quickly add subheadings, include labels with a missing col.key:

row_labels <- data.frame(
  subgroup = c(NA, "men", "women",
               NA, "35_49", "50_64", "65_79"),
  label    = c("Sex", "Men", "Women",
               "Age (years)", "35 - 49", "50 - 64", "65 - 79")
)

forest_plot(my_results,
            col.key    = "subgroup",
            row.labels = row_labels)

To automatically create groupings and add subheadings, use multiple columns in the row.labels data frame.

row_labels <- data.frame(
  subgroup = c("men", "women",
               "35_49", "50_64", "65_79"),
  group    = c("Sex", "Sex",
               "Age (years)", "Age (years)", "Age (years)"),
  label    = c("Men", "Women",
               "35 - 49", "50 - 64", "65 - 79")
)

forest_plot(my_results,
            col.key = "subgroup",
            row.labels = row_labels)

Use the row.labels.levels argument to choose columns for row labels and the hierarchy for grouping. (Otherwise, all character columns in the row labels data frame will be used.)

forest_plot(my_results,
            col.key           = "subgroup",
            row.labels        = row_labels,
            row.labels.levels = c("label"))

The order of rows is set by the row.labels data frame.

row_labels <- data.frame(
  subgroup = c("women", "men",
               "65_79", "50_64", "35_49"),
  group    = c("Sex", "Sex",
               "Age (years)", "Age (years)", "Age (years)"),
  label    = c("Women", "Men",
               "65 - 79", "50 - 64", "35 - 49")
)

forest_plot(my_results,
            col.key           = "subgroup",
            row.labels        = row_labels)

Add a heading above the row labels with row.labels.heading:

forest_plot(my_results,
            col.key            = "subgroup",
            row.labels         = row_labels,
            row.labels.heading = "Subgroup")

Multiple panels

my_resultsA <- my_results

my_resultsB <- data.frame(
  subgroup   = c("men", "women", "35_49", "50_64", "65_79"),
  est        = c(0.48, 0.54, 0.06, 0.3, 0.54),
  se         = c(0.12, 0.11, 0.11, 0.09, 0.15)
)

forest_plot(list("Observational" = my_resultsA,
                 "Genetic"       = my_resultsB),
            col.key    = "subgroup",
            row.labels = row_labels)

You can use split() to create a list of data frames from a single data frame:

my_resultsAB <- data.frame(
  analysis = factor(c(rep("Observational", 5), rep("Genetic", 5)),
                    levels = c("Observational", "Genetic")),
  subgroup = c("men", "women", "35_49", "50_64", "65_79",
               "men", "women", "35_49", "50_64", "65_79"),
  est      = c( 0.45, 0.58, 0.09, 0.35, 0.6,
                0.48, 0.54, 0.06, 0.3, 0.54),
  se        = c(0.07, 0.06, 0.06, 0.05, 0.08,
                0.12, 0.11, 0.11, 0.09, 0.15)
)

forest_plot(split(my_resultsAB, ~ analysis),
            col.key    = "subgroup",
            row.labels = row_labels)

Adding columns of text

Use col.left and col.right to add columns of text either side of each panel. Use col.left.heading and col.right.heading to customise the column headings.

my_results$n <- c(834, 923, 587, 694, 476)

forest_plot(my_results,
            col.key          = "subgroup",
            row.labels       = row_labels,
            col.left         = "n",
            col.left.heading = "No. of events")

Use col.left.hjust and col.right.hjust to set the horizontal justification of the columns (0 = left, 0.5 = center, 1 = right).

Scaling point size

Set scalepoints = TRUE to have point size (area) proportional to the inverse of the variance (SE2) of the estimate.

forest_plot(my_results,
            col.key     = "subgroup",
            row.labels  = row_labels,
            scalepoints = TRUE)

To have consistent scaling across plots, set minse to the same value (it must be smaller than the smallest SE). This will ensure the same size scaling is used across the plots.

Confidence interval lines

Narrow confidence interval lines can be hidden by points. Set the panel.width argument to change the appearance of narrow confidence interval lines. The function will by default try to change the colour and plotting order of confidence intervals so that they are not hidden. You can also supply vectors and lists to the cicolour argument to have more control.

Note that the calculations for identifying narrow confidence intervals has has been designed to work for shapes 15/‘square’ (the default) and 22/‘square filled’, and for symmetric confidence intervals. These may not be completely accurate in all scenarios, so check your final output carefully.

forest_plot(split(my_resultsAB, ~ analysis),
            col.key     = "subgroup",
            row.labels  = row_labels,
            scalepoints = TRUE,
            pointsize   = 8,
            xlim        = c(0.5, 3),
            xticks      = c(0.5, 1, 2, 3),
            panel.width = unit(28, "mm"))

forest_plot(split(my_resultsAB, ~ analysis),
            col.key     = "subgroup",
            row.labels  = row_labels,
            scalepoints = TRUE,
            pointsize   = 10,
            xlim        = c(0.5, 8),
            
            shape       = "square filled",
            stroke      = 0.5,
            fill        = list("black", "white"),
            panel.width = unit(28, "mm"))

Different limits on panels

forest_plot() uses ggplot facets to place forest plots side-by-side. Facets cannot easily have different scales applied, but you can use forest_plot() for each panel then arrange them side-by-side.

If xlim, xticks and panels are lists of the same length, then forest_plot() will do this automatically. The function will return a list containing “figure” (a graphic object of the whole figure) and “plots” (a list of ggplots, one for each panel).

forest <- forest_plot(split(my_resultsAB, ~ analysis),
                      col.key = "subgroup",
                      row.labels = row_labels,
                      xlim = list(c(0.5, 3 + 1e-10),
                                  c(0.1, 4)),
                      xticks = list(c(0.5, 1, 2, 3),
                                    c(0.1, 1, 2, 4)),
                      xlab = c("Hazard Ratio (95% CI)", "Odds Ratio (95% CI)"),
                      col.right.heading = list("HR (95% CI)", "OR (95% CI)"))
grid::grid.newpage()
grid::grid.draw(forest$figure)

Use grid::grid.draw() to draw the figure (use grid::grid.newpage() to clear), and ggsave() or save_figure() to save to a file.

Warnings: If scalepoints = TRUE (and minse is not specified the same for each plot) then this scaling will be on a panel-by-panel basis so box sizes are not comparable between panels.

Adding heterogeneity and trend test results

The addtext argument can be used to add results of heterogeneity or trend tests, or some other text, in the text column of estimates and CIs. It needs to be a list of data frames, the same length as panels. Data frames should contain a column with the name specified in col.key, and one or more of:

  • a column named ‘text’ containing character strings
  • a column named ‘expr’ containing character strings that will be parsed into expressions and displayed as described in ?plotmath
  • columns named ‘het_dof’, ‘het_stat’, and ‘het_p’ containing character strings
  • columns names ‘trend_stat’ and ‘trend_p’ containing character strings
het_trend_results <- data.frame(
 analysis   = factor(c("Observational", "Observational", "Observational", "Genetic", "Genetic", "Genetic"),
 levels     = c("Observational", "Genetic")),
 subgroup   = c( "men", "35_49", "35_49", "men", "35_49", "35_49"),
 het_dof    = c( "1", NA, NA, "1", NA, NA),
 het_stat   = c( "1.99", NA, NA, "0.136", NA, NA),
 het_p      = c("=0.16", NA, NA, "=0.71", NA, NA),
 trend_stat = c( NA, "27.2", NA, NA, "6.98", NA),
 trend_p    = c( NA, "<0.001", NA, NA, "=0.008", NA),
 text       = c( NA, NA, NA, NA, NA, "Note"),
 expr       = c(NA, NA, "frac(-b %+-% sqrt(b^2 - 4*a*c), 2*a)", NA, NA, NA)
)
forest_plot(split(my_resultsAB, ~ analysis),
            col.key     = "subgroup",
            row.labels  = row_labels,
            scalepoints = TRUE,
            pointsize   = 8,
            xlim        = c(0.5, 3),
            xticks      = c(0.5, 1, 2, 3),
            panel.width = unit(28, "mm"),
            right.space = unit(45, "mm"),
            addtext     = split(het_trend_results, ~ analysis))

Note that values should all be character strings, and P-values should include the necessary “=” or “<”.

The automatic positioning of columns and spacing of panels does not take into account this additional text, so you may need to use the right.space and col.right.pos arguments for a satisfactory layout.

Customisation

See Customising plots for more ways to customise forest plots.

Notes

Spacing

The function attempts to set the positions of columns of text and spacing automatically. Where this does not produce a satisfactory layout, you can use the arguments col.left.pos, col.right.pos, left.space, right.space, and mid.space to control positions and spacing manually.

The plot will fill the vertical space available. Use plot.margin to change the top and bottom margins as needed.

Confidence intervals

When standard errors are supplied to the shape_plot() and forest_plot() functions, confidence intervals are calculated as 95% confidence intervals using the Normal approximation method (with critical value 1.96).

Stroke

The stroke argument sets the stroke aesthetic for plotted shapes. See https://ggplot2.tidyverse.org/articles/ggplot2-specs.html for more details. The stroke size adds to the total size of a shape, so unless stroke = 0 the scaling of size by inverse variance will be very slightly inaccurate (but there are probably more important things to worry about).