Post-editing Forest Plot

Alimu Dayimu

2026-04-26

Post-editing a Forest Plot

The forestploter package creates plots where all elements are placed in cells. This structure makes it easy to edit any element by specifying its row and column. This vignette demonstrates how to post-edit a forest plot to achieve a publication-ready figure.

The plotting steps demonstrated in this vignette may not necessarily be optimal. There are other R packages that may be better suited for the plots demonstrated here. Please choose the one that best suits your needs. The final plot is shown below:

Drawing a Forest Plot

We will start by drawing a forest plot using an example from the metafor package, which is similar to the plot shown above. First, we load the data and create a simple forest plot.

library(grid)
library(forestploter)

# Read meta-analysis example data
dt <- read.csv(system.file("extdata", "metadata.csv", package = "forestploter"))

str(dt)
#> 'data.frame':    9 obs. of  16 variables:
#>  $ author : chr  "Amore-Coffea 2000" "Deliciozza 2004" "Kahve-Paradiso 2002" "Mama-Kaffa 1999" ...
#>  $ ai     : int  2 10 0 12 3 19 4 8 NA
#>  $ n1i    : int  31 40 0 53 15 68 35 35 277
#>  $ ci     : int  10 9 0 9 1 9 2 6 NA
#>  $ n2i    : int  34 40 0 61 17 64 37 37 290
#>  $ weights: num  10.19 18.37 NA 19.91 5.37 ...
#>  $ orci   : chr  "0.17 [0.03, 0.83]" "1.15 [0.41, 3.22]" "Not Estimated" "1.69 [0.65, 4.40]" ...
#>  $ rb.a   : chr  "?" "+" "-" "-" ...
#>  $ rb.b   : chr  "?" "?" "-" "-" ...
#>  $ rb.c   : chr  "?" "?" "?" "?" ...
#>  $ rb.d   : chr  "?" "?" "+" "-" ...
#>  $ rb.e   : chr  "-" "-" "+" "-" ...
#>  $ rb.f   : chr  "+" "+" "+" "+" ...
#>  $ est    : num  0.166 1.148 NA 1.691 4 ...
#>  $ lb     : num  0.033 0.409 NA 0.65 0.369 ...
#>  $ ub     : num  0.829 3.219 NA 4.4 43.383 ...

# Prepare a blank column for the CI
dt$cicol <- paste(rep(" ", 20), collapse = " ")

# Select some columns for plotting; this will serve as the skeleton of the forest plot
dt_fig <- dt[, c(1:7, 17, 8:13)]

colnames(dt_fig) <- c("Study or Subgroup",
                      "Events", "Total", "Events", "Total",
                      "Weight",
                      "", "",
                      LETTERS[1:6])

dt_fig$Weight <- sprintf("%0.1f%%", dt_fig$Weight)
dt_fig$Weight[dt_fig$Weight == "NA%"] <- ""

# Convert NA to a blank string
dt_fig[is.na(dt_fig)] <- ""

# Set background to white and summary diamond to black
tm <- forest_theme(core = list(bg_params = list(fill = c("white"))),
                   summary_col = "black",
                   arrow_label_just = "end",
                   arrow_type = "closed")

p <- forest(dt_fig,
            est = dt$est,
            lower = dt$lb, 
            upper = dt$ub,
            sizes = sqrt(dt$weights / 100),
            is_summary = c(rep(FALSE, nrow(dt) - 1), TRUE),
            ci_column = 8,
            ref_line = 1,
            x_trans = "log",
            arrow_lab = c("Favours caffeine", "Favours decaf"),
            xlim = c(0.05, 100),
            ticks_at = c(0.1, 1, 10, 100),
            theme = tm)
p

Editing the Forest Plot

The forestploter package provides several functions to modify a forest plot. These functions allow you to edit various aspects of the plot:

Editing the Plot

Below, we will make the “Total” row text bold, change the color of the diamond shape, and modify the background color of the “Total” row. We will also align the text in the last six columns to the center.

# Change font face
g <- edit_plot(p, row = 9, 
               gp = gpar(fontface = "bold"))

# Change color
g <- edit_plot(g, col = 8, row = 9, which = "ci", 
               gp = gpar(col = "blue", fill = "blue"))

# Change the background of the total row
# You need to change both fill and col if you don't want to see a gap between cells
g <- edit_plot(g, col = 1:7, 
               row = 9, 
               which = "background", 
               gp = gpar(fill = "#f6eff7", col = "#f6eff7"))

# Align text to center
g <- edit_plot(g, col = 9:14, 
               which = "text",
               hjust = unit(0.5, "npc"),
               x = unit(0.5, "npc"))
g

For text alignment: - hjust = unit(0, "npc") and x = unit(0, "npc") align text to the left. - hjust = unit(0.5, "npc") and x = unit(0.5, "npc") center-align text. - hjust = unit(1, "npc") and x = unit(0.9, "npc") align text to the right.

Adding and Inserting Text

In this step, we will add text to the header and display the total number of events from the data.

# Add or insert some text to the header on top of CI columns
g <- add_text(g, text = "IV, Random, 95% CI",
              part = "header", 
              col = 7:8,
              gp = gpar(fontface = "bold"))

g <- insert_text(g, text = "Odds ratio",
                 part = "header", 
                 col = 7:8,
                 gp = gpar(fontface = "bold"))

# Group outcomes
g <- add_text(g, text = "Caffeine",
              part = "header",
              row = 1,
              col = 2:3,
              gp = gpar(fontface = "bold"))

g <- add_text(g, text = "Decaf",
              part = "header", 
              row = 1,
              col = 4:5,
              gp = gpar(fontface = "bold"))

# Add text on the top of the risk of bias data
g <- add_text(g, text = "Risk of Bias",
              part = "header", 
              row = 1,
              col = 9:14,
              gp = gpar(fontface = "bold"))

# Insert event count
g <- insert_text(g, 
                 text = c("Total events:"),
                 row = 9,
                 col = 1,
                 before = FALSE,
                 just = "left")

# Note: The row counts need to add one to account for 
# `insert_text` in the previous step
g <- add_text(g, text = "58",
              col = 2,
              row = 10,
              just = "left")

g <- add_text(g, text = "46",
              col = 4,
              row = 10,
              just = "left")

g

Adding Borders

In this step, we will add borders to the header. By default, add_border adds a border to the bottom of the specified cell(s).

# Add or insert some text to the header
g <- add_border(g, 
                part = "header", 
                row = 1,
                col = 9:14,
                gp = gpar(lwd = .5))

g <- add_border(g, 
                part = "header", 
                row = 2,
                gp = gpar(lwd = 1))

g

Adding Grobs

In the next step, we will add a rounded rectangle with a dashed line around the risk of bias data. Then, we will draw circle grobs with different colors at the bottom of the text.

g <- add_grob(g,
              row = 1:(nrow(dt_fig) - 1),
              col = 9:14,
              order = "background",
              gb_fn = roundrectGrob,
              r = unit(0.05, "snpc"),
              gp = gpar(lty = "dotted",
                        col = "#bdbdbd"))

# Draw a circle grob; you can also draw a `pointsGrob`
cols <- c("#eeee00", "#00cc00", "#cc0000")
symb <- c("?", "+", "-")
for(i in seq_along(symb)){
  pos <- which(dt_fig == symb[i], arr.ind = TRUE)
  for(j in 1:nrow(pos)){
    g <- add_grob(g, 
                  row = pos[j, 1], 
                  col = pos[j, 2],
                  order = "background",
                  gb_fn = circleGrob,
                  r = 0.4,
                  gp = gpar(fill = cols[i]))
  }
}

g

The text we want to create involves math expressions and multiple lines. While this can be done with add_text by setting parse = TRUE, we can use the code below to achieve our desired result. The line break is based on a solution found here, which uses the atop function. You can also use the latex2exp package for math expressions.

txt <- bquote(atop(paste("Heterogeneity: ", tau^2, " = 0.22; ",
                         chi^2, " = 9.39, df = 6 (P = 0.15) ",
                         I^2, " = 36%"),
            "Test for overall effect: Z = 1.15 (P = 0.25)"))

add_text(g, text = txt,
         col = 1:6,
         row = 11,
         just = "left",
         parse = TRUE,
         gp = gpar(fontsize = 8))

As you can see, the second line is not left-aligned. To address this, we can use add_grob to leverage other packages that create grob objects. In this case, we will use the gridtext package to generate complex text and add it to the plot. More details about this package can be found here. With gridtext, you can write and render Markdown or HTML text in your plot. However, it does not currently support math equations. For Unicode math symbols, you can refer to this resource here.

txt <- "Heterogeneity: &tau;<sup>2</sup> = 0.22; &chi;<sup>2</sup> = 9.39,
df = 6 (P = 0.15); I<sup>2</sup> = 36%<br><span style='color:blue'>**Test for overall effect:**</span> Z = 1.15 (P = 0.25)"

add_grob(g, 
         row = 11, 
         col = 1:6,
         order = "background",
         gb_fn = gridtext::richtext_grob,
         text = txt,
         gp = gpar(fontsize = 8),
         hjust = 0, vjust = 1, halign = 0, valign = 1,
         x = unit(0, "npc"), y = unit(1, "npc"))