library(dplyr)
#> 
#> Attaching package: 'dplyr'
#> The following objects are masked from 'package:stats':
#> 
#>     filter, lag
#> The following objects are masked from 'package:base':
#> 
#>     intersect, setdiff, setequal, union
library(adjHelpR)
library(ggraph)
#> Loading required package: ggplot2

Get the time-stamped edge list

The first step is to load a time-stamped edge list from which to generate the time-unfolded graph. Let’s start from a small synthetic example.

el <- tibble(
 ts   = c( 1,  1,    2,  2,  2,     3,  3,  3,      4,  4,  4,      6),
 from = c('A','B',  'A','B','B',   'A','C','C',    'A','B','C',    'D'),
 to   = c('C','C',  'C','C','D',   'C','D','E',    'C','C','E',    'E')
 )
el
#> # A tibble: 12 x 3
#>       ts from  to   
#>    <dbl> <chr> <chr>
#>  1     1 A     C    
#>  2     1 B     C    
#>  3     2 A     C    
#>  4     2 B     C    
#>  5     2 B     D    
#>  6     3 A     C    
#>  7     3 C     D    
#>  8     3 C     E    
#>  9     4 A     C    
#> 10     4 B     C    
#> 11     4 C     E    
#> 12     6 D     E

The edge list should contain at least 3 column: the edge source, the edge targets, and the timestamps. If you simply want to plot the time-unfolded graph from this edge list, run simply the get_unfolded_plot function. The function takes as option the direction in which you the graph to unfold. Set vertical to TRUE for a vertical plot, and to FALSE otherwise.

get_unfolded_plot(edge.list = el, select_cols = c(2:3,1), 
                  resize_ratio = 1.4, enlarge_ratio = .8)
#> Warning: Removed 28 rows containing missing values (geom_text).
#> Warning: Removed 30 rows containing missing values (geom_text).

Note that we had to set the option select_cols. The function assumes that your edge.list takes the form (‘source’, ‘target’,‘timestamps’). Since the edge list we have passed has a different order, we pass to the function the positions of these 3 columns, i.e., 2,3,1, because the first one contains the timetamps.

More control

Often, we are not satisfied with the default plot, or we are interested in the time-unfolded adjacency matrix to perform further operations. If our aim is the latter, we can use the function get_unfolded_adjacency.

get_unfolded_adjacency(edge.list = el, select_cols = c(2:3,1), vertical = FALSE, sparse = TRUE)
#> 35 x 35 sparse Matrix of class "dgCMatrix"
#>    [[ suppressing 35 column names 'A_0', 'A_1', 'A_2' ... ]]
#>                                                                          
#> A_0 . . . . . . . . . . . . . . . 1 . . . . . . . . . . . . . . . . . . .
#> A_1 . . . . . . . . . . . . . . . . 1 . . . . . . . . . . . . . . . . . .
#> A_2 . . . . . . . . . . . . . . . . . 1 . . . . . . . . . . . . . . . . .
#> A_3 . . . . . . . . . . . . . . . . . . 1 . . . . . . . . . . . . . . . .
#> A_4 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
#> A_5 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
#> A_6 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
#> B_0 . . . . . . . . . . . . . . . 1 . . . . . . . . . . . . . . . . . . .
#> B_1 . . . . . . . . . . . . . . . . 1 . . . . . . 1 . . . . . . . . . . .
#> B_2 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
#> B_3 . . . . . . . . . . . . . . . . . . 1 . . . . . . . . . . . . . . . .
#> B_4 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
#> B_5 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
#> B_6 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
#> C_0 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
#> C_1 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
#> C_2 . . . . . . . . . . . . . . . . . . . . . . . . 1 . . . . . . 1 . . .
#> C_3 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1 . .
#> C_4 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
#> C_5 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
#> C_6 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
#> D_0 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
#> D_1 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
#> D_2 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
#> D_3 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
#> D_4 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
#> D_5 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1
#> D_6 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
#> E_0 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
#> E_1 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
#> E_2 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
#> E_3 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
#> E_4 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
#> E_5 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
#> E_6 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

The function returns a square matrix with \(n\) times \(T\) rows, where \(n\) is the number of nodes in the edge list (or that have been passed to the function via the nodes argument), and \(T\) is the total length of timesteps. An edge at time \(t\) in the edge list, from \(i\) to \(j\), is then translated as an edge from the instance \(i_{t-1}\) of node \(i\), to the instance \(j_t\) of node \(j\).

If we want to plot the time-unfolded graph with full control, we can the use the time-unfolded adjacency matrix constructed as above. However, it is often sufficient to start from a ggraph layout object which assigns the nodes of the matrix on a grid, and then add custom modifications. The function get_unfolded_layout allows to do exactly so.

lyt <- get_unfolded_layout(edge.list = el, select_cols = c(2:3,1), vertical = TRUE)
lyt
#>      x y name .ggraph.orig_index circular .ggraph.index
#> 1  0.0 6  A_0                  1    FALSE             1
#> 2  0.6 6  B_0                  2    FALSE             2
#> 3  1.2 6  C_0                  3    FALSE             3
#> 4  1.8 6  D_0                  4    FALSE             4
#> 5  2.4 6  E_0                  5    FALSE             5
#> 6  0.0 5  A_1                  6    FALSE             6
#> 7  0.6 5  B_1                  7    FALSE             7
#> 8  1.2 5  C_1                  8    FALSE             8
#> 9  1.8 5  D_1                  9    FALSE             9
#> 10 2.4 5  E_1                 10    FALSE            10
#> 11 0.0 4  A_2                 11    FALSE            11
#> 12 0.6 4  B_2                 12    FALSE            12
#> 13 1.2 4  C_2                 13    FALSE            13
#> 14 1.8 4  D_2                 14    FALSE            14
#> 15 2.4 4  E_2                 15    FALSE            15
#> 16 0.0 3  A_3                 16    FALSE            16
#> 17 0.6 3  B_3                 17    FALSE            17
#> 18 1.2 3  C_3                 18    FALSE            18
#> 19 1.8 3  D_3                 19    FALSE            19
#> 20 2.4 3  E_3                 20    FALSE            20
#> 21 0.0 2  A_4                 21    FALSE            21
#> 22 0.6 2  B_4                 22    FALSE            22
#> 23 1.2 2  C_4                 23    FALSE            23
#> 24 1.8 2  D_4                 24    FALSE            24
#> 25 2.4 2  E_4                 25    FALSE            25
#> 26 0.0 1  A_5                 26    FALSE            26
#> 27 0.6 1  B_5                 27    FALSE            27
#> 28 1.2 1  C_5                 28    FALSE            28
#> 29 1.8 1  D_5                 29    FALSE            29
#> 30 2.4 1  E_5                 30    FALSE            30
#> 31 0.0 0  A_6                 31    FALSE            31
#> 32 0.6 0  B_6                 32    FALSE            32
#> 33 1.2 0  C_6                 33    FALSE            33
#> 34 1.8 0  D_6                 34    FALSE            34
#> 35 2.4 0  E_6                 35    FALSE            35

We can then proceed plotting the layout with our chosen specs. By passing the node labels in our preferred order we ensure the correct ordering of the nodes. For example, here we can plot from E to A

# get the nodes' labels from the edge list
node.labels <- sort(nodes_from_el(edge.list = el, select_cols = 2:3), decreasing = TRUE)
lyt <- get_unfolded_layout(edge.list = el, select_cols = c(2:3,1), 
                           vertical = TRUE, nodes = node.labels,
                           resize_ratio = .75, enlarge_ratio = .8)

# generate the plot
lyt %>%
  # create the graph
    ggraph() +
  # add edges with arrows
    geom_edge_link(width = grid::unit(0.5, 'mm'),
                  arrow = arrow(length = grid::unit(1, 'mm'), type = 'closed'),
                  end_cap = circle(2, 'mm'),
                  start_cap = circle(2, 'mm'),
                  color='gray50') +
  # add nodes
    geom_node_point(size=2) +
  # add timestamps' labels on the right
    geom_node_text(aes(
      label=ifelse(gsub('_.*', '', name)==node.labels[1],
                   gsub(paste0(node.labels[1],'_'), '', name),
                   NA)),
      nudge_x = - .6,
      size=5
    ) +
    # add nodes' names labels at the top
    geom_node_text(aes(
      label=ifelse(gsub('.*_', '', name)==(min(el$ts)-1), gsub('_.*', '', name), NA)),
      nudge_y = 0.6,
      size=5
    ) +
    coord_fixed(clip='off') + theme_graph() +
    theme(legend.position = "none")
#> Warning: Removed 28 rows containing missing values (geom_text).
#> Warning: Removed 30 rows containing missing values (geom_text).