4

I would like to rotate the legends keys of geom_abline to a horizontal legend key or if possible based on their slope. Here is some reproducible code (dput df below):

library(ggplot2)
p = ggplot(df, mapping = aes(x = x, y = y)) +
  geom_point() +
  geom_abline(df, mapping = aes(slope = slope, intercept = 0, color = factor(group))) +
  coord_cartesian(xlim = c(0, 3), ylim = c(0, 3)) 
p

Created on 2023-04-19 with reprex v2.0.2

I tried using @Sandy Muspratt his answer here with grid functions, but that also rotates the ablines and the legend keys. So I was wondering if anyone knows how to rotate the legend keys of geom_abline?


dput df:

df<-structure(list(x = c(0, 0, 0), y = c(0, 0, 0), slope = c(0.5, 
1, 0.75), group = c("A", "B", "C")), class = "data.frame", row.names = c(NA, 
-3L))
Quinten
  • 35,235
  • 5
  • 20
  • 53
  • related https://stackoverflow.com/questions/72784232/defining-modifying-data-for-draw-key - same problem here, data$slope returns NULL, thus one cannot simply use data$slope for grid::segmentGrob. Annoying. – tjebo Apr 19 '23 at 19:37
  • 1
    @tjebo annoying but not insurmountable - see below – Allan Cameron Apr 19 '23 at 20:09

3 Answers3

5

The solution here lies in defining a new draw key. However, as Tjebo points out in the comments, it is difficult to customize the slopes this way because the data passed to the draw_key_* functions does not contain all the aesthetic data - only that necessary to make the standard draw keys (linewidth, colour, fill, etc).

One way round this is to take advantage of the fact that ggplots are built using the ggproto system, which itself is based on nested environments. A custom draw_key_* function can be written that looks up the appropriate ancestor frame to find the whole aesthetic data set:

draw_key_custom <- function (data, params, size) {
  colour <- data$colour 
  datalist <- get("data", parent.frame((10)))
  i <- which(sapply(datalist, function(x) "slope" %in% names(x)))[1]
  data <- datalist[[i]]
  data <- data[data$colour == colour,]
  slope <- data$slope
  intercept <- (1 - slope)/2
  y1 <- ifelse(abs(slope) > 1, (sign(slope) + 1)/2, intercept)
  y2 <- ifelse(abs(slope) > 1, 1 - (sign(slope) + 1)/2, intercept + slope)
  x1 <- ifelse(abs(slope) > 1, (y1 - intercept)/slope, 0)
  x2 <- ifelse(abs(slope) > 1, (y2 - intercept)/slope, 1)
  grid::segmentsGrob(x1, y1, x2, y2,
               gp = grid::gpar(col = data$colour, 
                               lwd = data$linewidth * 2))
}

This allows:

ggplot(df, mapping = aes(x = x, y = y)) +
  geom_point() +
  geom_abline(aes(slope = slope, intercept = 0, color = factor(group)),
              key_glyph = draw_key_custom) +
  coord_cartesian(xlim = c(0, 3), ylim = c(0, 3))

enter image description here

A couple of things I would point out are that:

  1. It is unusual and unnecessary to change slopes in a legend
  2. The resulting slopes will not necessarily match the slopes in the plot unless the aspect ratios of the legend keys fit the aspect ratio of the plotting panel. This ratio is not stable under plot resizing unless the aspect ratio is specified in theme
  3. The above implementation is fragile to changes within ggplot, and may not work if multiple lines have the same colour.

Still, nice to know it can be done I suppose...

Allan Cameron
  • 147,086
  • 7
  • 49
  • 87
3

Not a real answer to the question, and I understand that this was also more about proof of concept. And it is absolutely amazing to think of all the possibilities that arise from it. However, I believe it is important to offer an alternative especially for future readers who might have a fancy legend in mind although the arguably better solution might be:

No legend.

You could avoid a legend altogether by simply direct labelling, here basically improving on Allan's answer with Allan's very own geomtextpath package.

library(geomtextpath)

ggplot(df, mapping = aes(x = x, y = y)) +
  geom_point() +
  geom_textabline(aes(label = group, slope = slope, intercept = 0, color = group), hjust = .8) +
  coord_cartesian(xlim = c(0, 3), ylim = c(0, 3)) +
  theme(legend.position = "none")

tjebo
  • 21,977
  • 7
  • 58
  • 94
1

On the same site there is an answer and comment of @user20650. According to his answer here How to change angle of line in customized legend in ggplot2 and reading this https://ggplot2.tidyverse.org/reference/draw_key.html we could do it this way:

To make it work we have to replace lwd = data$size by lwd = 0.5

library(ggplot2)
library(grid)

library(ggplot2)
library(grid)
df<-structure(list(x = c(0, 0, 0), y = c(0, 0, 0), slope = c(0.5, 
1, 0.75), group = c("A", "B", "C")), class = "data.frame", row.names = c(NA, 
-3L))


GeomAbline$draw_key <- function(data, params, size) 
{
  segmentsGrob(0, 0.5, 1, 0.5,
               gp = gpar(col = alpha(data$colour, 
                                     data$alpha), 
                         lwd = 0.5* .pt, lty = data$linetype, 
                         lineend = "butt"))
}

ggplot(df, mapping = aes(x = x, y = y)) +
  geom_point() +
  geom_abline(df, mapping = aes(slope = slope, intercept = 0, color = factor(group))) +
  coord_cartesian(xlim = c(0, 3), ylim = c(0, 3)) 

enter image description here

TarJae
  • 72,363
  • 6
  • 19
  • 66
  • 2
    Although modifying your draw_key is the way to go, this does not do what the OP requests. One needs to use the slope aesthetic for segmentGrob, in particular, y1 will be equal to the slope in npc units. – tjebo Apr 19 '23 at 19:39
  • @tjebo thank you very much for your feedback. OP asks: "I would like to rotate the legends keys of geom_abline to a horizontal legend key or if possible based on their slope". I thought at least the part with the horizontal legend is answered?! – TarJae Apr 19 '23 at 19:42