1

I'm looking to add some annotations (ideally a text and an arrow) to a faceted ggplot outside the plot area.

What's that, you say? Hasn't someone asked something similar here, here and here? Well yes. But none of them were trying to do this below an x-axis with a log scale.

With the exception of this amazing answer by @Z.Lin — but that involved a specific package and I'm looking for a more generic solution.

At first glance this would appear to be a very niche question, but for those of you familiar with forest plots this may tweak some interest.

Firstly, some context... I'm interested in presenting the results of a coxph model using a forest plot in a publication. My goal here is to take the results of a model (literally a standalone coxph object) and use it to produce output that is customisable (gotta match the style guide) and helps translate the findings for an audience that might not be au fait with the technical details of hazard ratios. Hence the annotations and directional arrows.

Before you start dropping links to r packages/functions that could help do this... here are those that I've tried so far:

  • ggforestplotthis package produces lovely customisable forest plots (if you are using odds ratios), but it hard codes a geom_vline at zero which doesn't help for HR's
  • ggforestthis package is a nerd paradise of detail, but good luck a) editing the variable names and b) trying to theme it (I mentioned earlier that I'm working with a coxph object, what I didn't mention was that the varnames are ugly — they need to be changed for a punter to understand what we're trying to communicate)
  • finalfit offers a great workflow and its hr_plot kicks out some informative output, but it doesn't play nice if you've already got a coxph object and you just want to plot it

So... backstory out of the way. I've created my own framework for a forest plot below to which I'd love to add — in the space below the x-axis labels and the x-axis title — two annotations that help interpret the result. My current code struggles with:

  • repeating the code under each facet (this is something I'm trying to avoid)
  • mirroring the annotations of either side of the geom_vline with a log scale

Any advice anyone might have would be much appreciated... I've added a reproducible example below.

## LOAD REQUIRED PACKAGES

library(tidyverse)
library(survival)
library(broom)
library(ggforce)
library(ggplot2)

## PREP DATA

model_data <- lung %>%
  mutate(inst_cat = case_when(
    inst %% 2 == 0 ~ 2,
    TRUE ~ 1)) %>%
  mutate(pat.karno_cat = case_when(
    pat.karno < 75 ~ 2,
    TRUE ~ 1)) %>%
  mutate(ph.karno_cat = case_when(
    ph.karno < 75 ~ 2,
    TRUE ~ 1)) %>%
  mutate(wt.loss_cat = case_when(
    wt.loss > 15 ~ 2,
    TRUE ~ 1)) %>%
  mutate(meal.cal_cat = case_when(
    meal.cal > 900 ~ 2,
    TRUE ~ 1))

coxph_model <- coxph(
  Surv(time, status) ~
    sex + 
    inst_cat +
    wt.loss_cat +
    meal.cal_cat +
    pat.karno_cat +
    ph.karno_cat,
  data = model_data)

## PREP DATA

plot_data <- coxph_model %>%
  broom::tidy(
    exponentiate = TRUE, 
    conf.int = TRUE, 
    conf.level = 0.95) %>%
  mutate(stat_sig = case_when(
    p.value < 0.05 ~ "p < 0.05",
    TRUE ~ "N.S.")) %>%
  mutate(group = case_when(
    term == "sex" ~ "gender",
    term == "inst_cat" ~ "site",
    term == "pat.karno_cat" ~ "outcomes",
    term == "ph.karno_cat" ~ "outcomes",
    term == "meal.cal_cat" ~ "outcomes",
    term == "wt.loss_cat" ~ "outcomes"))

## PLOT FOREST PLOT

forest_plot <- plot_data %>%
  ggplot() +
  aes(
    x = estimate,
    y = term,
    colour = stat_sig) +
  geom_vline(
    aes(xintercept = 1),
    linetype = 2
  ) +
  geom_point(
    shape = 15,
    size = 4
  ) +
  geom_linerange(
    xmin = (plot_data$conf.low),
    xmax = (plot_data$conf.high)
  ) +
  scale_colour_manual(
    values = c(
      "N.S." = "black",
      "p < 0.05" = "red")
  ) +
  annotate(
    "text", 
    x = 0.45, 
    y = -0.2, 
    col="red", 
    label = "indicates y",
    ) +
  annotate(
    "text", 
    x = 1.5, 
    y = -0.2, 
    col="red", 
    label = "indicates y",
  ) +
  labs(
    y = "",
    x = "Hazard ratio") +
  coord_trans(x = "log10") +
  scale_x_continuous(
    breaks = scales::log_breaks(n = 7),
    limits = c(0.1,10)) +
  ggforce::facet_col(
    facets = ~group,
    scales = "free_y",
    space = "free"
  ) +
  theme(
    legend.position = "bottom",
    legend.title = element_blank(),
    strip.text = element_text(hjust = 0),
    axis.title.x = element_text(margin = margin(t = 25, r = 0, b = 0, l = 0))
  )

Created on 2022-05-10 by the reprex package (v2.0.1)

enter image description here

vengefulsealion
  • 756
  • 11
  • 18

1 Answers1

2

I think I would use annotation_custom here. This requires standard coord_cartesian with clip = 'off', but it should be easy to re-jig your x axis to use scale_x_log10

plot_data %>%
  ggplot() +
  aes(
    x = estimate,
    y = term,
    colour = stat_sig) +
  geom_vline(
    aes(xintercept = 1),
    linetype = 2
  ) +
  geom_point(
    shape = 15,
    size = 4
  ) +
  geom_linerange(
    xmin = (log10(plot_data$conf.low)),
    xmax = (log10(plot_data$conf.high))
  ) +
  scale_colour_manual(
    values = c(
      "N.S." = "black",
      "p < 0.05" = "red")
  ) +
  annotation_custom(
    grid::textGrob( 
    x = unit(0.4, 'npc'),
    y = unit(-7.5, 'mm'),
    label = "indicates yada",
    gp = grid::gpar(col = 'red', vjust = 0.5, hjust = 0.5))
  ) +
  annotation_custom(
    grid::textGrob( 
      x = unit(0.6, 'npc'),
      y = unit(-7.5, 'mm'),
      label = "indicates bada",
      gp = grid::gpar(col = 'blue', vjust = 0.5, hjust = 0.5))
  ) +
  annotation_custom(
    grid::linesGrob( 
      x = unit(c(0.49, 0.25), 'npc'),
      y = unit(c(-10, -10), 'mm'),
      arrow = arrow(length = unit(3, 'mm')),
      gp = grid::gpar(col = 'red'))
  ) +
  annotation_custom(
    grid::linesGrob( 
      x = unit(c(0.51, 0.75), 'npc'),
      y = unit(c(-10, -10), 'mm'),
      arrow = arrow(length = unit(3, 'mm')),
      gp = grid::gpar(col = 'blue'))
  ) +
  labs(
    y = "",
    x = "Hazard ratio") +
  scale_x_log10(
    breaks = c(0.1, 0.3, 1, 3, 10),
    limits = c(0.1,10)) +
  ggforce::facet_col(
    facets = ~group,
    scales = "free_y",
    space = "free"
  ) +
  coord_cartesian(clip = 'off') +
  theme(
    legend.position = "bottom",
    legend.title = element_blank(),
    strip.text = element_text(hjust = 0),
    axis.title.x = element_text(margin = margin(t = 25, r = 0, b = 0, l = 0)),
    panel.spacing.y = (unit(15, 'mm'))
  )

enter image description here

Allan Cameron
  • 147,086
  • 7
  • 49
  • 87
  • this is pretty great mate! Here's where I realised that my question may have missed a beat. Ideally I'd love the annotations to only appear once... any ideas for tweaking your code to achieve this? – vengefulsealion May 10 '22 at 13:44