How do I shift the geom_text labels to AFTER a geom_segment arrow in ggplot2?

Viewed 871

I have an NMDS ordination that I've plotted using ggplot2. I've added environmental vectors on top (from the envfit() function in vegan) using geom_segment() and added corresponding labels to the same coordinates as the segments using geom_text() (code below):

ggplot() +
  geom_point(data = nmds.sites.plot, aes(x = NMDS1, y = NMDS2, col = greening), size = 2) +
  labs(title = "Study Area", 
       col = "Sites") +
  geom_polygon(data = hull.data, aes(x = NMDS1, y = NMDS2, fill = grp, group = grp), alpha = 0.2) +
  scale_fill_discrete(name = "Ellipses", 
                      labels = c("High", "Moderate", "Control")) +
  xlim(c(-1, 1)) +
  guides(shape = guide_legend(order = 1), 
         colour = guide_legend(order = 2)) +
  geom_segment(data = env.arrows, 
               aes(x = 0, xend = NMDS1, y = 0, yend = NMDS2),
               arrow = arrow(length = unit(0.25, "cm")),
               colour = "black", inherit.aes = FALSE) +
  geom_text(data = env.arrows, aes(x = NMDS1, y = NMDS2, label = rownames(env.arrows))) +
  coord_fixed() +
  theme_bw() +
  theme(text = element_text(size = 14))

NMDS Ordination Output

However, since the labels are justified to centre, part of the label sometimes overlaps with the end of the arrow. I want to have the text START at the end of the arrow. In some other cases, if the arrow is pointing up, it pushes into the middle of the text. Essentially, I want to be able to see both the arrow head AND the text.

I have tried using geom_text_repel() from the ggrepel package but the placement seems random (and will also repel from other points or text in the plot (or just not do anything at all).

[EDIT] Below are the coordinates of the NMDS vectors (this is the env.arrows object from the example code above):

                NMDS1       NMDS2
Variable1 -0.46609087  0.27567532
Variable2 -0.21524887 -0.10128795
Variable3  0.59093184  0.03423775
Variable4 -0.00136418  0.46550043
Variable5 -0.30900813 -0.19659929
Variable6  0.53510347 -0.36387227
Variable7  0.66376246 -0.05220685
1 Answers

In the code below, we create a radial shift function to move the labels away from the arrows. The shift includes a constant amount plus an additional shift that varies with the absolute value of the cosine of the label's angle to the x-axis. This is because labels with theta near 0 or 180 degrees have a larger length of overlap with the arrows, and therefore need to be moved farther, than labels with theta near 90 or 270 degrees.

You may need to tweak the code a bit to get the labels exactly where you want them. Also, you'll likely need to add an additional adjustment if the variable names can have different widths.

One additional note: I've turned the variable names into a data column. You should do this with your data as well and then map that data column to the label argument of aes. Using rownames(env.arrows) for the labels reaches outside the ggplot function environment to the external data frame env.arrows and breaks the mapping to the data frame you've provided in the data argument to geom_text (although it likely won't cause a problem in this particular case).

library(tidyverse)
library(patchwork)

# data
env.arrows = read.table(text="    var            NMDS1       NMDS2
Variable1 -0.46609087  0.27567532
Variable2 -0.21524887 -0.10128795
Variable3  0.59093184  0.03423775
Variable4 -0.00136418  0.46550043
Variable5 -0.30900813 -0.19659929
Variable6  0.53510347 -0.36387227
Variable7  0.66376246 -0.05220685", header=TRUE)

# Radial shift function
rshift = function(r, theta, a=0.03, b=0.07) {
  r + a + b*abs(cos(theta))
}

# Calculate shift
env.arrows = env.arrows %>% 
  mutate(r = sqrt(NMDS1^2 + NMDS2^2),
         theta = atan2(NMDS2,NMDS1),
         rnew = rshift(r, theta),
         xnew = rnew*cos(theta),
         ynew = rnew*sin(theta))

p = ggplot() +
  geom_segment(data = env.arrows, 
               aes(x = 0, xend = NMDS1, y = 0, yend = NMDS2),
               arrow = arrow(length = unit(0.25, "cm")),
               colour = "black", inherit.aes = FALSE) +
  geom_text(data = env.arrows, aes(x = NMDS1, y = NMDS2, label = var)) +
  coord_fixed() +
  theme_bw() +
  theme(text = element_text(size = 14))

pnew = ggplot() +
  geom_segment(data = env.arrows, 
               aes(x = 0, xend = NMDS1, y = 0, yend = NMDS2),
               arrow = arrow(length = unit(0.2, "cm")),
               colour = "grey60", inherit.aes = FALSE) +
  geom_text(data = env.arrows, aes(x = xnew, y = ynew, label = var), size=3.5) +
  coord_fixed() +
  theme_bw() +
  theme(text = element_text(size = 14)) +
  scale_x_continuous(expand=expansion(c(0.12,0.12))) +
  scale_y_continuous(expand=expansion(c(0.07,0.07))) 

p / pnew

enter image description here

Related