0

Consider the following two ways to draw diagonal lines, here shown on the same plot:

ggplot(iris,
       aes(x=Petal.Length,
           y=Petal.Width))+
  geom_point()+
  geom_abline() +
  geom_smooth(method = "lm", se = F)

enter image description here

However, I need a line which goes exactly from the bottom left corner of the plot (except the margin) to exactly the top right corner of the plot (except the margin). Here I show the line drawn in Paint to compare with the other two lines.

enter image description here

How can I draw this line without having to manually calculate the coordinates of the bottom left and the top right corner or the slope of the line?

J. Doe
  • 1,544
  • 1
  • 11
  • 26
  • Can you be more precise about where you want the red line to start and end? In some places it extends to the range of the data, in others it seems to the next gridline – George Savva May 22 '23 at 14:04
  • The red line doesn't seem to be exactly the bottom corner to the top corner. If you extend that line it won't hit the corners; the blue line would. For the lower left I assume you want the line to hit (0,0). It's less clear to me what you want the top right to hit. – MrFlick May 22 '23 at 14:04
  • You could save the plot, [extract its limits](https://stackoverflow.com/questions/7705345/how-can-i-extract-plot-axes-ranges-for-a-ggplot2-object), and later add the line based on those limits. This way it wouldn't go to the actual corners (like your example), but is indirect – Ricardo Semião e Castro May 22 '23 at 14:04

2 Answers2

4

There are three limits that you might want to use.

  1. The plot corners (green points);
  2. The last breaks (red points);
  3. The actual xlim's and ylim's (blue points).

enter image description here

"1." Can be achieved with Allan's annotation_custom answer. For "2." and "3." we must extract the limits and breaks. This can be done by acessing the plot metadata with ggplot_build(). I created custom functions that save that metadata in a geom_segment friendly format:

get_lims <- function(graph){
  ggplot_build(graph)$layout$panel_params[[1]][c('x.range', 'y.range')] %>%
    flatten() %>%
    set_names(c('x', 'xend', 'y', 'yend'))
}

The limits/ranges are on these ...$x.range and ...$y.range vectors, we are simply flattening them, and saving with the geom_segment's arguments names.

get_breaks <- function(graph){
  ggplot_build(graph)$layout$panel_params[[1]][c('x.sec', 'y.sec')] %>%
    map(~ c(.x$breaks, .x$minor_breaks) %>% na.omit() %>% {c(min(.), max(.))}) %>%
    flatten() %>%
    set_names(c('x', 'xend', 'y', 'yend'))
}

Here, the breaks info are under ...$x.sec$breaks and ...$x.sec$minor_breaks vectors (for major and minor breaks). Thus, the map here is joining major and minor (as any one of them could be the first/last), omitting NA's, and getting the minimal and maximal values. We then flatten and rename.

Lastly, you might be interested in expanding the red line of your example, which can be made by manually calculating a abline for it (line in purple).

Now to compare the methods:

g <- ggplot(iris, aes(x=Petal.Length, y=Petal.Width))+
  geom_point() +
  geom_smooth(method = "lm", se = F, color = 'black')

breaks <- get_breaks(g)
b <- (breaks$yend - breaks$y)/(breaks$xend - breaks$x)
a <- breaks$y - b*breaks$x

g +
  annotation_custom(grid::linesGrob(gp = grid::gpar(col = 'green'))) +
  geom_abline(slope = b, intercept = a, color = 'purple') +
  geom_segment(do.call(aes, get_breaks(g)), color = 'red') +
  geom_segment(do.call(aes, get_lims(g)), color = 'blue')

enter image description here

Obs: the points were added later.

2

It's actually pretty straightforward to get a line going exactly across the diagonal of the panel using annotation_custom with a grid::linesGrob and styling it using grid::gpar

library(ggplot2)

ggplot(iris, aes(x = Petal.Length, y = Petal.Width)) +
  geom_point() +
  annotation_custom(
    grid::linesGrob(gp = grid::gpar(col = 'red', lty = 2, lwd = 3)))

enter image description here

It is more difficult (and less well defined) to draw a diagonal that does not cross the "margins" of the plot, since these are not in fact margins, but just expansions of the plot area proper. The desired line you have drawn on your output is not a perfect diagonal across the panel, but a diagonal across the outermost breaks of your x axis and y axis scales. These are calculated by ggplot from your data at draw time, and can be changed by the user via scale_*_continuous, at which point your notion of the 'correct' diagonal might change. For example, if we change the breaks we end up changing the apparent desired diagonal:

ggplot(iris, aes(x = Petal.Length, y = Petal.Width)) +
  geom_point() +
  annotate('path', color = 'red', x = c(2.4, 6.2), y = c(0.4, 2.2)) +
  scale_x_continuous(breaks = c(2.4, 6.2)) +
  scale_y_continuous(breaks = c(0.4, 1.3, 2.2))

enter image description here

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