Search code examples
rggplot2geom-text

Remove segment around label/text in ggplot2


Consider a plot with a segment/line and a text/label. I'd like the text to overlay the segment such that the text does not overlap the segment.

I tried using geom_label but I still want the background to be the same, just the other objects to be removed around the text. I also tried with geomtextpath but couldn't get the text to be horizontal. Any ideas?

seg <- data.frame(x = 1, xend = 1, y = 2, yend = 3)
plot1 <- ggplot(seg) +
  aes(x = x, xend = xend, y = y, yend = yend) +
  geom_segment() +
  geom_text(aes(y = (y + yend) / 2), label = "Hello", size = 10) +
  labs(title = "geom_text", 
       subtitle = "The text is horizontal but the segment should\nbe cut around the text.")

library(geomtextpath)
plot2 <- ggplot(seg) +
  aes(x = x, xend = xend, y = y, yend = yend) +
  geom_textsegment(label = "Hello", size = 10) +
  labs(title = "geomtextpath",
       subtitle = "The segment is correctly overlaid but\nI need the text to be horizontal")

seg2 <- data.frame(x = c(1, 1), xend = c(1, 1), y = c(2, 2.55), yend = c(2.45, 3))
plot3 <- ggplot(seg2) +
  aes(x = x, xend = xend, y = y, yend = yend) +
  geom_segment() +
  geom_text(aes(y = 2.5), label = "Hello", size = 10) +
  labs(title = "manual geom_segment", 
       subtitle = "This is the expected output, but it is done\nmanually. I need a scalable solution.")

library(patchwork)
plot1 + plot2 + plot3

enter image description here


Solution

  • Without writing a new Geom ggproto object (or adding this as a feature to geomtextpath), it will be difficult to get a fully functional geom layer. However, we can use geomtextpath to generate the broken line by making its text invisible, and getting the height of the break correct by shrinking the invisible text according to its width:height ratio. Then we just add a text label in the middle.

    Note that this means the x, y, xend, yend and label need to be passed rather than being mapped as aesthetics, so it acts more like an annotation layer than a true geom layer:

    library(geomtextpath)
    #> Loading required package: ggplot2
    
    geom_segment_text <- function(label = NULL, data = NULL, mapping = NULL,
                                  inherit.aes = TRUE, x, xend, y, yend, ...,
                                  size = 11/.pt, linecolour = "black") {
      
      df <- data.frame(x = x, y = y, xend = xend, yend = yend, label = label)
      ts <- textshaping::shape_text(label)$metric
      ratio <- ts$height / ts$width * 0.6
      list(
        geom_segment(aes(x, y, xend = xend, yend = yend), data = df, colour = NA),
        layer(geom = "text", stat = "identity", data = df, 
              mapping = aes((x + xend)/2, (y + yend)/2, label = label),
              position = "identity",
              params = list(size = size, ...), inherit.aes = inherit.aes),
        layer(geom = "textsegment", stat = "identity", data = df, 
              mapping = aes(x, y, xend = xend, yend = yend, label = label),
              position = "identity",
              params = list(colour = NA, size = size * ratio, 
                            linecolour = linecolour, padding = unit(0, "mm"), ...),
              inherit.aes = inherit.aes)
      )
    }
    

    This allows:

    ggplot() +
      geom_segment_text(label = "Hello", size = 10, x = 1, y = 2, xend = 1, yend = 3)
    

    We can see that the line breaks scale appropriately if the text size is changed. Crucially, because we are using geomtextpath, the spacing of the lines around the text remain constant if the image is resized:

    ggplot() +
      geom_segment_text(label = "Hello", size = 20, x = 1, y = 2, xend = 1, yend = 3)
    

    Created on 2022-10-18 with reprex v2.0.2