Search code examples
rggplot2gridpie-chart

Draw arrow between two ggplot pie charts


Is there a way to draw an arrow between two pie charts using coordinates from the outer circle of the two pie charts as start and end position? My arrow is drawn by trying with different x's and y's.

#pie chart 1
pie1 <- count(diamonds, cut) %>%
  ggplot() +
  geom_bar(aes(x = '', y = n, fill = cut), stat = 'identity', width = 1) +
  coord_polar('y', start = 0) +
  theme_void()+
  theme(legend.position = 'none')

#pie chart 2
pie2 <- count(diamonds, color) %>%
  ggplot() +
  geom_bar(aes(x = '', y = n, fill = color), stat = 'identity', width = 1) +
  coord_polar('y', start = 0) +
  theme_void()+
  theme(legend.position = 'none')

# Plots and arrow combined
grid.newpage()
vp_fig <- viewport() # top plot area
pushViewport(vp_fig)
grid.draw(rectGrob())
vp_pie1 <- viewport(x =.5, y= 1, width = .25, height = .25, just = c('centre', 'top')) #viewport for pie chart 1
pushViewport(vp_pie1)
grid.draw(ggplotGrob(pie1))
popViewport()
vp_pie2 <- viewport(x =.25, y= .5, width = .25, height = .25, just = c('left', 'centre')) #viewport for pie chart 2
pushViewport(vp_pie2)
grid.draw(ggplotGrob(pie2))
popViewport()
upViewport() #move to top plot area
grid.lines(x = c(.45, .37), y = c(.8, .61), arrow = arrow()) # arrow between the pie charts

enter image description here


Solution

  • Here's a possible approach.:

    final result

    Step 0. Create pie charts, & convert them to a list of grobs:

    pie1 <- count(diamonds, fill = cut) %>%
      ggplot() +
      geom_col(aes(x = '', y = n, fill = fill), width = 1) +
      coord_polar('y', start = 0) +
      theme_void()+
      theme(legend.position = 'none')
    
    pie2 <- pie1 %+% count(diamonds, fill = color)
    
    pie3 <- pie1 %+% count(diamonds, fill = clarity)
    
    pie.list <- list(pie1 = ggplotGrob(pie1),
                     pie2 = ggplotGrob(pie2),
                     pie3 = ggplotGrob(pie3))
    rm(pie1, pie2, pie3)
    

    Step 1. Define centre coordinates / radius for each pie:

    pie.coords <- data.frame(
      pie = names(pie.list),
      center.x = c(0, 3, 5),
      center.y = c(0, 4, 2),
      radius = c(1, 1.5, 0.5)
    )
    

    Step 2. Calculate the appropriate start & end arrow coordinates for each combination of pies, taking into account each pie's size (assuming each pie can have a different radius value):

    arrow.coords <- expand.grid(start = pie.coords$pie,
                                end = pie.coords$pie,
                                KEEP.OUT.ATTRS = FALSE,
                                stringsAsFactors = FALSE) %>%
      filter(start != end) %>%
      left_join(pie.coords, by = c("start" = "pie")) %>%
      left_join(pie.coords, by = c("end" = "pie"))
    colnames(arrow.coords) <- colnames(arrow.coords) %>%
      gsub(".x$", ".start", .) %>%
      gsub(".y$", ".end", .)
    arrow.coords <- arrow.coords %>%
      mutate(delta.x = center.x.end - center.x.start,
             delta.y = center.y.end - center.y.start,
             distance = sqrt(delta.x^2 + delta.y^2)) %>%
      mutate(start.x = center.x.start + radius.start / distance * delta.x,
             start.y = center.y.start + radius.start / distance * delta.y,
             end.x = center.x.end - radius.end / distance * delta.x,
             end.y = center.y.end - radius.end / distance * delta.y) %>%
      select(starts_with("start"),
             starts_with("end")) %>%
      mutate_at(vars(start, end), factor)
    

    Step 3. Convert pie center / radius into x & y min/max coordinates:

    pie.coords <- pie.coords %>%
      mutate(xmin = center.x - radius,
             xmax = center.x + radius,
             ymin = center.y - radius,
             ymax = center.y + radius)
    

    Step 4. Define function to create an annotation_custom() layer for each pie (this is optional; I just don't want to type the same thing repeatedly for each pie):

    annotation_custom_list <- function(pie.names){
      result <- vector("list", length(pie.names) + 1)
      for(i in seq_along(pie.names)){
        pie <- pie.names[i]
    
        result[[i]] <- annotation_custom(
          grob = pie.list[[pie]],
          xmin = pie.coords$xmin[pie.coords$pie == pie],
          xmax = pie.coords$xmax[pie.coords$pie == pie],
          ymin = pie.coords$ymin[pie.coords$pie == pie],
          ymax = pie.coords$ymax[pie.coords$pie == pie])
      }
    
      # add a blank geom layer to ensure the resulting ggplot's
      # scales extend sufficiently to show each pie
      result[[length(result)]] <- geom_blank(
        data = pie.coords %>% filter(pie %in% pie.names),
        aes(xmin = xmin, ymin = ymin, xmax = xmax, ymax = ymax)
      )
      return(result)
    }
    

    Step 5. Putting it all together:

    ggplot() +
    
      # plot pie grobs
      annotation_custom_list(c("pie1", "pie2", "pie3")) +
    
      # plot arrows between grobs
      # (adjust the filter criteria to only plot between specific pies)
      geom_segment(data = arrow.coords %>% 
                     filter(as.integer(start) < as.integer(end)),
                   aes(x = start.x, y = start.y,
                       xend = end.x, yend = end.y),
                   arrow = arrow()) +
    
      # theme_void for clean look
      theme_void()