Search code examples
rggplot2annotationslabelfacet-wrap

Displaying annotation over facet_wrap label


I am trying to add labels (a, b, c...) to a plot using facet_wrap, outside the margins of the actual plot. I'd like to have them in the label area, but separate from the facet_wrap label (which is the name of the data group). I can push the text outside the border of the plot using coord_cartesian(clip = "off"), but the label background created by facet_wrap blocks the annotation. I can suppress the label background using theme(strip.background = element_blank()), which makes the annotation visible, but aesthetically I would prefer having the background. Is there any way to make the geom_text annotation appear over, rather than under, the facet_wrap label?

Here is an example:

df <- data.frame(x=rep(1:3, 4), mydat=rep(c('apple', 'kiwi', 'orange', 'pear'), each=3))
labels <- data.frame(mydat=as.factor(c('apple', 'kiwi', 'orange', 'pear')), 
                     label=paste0('(', letters[1:4], ')'))

ggplot(df, aes(x,x)) +
  facet_wrap(~mydat) +
  geom_point() +
  geom_text(data = labels, aes(label=label), #geom_text or geom_label
            x = -Inf, y = Inf, hjust=0, vjust=0,
            inherit.aes = FALSE) + 
  #theme(strip.background = element_blank()) +
  coord_cartesian(clip = "off")

With the facet_wrap background: enter image description here

Without the facet_wrap background: enter image description here


Solution

  • One option would be the ggh4x package which offers some useful extensions of the default facets, e.g. using ggh4x::facet_wrap2 you could add you annotations as a second layer of facets. Additionally ggh4x::facet_wrap2 allows style the two layers separately via the strip argument:

    df <- data.frame(x = rep(1:3, 4), mydat = rep(c("apple", "kiwi", "orange", "pear"), each = 3))
    labels <- data.frame(
      mydat = as.factor(c("apple", "kiwi", "orange", "pear")),
      label = paste0("(", letters[1:4], ")")
    )
    
    df <- merge(df, labels, by = "mydat")
    
    library(ggplot2)
    library(ggh4x)
    
    ggplot(df, aes(x, x)) +
      facet_wrap2(vars(label, mydat),
        strip = strip_themed(
          background_x = list(
            element_blank(),
            element_rect(fill = "grey85")
          ),
          text_x = list(
            element_text(hjust = 0),
            element_text()
          ),
          by_layer_x = TRUE
        )
      ) +
      geom_point() +
      coord_cartesian(clip = "off")
    

    UPDATE As you mentioned in your comment you want the annotations as part of the strip label to save vertical space. IMHO the cleanest way to do this would be to add the annotations as part of the facet label. Just as an exercise in manipulating the gtable I wrote a custom function which allows to tag the facets with letters a, b, ...:

    library(ggplot2)
    library(grid)
    
    g <- ggplot(df, aes(x, x)) +
      facet_wrap(~mydat) +
      geom_point() +
      coord_cartesian(clip = "off")
    
    add_tag <- function(gg) {
      # Indices of strips in gtable
      ix_strips <- grep('strip', gg$layout$name)
      strips <- gg$grobs[ix_strips]
      # Strip positions
      strip_rc <- gsub("^.*?(\\d+)\\-(\\d+)$", "\\2-\\1", gg$layout$name[ix_strips])
      
      tag_order <- order(strip_rc)
    
      gg$grobs[ix_strips] <- mapply(function(x, y) {
        tag <- x$grobs[[1]]$children[[2]]
        tag$children[[1]]$label <- paste0("(", letters[[y]], ")")
        tag$children[[1]]$hjust <- 0
        tag$children[[1]]$x <- unit(0, "npc")
        
        x$grobs[[1]]$children[[2]] <- gList(
          tag, x$grobs[[1]]$children[[2]]   
        )
        
        x
      }, strips, tag_order)
      
      gg
    }
    
    gg = ggplotGrob(g)
    
    gg <- add_tag(gg)
    
    grid.draw(gg)