4

Is it possible to draw an arrowhead in the middle of an edge using ggraph::geom_edge_link(), and if so how can this be done?

Rather than something like this with the arrowheads drawn at the ends of the edges:

library(ggraph)
library(tidygraph)
library(dplyr)

create_notable('bull') %>%
  ggraph(layout = 'graphopt') + 
  geom_edge_link(arrow = arrow(length = unit(4, 'mm')), 
                 end_cap = circle(3, 'mm')) +  
  geom_node_point(size = 5) +
  theme_graph()

Rplot.png

I'd like to be able to achieve something like this:

Rplot01.png

I've checked the ggraph::geom_edge_link() and grid::arrow() documentation but couldn't see anything obvious about how to do this.

Z.Lin
  • 28,055
  • 6
  • 54
  • 94
Mark_1
  • 331
  • 1
  • 3
  • 16
  • The answer [here](https://stackoverflow.com/a/55141455/8449629) may be relevant. – Z.Lin Oct 04 '19 at 04:43
  • Thanks. Yes, I've come across that idea of drawing half a line with an arrow and then the other half of the line, but I'm not sure how to do that in a graph context. Also my real graph has a lot more edges so I can't really draw each one individually as they did with the line segments in that example. – Mark_1 Oct 04 '19 at 09:59

1 Answers1

3

I haven't used the ggraph package myself, but based on my understanding of the underlying grobs, you can try the following:

Step 1. Run the following line in your console:

trace(ggraph:::cappedPathGrob, edit = TRUE)

Step 2. In the pop-up window, change the last chunk of code from this:

if (is.null(start.cap) && is.null(end.cap)) {
  if (constant) {
    grob(x = x, y = y, id = id, id.lengths = NULL, arrow = arrow, 
         name = name, gp = gp, vp = vp, cl = "polyline")
  }
  else {
    grob(x0 = x[!end], y0 = y[!end], x1 = x[!start], 
         y1 = y[!start], id = id[!end], arrow = arrow, 
         name = name, gp = gp, vp = vp, cl = "segments")
  }
} else {
  gTree(x = x, y = y, id = id, arrow = arrow, constant = constant, 
        start = start, end = end, start.cap = start.cap, 
        start.cap2 = start.cap2, start.captype = start.captype, 
        end.cap = end.cap, end.cap2 = end.cap2, end.captype = end.captype, 
        name = name, gp = gp, vp = vp, cl = "cappedpathgrob")
}

To this:

if(is.null(arrow)) {
  # same code as before, if no arrow needs to be drawn
  if (is.null(start.cap) && is.null(end.cap)) {
    if (constant) {
      grob(x = x, y = y, id = id, id.lengths = NULL, arrow = arrow, 
           name = name, gp = gp, vp = vp, cl = "polyline")
    }
    else {
      grob(x0 = x[!end], y0 = y[!end], 
           x1 = x[!start], y1 = y[!start], 
           id = id[!end], arrow = arrow, 
           name = name, gp = gp, vp = vp, cl = "segments")
    }
  } else {
    gTree(x = x, y = y, id = id, arrow = arrow, constant = constant, 
          start = start, end = end, start.cap = start.cap, 
          start.cap2 = start.cap2, start.captype = start.captype, 
          end.cap = end.cap, end.cap2 = end.cap2, end.captype = end.captype, 
          name = name, gp = gp, vp = vp, cl = "cappedpathgrob")
  }
} else {
  # split x/y/ID values corresponding to each ID into two halves; first half to
  # end with the specified arrow aesthetics; second half (with a repetition of the
  # last value from first half, so that the two halves join up) has arrow set to NULL.
  id.split = split(id, id)
  id.split = lapply(id.split, 
                    function(i) c(rep(TRUE, ceiling(length(i)/2)), 
                                  rep(FALSE, length(i) - ceiling(length(i)/2))))
  id.split = unsplit(id.split, id)
  id.first.half = which(id.split == TRUE)
  id.second.half = which(id.split == FALSE |
                           (id.split == TRUE & c(id.split[-1], FALSE) == FALSE))

  if (is.null(start.cap) && is.null(end.cap)) {
    if (constant) {
      gList(grob(x = x[id.first.half], y = y[id.first.half], id = id[id.first.half], 
                 id.lengths = NULL, arrow = arrow, 
                 name = name, gp = gp, vp = vp, cl = "polyline"),
            grob(x = x[id.second.half], y = y[id.second.half], id = id[id.second.half], 
                 id.lengths = NULL, arrow = NULL, 
                 name = name, gp = gp, vp = vp, cl = "polyline"))

    }
    else {
      # I haven't modified this chunk as I'm not familiar with ggraph,
      # & haven't managed to trigger constant == FALSE condition yet
      # to test out code modifications here
      grob(x0 = x[!end], y0 = y[!end], 
           x1 = x[!start], y1 = y[!start], 
           id = id[!end], arrow = arrow, 
           name = name, gp = gp, vp = vp, cl = "segments")
    }
  } else {
    gList(gTree(x = x[id.first.half], y = y[id.first.half], id = id[id.first.half], 
                arrow = arrow, constant = constant, 
                start = start, end = end, start.cap = start.cap, 
                start.cap2 = start.cap2, start.captype = start.captype, 
                end.cap = end.cap, end.cap2 = end.cap2, end.captype = end.captype, 
                name = name, gp = gp, vp = vp, cl = "cappedpathgrob"),
          gTree(x = x[id.second.half], y = y[id.second.half], id = id[id.second.half],
                arrow = NULL, constant = constant, 
                start = start, end = end, start.cap = start.cap, 
                start.cap2 = start.cap2, start.captype = start.captype, 
                end.cap = end.cap, end.cap2 = end.cap2, end.captype = end.captype, 
                name = name, gp = gp, vp = vp, cl = "cappedpathgrob"))
  }
}

Step 3. Run ggraph code as per normal:

set.seed(777) # set seed for reproducibility

create_notable('bull') %>%
  ggraph(layout = 'graphopt') + 
  geom_edge_link(arrow = arrow(length = unit(4, 'mm')), 
                 end_cap = circle(0, 'mm')) +
  geom_node_point(size = 5) +
  theme_graph()

# end_cap parameter has been set to 0 so that the segments join up;
# you can also refrain from specifying this parameter completely.

result

This effect will remain in place for the rest of your current R session (i.e. all arrowed segments created by ggraph have their arrows in the middle rather than at the end) until you run the following line:

untrace(ggraph:::cappedPathGrob)

Thereafter, normal behaviour will resume.

Z.Lin
  • 28,055
  • 6
  • 54
  • 94
  • Might there also be a way of making these edits non-interactively, e.g. overwriting `ggraph:::cappedPathGrob` with a sourced script with the modified code, or something like that, so I (or people I might be sharing the code with) wouldn't need to manually edit the code each session? – Mark_1 Oct 04 '19 at 18:53
  • Sure. That approach would involve saving the new version of the function under a different name, then defining alternate versions of `grom_edge_link` & its underlying Geom object to use the new function. This would add up copying a lot of code, which can be fragile if the package updates, but I can post that as an alternative if you are interested. – Z.Lin Oct 04 '19 at 23:38
  • Having peeked under the hood of `geom_edge_link` it looks like there are multiple functions at multiple levels that would need to be changed, so I'm now thinking it might be more trouble than it's worth! I'll just stick with the manual approach which is already very elegant. Thank you anyway for offering! – Mark_1 Oct 05 '19 at 09:17