Search code examples
rggplot2igraphgganimateggraph

Smooth transitions between different layouts of the same network in ggraph


I would like to animate the same network using different layouts and having a smooth transition between layouts. I'd like to do this inside the gganimate framework.

library(igraph)
library(ggraph)
library(gganimate)

set.seed(1)
g <- erdos.renyi.game(10, .5, "gnp")
V(g)$name <- letters[1:vcount(g)]
l1 <- create_layout(g, "kk")
l2 <- create_layout(g, "circle")
l3 <- create_layout(g, "nicely")
long <- rbind(l1,l2,l3)
long$frame <- rep(1:3, each =10)

Following the ggplot approach, I store the node positions in the long format (long) and add a frame variable to each layout. I tried to make it work with the following code, which is working fine and almost what I want. However, I cannot seem to find a way to include the edges:

ggplot(long, aes(x, y, label = name, color = as.factor(name), frame = frame)) +
  geom_point(size = 3, show.legend = F) +
  geom_text(show.legend = F) +
  transition_components(frame)

I also tried to add the edges as geom_segment but ended up with them being static while the nodes kept moving. This is why I use the ggraph package and fail:

ggraph(g, layout = "manual", node.position = long) +
  geom_node_point() +
  geom_edge_link() +
  transition_components(frame)

I'd like to have an animation of one network with changing node positions that both displays nodes and edges.

Any help is much appreciated!

Edit: I learned that one can include the layout directly into ggraph (and even manipulate the attributes). This is what I've done in the following gif. Additionally geom_edge_link0' instead of geom_edge_link is being used.

ggraph(long) +
          geom_edge_link0() +
          geom_node_point() +
          transition_states(frame)

enter image description here

Note that the edges are not moving.


Solution

  • I'm not sure this is currently ready in gganimate as is. As of May 2019, here's what looks to be a related issue: https://github.com/thomasp85/gganimate/issues/139


    EDIT I've replaced with a working solution. Fair warning, I'm a newbie with network manipulations, and I expect someone with more experience could refactor the code to be much shorter.

    My general approach was to create the layouts, put the nodes into a table long2, and then create another table with all the edges. gganimate then calls the respective data source each layer needs.

    1. Create the nodes table for the three layouts:

    set.seed(1)
    g <- erdos.renyi.game(10, .5, "gnp")
    V(g)$name <- letters[1:vcount(g)]
    
    layouts <- c("kk", "circle", "nicely")
    long2 <- lapply(layouts, create_layout, graph = g) %>%
      enframe(name = "frame") %>%
      unnest()
    
    > head(long2)
    # A tibble: 6 x 7
      frame       x      y name  ggraph.orig_index circular ggraph.index
      <int>   <dbl>  <dbl> <fct>             <int> <lgl>           <int>
    1     1 -1.07    0.363 a                     1 FALSE               1
    2     1  1.06    0.160 b                     2 FALSE               2
    3     1 -1.69   -0.310 c                     3 FALSE               3
    4     1 -0.481   0.135 d                     4 FALSE               4
    5     1 -0.0603 -0.496 e                     5 FALSE               5
    6     1  0.0373  1.02  f                     6 FALSE               6
    

    2. Convert the edges from the original layout into a table.

    Here, I extract the edges from g and reshape into format that geom_segment can use, with columns for x, y, xend, and yend. This is ripe for refactoring, but it works.

    edges_df <- igraph::as_data_frame(g, "edges") %>% 
      tibble::rowid_to_column() %>%
      gather(end, name, -rowid) %>%
      # Here we get the coordinates for each node from `long2`.
      left_join(long2 %>% select(frame, name, x, y)) %>%
      gather(coord, val, x:y) %>%
      # create xend and yend when at the "to" end, for geom_segment use later
      mutate(col = paste0(coord, if_else(end == "to", "end", ""))) %>%
      select(frame, rowid, col, val) %>%
      arrange(frame, rowid) %>%
      spread(col, val) %>%
      # Get the node names for the coordinates we're using, so that we
      #   can name the edge from a to b as "a_b" and gganimate can tween
      #   correctly between frames. 
      left_join(long2 %>% select(frame, x, y, start_name = name)) %>%
      left_join(long2 %>% select(frame, xend = x, yend = y, end_name = name)) %>%
      unite(edge_name, c("start_name", "end_name"))
    
    > head(edges_df)
      frame rowid          x        xend          y       yend edge_name
    1     1     1 -1.0709480 -1.69252646  0.3630563 -0.3095612       a_c
    2     1     2 -1.0709480 -0.48086213  0.3630563  0.1353664       a_d
    3     1     3 -1.6925265 -0.48086213 -0.3095612  0.1353664       c_d
    4     1     4 -1.0709480 -0.06032354  0.3630563 -0.4957609       a_e
    5     1     5  1.0571895 -0.06032354  0.1596417 -0.4957609       b_e
    6     1     6 -0.4808621 -0.06032354  0.1353664 -0.4957609       d_e
    

    3. Plot!

    ggplot() +
      geom_segment(data = edges_df, 
                   aes(x = x, xend = xend, y = y, yend = yend, color = edge_name)) +
      geom_point(data = long2, aes(x, y, color = name), size = 4) +
      geom_text(data = long2, aes(x, y, label = name)) +
      guides(color = F) +
      ease_aes("quadratic-in-out") +
      transition_states(frame, state_length = 0.5) -> a
    
    animate(a, nframes = 400, fps = 30, width = 700, height = 300)
    

    enter image description here