Search code examples
rggplot2ggraph

How to draw an arrowhead in the middle of an edge in ggraph


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.


Solution

  • 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.