vengefulsealion
vengefulsealion

Reputation: 766

How can I add an annotation to a faceted ggplot (with a log scale) outside the plot area

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:

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:

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

Upvotes: 1

Views: 496

Answers (1)

Allan Cameron
Allan Cameron

Reputation: 174586

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

Upvotes: 2

Related Questions