1

I have a plot for which I use polar coordinates. Now I would like to add some annotations to this plot, using straight arrows using geom_segment(). However, when I use coord_polar(), as expected these segments get transformed to the polar coordinate system too. That is, of course, the appropriate behaviour, but I would like to add some straight arrows (in the cartesian sense) to the plot. How can I best do that. These two questions, got me close, but not there (R: How to combine straight lines of polygon and line segments with polar coordinates? and Add line segments to histogram in ggplot2 with radar coordinates). For the solution for my plot, I cannot use coord_radar instead.

This works without coord_polar, but not with

library(tidyverse)
df <- tibble(x = rep(letters,  each = 5),
             y = rep(1:5, 26),
             d =  rnorm(26 * 5))


p1 <- ggplot() +
  geom_tile(data = df,
            aes(x = x,
                y = y,
                fill = d)) +
  ylim(c(-2, 5)) +
  geom_segment(
    aes(
      x = "o",
      y = -1,
      xend = "z",
      yend = 3
    ),
    arrow = arrow(length = unit(0.2, "cm")),
    col = "red",
    size = 2
  ) 
p1

enter image description here

p1 + coord_polar()

enter image description here

L Smeets
  • 888
  • 4
  • 17

1 Answers1

5

This is going to be a bit more of a pain than it might first appear, I'm afraid. Essentially, you'd have to write a new panel drawing method for the segments that ignores whether a coord system is linear or not. To do so, you can do the following, based on GeomSegment$draw_panel:

library(tidyverse)

geom_segment_straight <- function(...) {
  layer <- geom_segment(...)
  new_layer <- ggproto(NULL, layer)
  old_geom <- new_layer$geom
  geom <- ggproto(
    NULL, old_geom,
    draw_panel = function(data, panel_params, coord, 
                          arrow = NULL, arrow.fill = NULL,
                          lineend = "butt", linejoin = "round",
                          na.rm = FALSE) {
      data <- ggplot2:::remove_missing(
        data, na.rm = na.rm, c("x", "y", "xend", "yend", 
                               "linetype", "size", "shape")
      )
      if (ggplot2:::empty(data)) {
        return(zeroGrob())
      }
      coords <- coord$transform(data, panel_params)
      # xend and yend need to be transformed separately, as coord doesn't understand
      ends <- transform(data, x = xend, y = yend)
      ends <- coord$transform(ends, panel_params)
      
      arrow.fill <- if (!is.null(arrow.fill)) arrow.fill else coords$colour
      return(grid::segmentsGrob(
        coords$x, coords$y, ends$x, ends$y,
        default.units = "native", gp = grid::gpar(
          col = alpha(coords$colour, coords$alpha),
          fill = alpha(arrow.fill, coords$alpha),
          lwd = coords$size * .pt,
          lty = coords$linetype,
          lineend = lineend,
          linejoin = linejoin
        ),
        arrow = arrow
      ))
      
    }
  )
  new_layer$geom <- geom
  return(new_layer)
}

Then you can use it like any other geom.

ggplot() +
  geom_tile(data = df,
            aes(x = x,
                y = y,
                fill = d)) +
  ylim(c(-2, 5)) +
  geom_segment_straight(
    aes(
      x = "o",
      y = -1,
      xend = "z",
      yend = 3
    ),
    arrow = arrow(length = unit(0.2, "cm")),
    col = "red",
    size = 2
  ) + 
  coord_polar()

enter image description here

EDIT: geom_curve()

Here is the same trick applied to geom_curve():

geom_curve_polar <- function(...) {
  layer <- geom_curve(...)
  new_layer <- ggproto(NULL, layer)
  old_geom <- new_layer$geom
  geom <- ggproto(
    NULL, old_geom,
    draw_panel = function(data, panel_params, coord, 
                          curvature = 0.5, angle = 90, ncp = 5,
                          arrow = NULL, arrow.fill = NULL,
                          lineend = "butt", linejoin = "round",
                          na.rm = FALSE) {
      data <- ggplot2:::remove_missing(
        data, na.rm = na.rm, c("x", "y", "xend", "yend", 
                               "linetype", "size", "shape")
      )
      if (ggplot2:::empty(data)) {
        return(zeroGrob())
      }
      coords <- coord$transform(data, panel_params)
      ends <- transform(data, x = xend, y = yend)
      ends <- coord$transform(ends, panel_params)
      
      arrow.fill <- if (!is.null(arrow.fill)) arrow.fill else coords$colour
      return(grid::curveGrob(
        coords$x, coords$y, ends$x, ends$y,
        default.units = "native", gp = grid::gpar(
          col = alpha(coords$colour, coords$alpha),
          fill = alpha(arrow.fill, coords$alpha),
          lwd = coords$size * .pt,
          lty = coords$linetype,
          lineend = lineend,
          linejoin = linejoin
        ),
        curvature = curvature, angle = angle, ncp = ncp,
        square = FALSE, squareShape = 1, inflect = FALSE, open = TRUE,
        arrow = arrow
      ))
      
    }
  )
  new_layer$geom <- geom
  return(new_layer)
}

The above yields the following plot after replacing geom_segment_straight() with geom_curve_polar():

enter image description here

Small note: this way of making new geoms is the quick and dirty way of doing it. If you plan to do it properly, you should write the constructors and ggproto classes separately.

teunbrand
  • 33,645
  • 4
  • 37
  • 63
  • Perfect, this indeed does exactly what I need, which parameters would need to be changed to extend this to `geom_curve()`? – L Smeets Feb 14 '21 at 16:54
  • This is also a much better solution than the one proposed here: https://stackoverflow.com/a/51376386/10624798, which is basically just to overlay another plot – L Smeets Feb 14 '21 at 17:01
  • I guess you'd need to (1) replace `geom_segment(...)` with `geom_curve(...)`, (2) add curvature parameters (`curvature`, `angle`, `ncp`) to the formals of the `draw_panels` method, (3) use `grid::curveGrob` instead of `grid::segmentsGrob` and (4) pass the curvature parameters to that function, and in addition `square = FALSE, squareShape = 1, inflect = FALSE, open = TRUE` (see the `GeomCurve$draw_panel` method). – teunbrand Feb 14 '21 at 17:04
  • 1
    If you don't understand what I'm trying to say I could probably make an example. – teunbrand Feb 14 '21 at 17:06
  • that would be great, I am still wrapping my head around the ggplot internals. I tried a few things, but keep getting a `Error in is.unit(x1) : object 'trans' not found`error ` – L Smeets Feb 14 '21 at 17:15
  • No worries, I also sometimes don't understand ggplot2 internals. It's just that I understand them enough to work with them after trial and error. – teunbrand Feb 14 '21 at 17:20