Interactive and Dynamic Social Network Visualization in R

Prof James P. Curley
6th April 2016

Section III. Temporal Networks

Some Current Options


static img → compiled video
e.g. png → mp4 (using ffmpeg)

gganimate (for ggplot based packages)

ndtv & networkDynamic

There are pros and several cons for all methods. It can be very hard to get the look you want.

Static images to video


  1. Produce and set initial layout of network nodes.

  2. Iterate through each time interval of the network, remaking the network using the initial layout of the nodes (some jitter ok).

  3. Optional: Hide nodes/edges that are not active yet or not active in a time bin.

  4. Generate static images for each time unit (or several images per time unit for smoother transitions)

  5. Compile them into a movie using ffmpeg or something similar.

igraph + ffmpeg


# Set Output for each frame 
png(file="output_%03d.png", width=800,height=450)


#Time loop starts
for(time in seq(1, total_time,dt)){   #dt is the defined interval between successive plots

  gt <- delete_edges(g,which(E(g)$time > time)) #remove absent edges

  layout.new <- layout_with_fr(gt,coords=layout.old,niter=10,start.temp=0.05,grid="nogrid") #jitter layout

  plot(gt,layout=layout.new,vertex.label="",vertex.size=1+2*log(degree(gt)),
       vertex.frame.color=V(g)$color,edge.width=1.5,asp=9/16,margin=-0.15) #make plot

  layout.old <- layout.new #keep layout for next image
}


This code is based on an example by Esteban Moro. My version is here.

Improve Video Quality


The video examples here are for demo purposes - they aren't optimized!

I chose the level of video quality to fit into a talk. There are several things that can be done to improve video quality including:

  • improving the quality/size of original images (e.g. use svg)
  • use more transitional images between time bins to smooth transitions.
  • increasing the frame rate in ffmpeg
  • specify video bitrates and codecs that optimize performance
  • etc.

ggnetwork/ggplot + ffmpeg


Can also do this for ggnetwork output:

  1. Flatten igraph/network object to a dataframe using ggnetwork

  2. Identify time point when nodes first appear

  3. Make list of dataframes for each time, adding in rows with only nodes that have appeared by this time-point (use “NA” for all variables except x/y/vertex.name)

  4. Add in other nodes to df but you NA for x/y (to ensure color consistency)

5.. Use ggsave to store all images.

ggnetwork + ffmpeg

df.sp <- split(df, df$time)

for(i in 1:max(df$time)){

 #to ensure color consistency among groups
tmp<-rbind(df.sp[[i]],
           data.frame(x=NA,y=NA,Group=1:max(df$Group),
                      na.x=NA,vertex.names=NA,xend=NA,yend=NA,na.y=NA,time=NA))

  pp <- ggplot(tmp, aes(x = x, y = y, xend = xend, yend = yend)) +
    geom_edges(color = "black") +
    geom_nodes(aes(color = factor(Group)), size = 8) + 
    geom_nodetext(aes(label = vertex.names),color="black", fontface = "bold") +
    theme_blank() +
    xlim(0,1)+ylim(0,1) +
    theme(legend.position="none") +
    guides(color=guide_legend(keyheight=0.3,default.unit="inch",override.aes = list(size=6)))

   ggsave(pp,filename=paste0("output_",i,".png", sep=""))

}

This example can be found here.

This output is too fast - I did not put images between changes in nodes yet to make it smoother - will update

ggnetwork + gganimate


The same plot as above can be made directly in R using David Robinson's gganimate package which wraps the animation package to animate ggplot2 plots. My example here.

It also requires ffmpeg being installed.

There are various options that can be used to change the quality of output (it can output to other format types e.g. gif)

ggraph + gganimate


Similarly we can use gganimate with ggraph - though I have yet to fully explore this option.

Here is one example.

  • this visualizes edges over time but fades older ones out using a delay.

  • the delay effect is achieved by adjusting the edge weight by 'delay' time.

  • edge color is achieved using the gEdges function.

ggraph + gganimate


We use frame to determine the 'timebin' to compile images by, and set edge attributes using gEdges and geom_edge_link0 by time using the 'delay' variable.

geom_node_point(aes(size = degree, colour = factor(group))) + 

geom_edge_link0(aes(frame = timebins, alpha = delay, width = delay, colour = factor(node1.group)), data = gEdges(nodePar = 'group')) +  


Thomas Pedersen’s example is available here.

Computer Man

ndtv

Network Dynamic Temporal Visualizations - used a lot in dynamic network model analysis

https://cran.r-project.org/web/packages/ndtv/vignettes/ndtv.pdf

vignette

author: Skye Bender-deMoll

There is a lot in this package ! The following examples are based on the vignette above and some examples in this tutorial by Katherine Ognyanova.

My code is available here.

ndtv

# make a random network graph and add some V/E attributes

library(intergraph)
library(network)
library(randomNames)

set.seed(5)
N=30
genders <- sample(c("Female", "Male"), N,T)

df <- data.frame(id = randomNames(N,gender=genders, name.order = "first.last", name.sep = " "),
           sex=genders)

g <- sample_forestfire(N, fw.prob=0.3,bw.factor=.9,directed=F)
net<-intergraph::asNetwork(g)
net %v% "col" <- c("green", "gold", "blue", "red", "pink")[edge.betweenness.community(g)$membership]

net %v% "sex" <- genders
net %v% 'id'<- as.character(df$id)
net %v% "sizevar" <- sample(5:15,vcount(g),T)

net %e% "type" <- sample(LETTERS[1:4],ecount(g),T) 
net %e% "weight"  <- igraph::degree(g)

ndtv

library(ndtv)
render.d3movie(net, usearrows = F, displaylabels = F, bg="#111111", 
               vertex.border="#ffffff", vertex.col =  net %v% "col",
               vertex.cex = (net %v% "sizevar")/8, 
               edge.lwd = (net %e% "weight")/3, edge.col = '#55555599',
               vertex.tooltip = paste("<b>Name:</b>", (net %v% 'id') , "<br>",
                                      "<b>Gender:</b>", (net %v% 'sex')),
               edge.tooltip = paste("<b>Edge type:</b>", (net %e% 'type'), "<br>", 
                                    "<b>Edge weight:</b>", (net %e% "weight" ) ),
               launchBrowser=T, filename="Network.html" )  

'network' object

 net
 # Network attributes:
 #  vertices = 30 
 #  directed = FALSE 
 #  hyper = FALSE 
 #  loops = FALSE 
 #  multiple = FALSE 
 #  bipartite = FALSE 
 #  name = Forest fire model 
 #  fw.prob = 0.3 
 #  bw.factor = 0.9 
 #  ambs = 1 
 #  total edges= 45 
 #    missing edges= 0 
 #    non-missing edges= 45 
 # 
 # Vertex attribute names: 
 #    col id sex sizevar vertex.names 
 # 
 # Edge attribute names: 
 #    type weight 
 # 

'networkDynamic' object

#  NetworkDynamic properties:
#   distinct change times: 46 
#   maximal time range: 0 until  45 
# 
#  Dynamic (TEA) attributes:
#   Vertex TEAs:    animation.x.active 
#      animation.y.active 
# 
# Includes optional net.obs.period attribute:
#  Network observation period info:
#   Number of observation spells: 1 
#   Maximal time range observed: 0 until 45 
#   Temporal mode: continuous 
#   Time unit: unknown 
#   Suggested time increment: NA 
# 
#  Network attributes:
#   vertices = 30 
#   directed = FALSE 
#   hyper = FALSE 
#   loops = FALSE 
#   multiple = FALSE 
#   bipartite = FALSE 
#   name = Forest fire model 
#   fw.prob = 0.3 
#   bw.factor = 0.9 
#   ambs = 1 
#   net.obs.period: (not shown)
#  ...

'networkDynamic' object

#  ...
# slice.par:
#               Length Class  Mode     
# start         1      -none- numeric  
# end           1      -none- numeric  
# interval      1      -none- numeric  
# aggregate.dur 1      -none- numeric  
# rule          1      -none- character
#   total edges= 45 
#     missing edges= 0 
#     non-missing edges= 45 
# 
#  Vertex attribute names: 
#     active animation.x.active animation.y.active col id sex sizevar vertex.names 
# 
#  Edge attribute names: 
#     active type weight

ndtv filmstrip()

ndtv

# set up NetworkDynamic df
vs <- data.frame(onset=0, terminus=45, vertex.id=1:30)
es <- data.frame(onset=1:45, terminus=45, 
                 head=as.matrix(net, matrix.type="edgelist")[,1],
                 tail=as.matrix(net, matrix.type="edgelist")[,2])

net.dyn <- networkDynamic(base.net=net, edge.spells=es, vertex.spells=vs)

# Pre-compute animation coordinates
compute.animation(net.dyn, animation.mode = "kamadakawai",
                  slice.par=list(start=0, end=45, interval=2, 
                                 aggregate.dur=1, rule='any'))

# Make movie
render.d3movie(net.dyn, usearrows = F, displaylabels = F, label=net %v% "id",
               bg="#111111", 
               vertex.col =  net %v% "col",
               vertex.cex = function(slice){ degree(slice)/2.5 },  
               edge.lwd = (net %e% "weight")/3, edge.col = '#55555599',
               vertex.tooltip = paste("<b>Name:</b>", (net %v% 'id') , "<br>",
                                      "<b>Gender:</b>", (net %v% 'sex')),
               edge.tooltip = paste("<b>Edge type:</b>", (net %e% 'type'), "<br>", 
                                    "<b>Edge weight:</b>", (net %e% "weight" ) ),
               launchBrowser=F, filename="NetworkDynamic1.html",
               render.par=list(tween.frames = 15, show.time = F), 
               script.type='remoteSrc')

Acknowledgements


  • Kenton Russell

  • Bryan Lewis

  • Bob Rudis

  • Thomas Pedersen

  • Bennett Zhang