Search code examples
rggplot2ggrepel

How to nudge starting point of segments in geom_text_repel when axis is discrete?


library(tidyverse)
library(ggrepel)

df <- iris %>% 
  pivot_longer(starts_with("Sepal")) %>% 
  group_by(Species, name) %>% 
  summarise(
    y0 = min(value),
    y1 = max(value)
  ) 

I'm often using the ggrepel package for adding labels and annotating my ggplots, but sometimes the appearance can be a bit difficult to tweak. As an example for this question, I made the following stupid plot of the well known ìris data.

df %>% 
  ggplot(aes(x = name)) +
  geom_segment(
    aes(xend = name, y = y0, yend = y1, color = Species), size = 10
  ) +
  geom_text_repel(
    aes(label = Species, y = y1), direction = "y", nudge_x = .5
  )

I'm looking for a way to adjust the starting position of the ggrepel arrows such that they start at the right side of the segments, and not at the middle. One workaround would of course be to place the segments on top of the arrow:

df %>% 
  ggplot(aes(x = name)) +
  geom_text_repel(
    aes(label = Species, y = y1), direction = "y", nudge_x = .5
  ) +
  geom_segment(
    aes(xend = name, y = y0, yend = y1, color = Species), size = 10
  ) 

But that only works if you don't have any transparency, so i was wondering if there might be another solution. I guess I'm looking for a nudge_x_start or something like that.

EDIT: @stefan suggested using the point.padding parameter which works for the iris example, but unfortunately it doesn't work when the points are close to one another.

df2 <- enframe(month.name, "y0", "label") %>% 
  mutate(y1 = y0 + 1)

df2 %>% 
  ggplot(aes(x = "month")) +
  geom_segment(
    aes(xend = "month", y = y0, yend = y1, color = label), size = 10
  ) +
  geom_text_repel(
    aes(label = label, y = y0 + 0.5), direction = "y", nudge_x = 1/8,
    size = 5, point.padding = 1.25, hjust = 0
  ) +
  ylim(-12*2, 12*3)


Solution

  • Maybe this is what you are looking for. Even with a discrete axis ggplot2 uses numerics under the hood, i.e. first category is positioned at 1, second on 2, ... Therefore you could "nudge" the x inside aes as you did with the y. Do so you have to convert the categorical variable mapped on x to a numeric:

    library(tidyverse)
    library(ggrepel)
    
    df <- iris %>%
      pivot_longer(starts_with("Sepal")) %>%
      group_by(Species, name) %>%
      summarise(
        y0 = min(value),
        y1 = max(value)
      )
    #> `summarise()` has grouped output by 'Species'. You can override using the `.groups` argument.
    
    df %>%
      ggplot(aes(x = name)) +
      geom_segment(
        aes(xend = name, y = y0, yend = y1, color = Species),
        size = 10
      ) +
      geom_text_repel(
        aes(label = Species, y = y1, x = as.numeric(factor(name)) + .06),
        direction = "y", nudge_x = .5
      )
    

    df2 <- enframe(month.name, "y0", "label") %>% 
      mutate(y1 = y0 + 1)
    
    df2 %>% 
      ggplot(aes(x = "month")) +
      geom_segment(
        aes(xend = "month", y = y0, yend = y1, color = label), size = 10
      ) +
      geom_text_repel(
        aes(label = label, y = y0 + 0.5, x = as.numeric(factor("month")) + .025), direction = "y", nudge_x = .5,
        size = 5, hjust = 0
      ) +
      ylim(-12*2, 12*3) +
      guides(color = "none")