Search code examples
rggplot2

geom_segment shows up in the wrong place with a logarithmic scale


I noticed some strange behavior when using geom_segment and scale_y_log10 simultaneously. The following should plot an arrow from (2,1) to (4,1).

ggplot(data.frame(x = 1, y = 1), aes(x, y)) +
  geom_point() +
  coord_cartesian(clip = 'off') +
  scale_x_continuous(limits = c(1, 5)) +
  scale_y_log10(breaks = c(0.1, 1, 10, 100), limits = c(0.1, 100)) +
  geom_segment(x = 2, y = 1, xend = 4, yend = 1,
               arrow = arrow(length = unit(0.5, 'cm')))

In reality, it plots an arrow from (2,10) to (4,10).

plot output

After doing a little experimentation, it seems the log transformation is not applied to geom_segment, so for whatever y value you input to geom_segment the segment will actually be plotted at 10^y. So, I have two questions:

  1. Why does it do this?
  2. How do I fix it? (Preferably something better than taking log10() of every y value I pass.)

Solution

  • The issue has to do with the steps ggplot2 takes to take data, transform it into stats (if necessary), and then map to the scales. From the help for ?aes_eval: "ggplot2 has three stages of the data that you can map aesthetics from," and what you're doing skips to the last one, missing the step of translating the input to the mapping.

    When you specify geom_segment(y = 1...) in a plot with scale_y_log10(), ggplot2 understands that to mean the data belongs at 10^1. In ggplot2 terminology, the data is treated as if it were in the after_scale() step.

    What you intended, though, was for your data to be mapped to that scale, ie you wanted your y = 1 to be treated as 10^0. If you put it inside aes(), it will do that, transforming the data value of 1 to be mapped to the corresponding point on the scale.

    We can see that using aes(after_scale(y=1...)) will bring back the unintended behavior.

    https://search.r-project.org/CRAN/refmans/ggplot2/html/aes_eval.html

    https://www.reddit.com/r/rprogramming/comments/tsspot/how_does_control_aesthetic_evaluation_work/

    ggplot(data.frame(x = rep(1,3), y = rep(1,3)), aes(x, y)) +
      geom_point() +
      coord_cartesian(clip = 'off') +
      scale_x_continuous(limits = c(1, 5)) +
      scale_y_log10(breaks = c(0.1, 1, 10, 100), limits = c(0.1, 1000)) +
    
      # raw data will be treated "after_scale"
      geom_segment(x = rep(2,3), y = rep(1,3), xend = rep(4,3), yend = 1:3,
                   arrow = arrow(length = unit(0.2, 'cm'))) +
    
      # data inside  aes(after_scale(...   will be treated "after_scale"
      geom_segment(aes(x = 2, y = 1, xend = 4, yend = after_scale(1:3)),
                   color = "blue",
                   arrow = arrow(length = unit(0.2, 'cm'))) +
    
      # but data inside aes() will be mapped to the scale as we want
      geom_segment(aes(x = 2, y = 1, xend = 4, yend = 1:3),
                   color = "red",
                   arrow = arrow(length = unit(0.2, 'cm'))) 
    

    enter image description here