This is packed with material that you can go through item by item, but will likely prove more useful as a reference that you can search. Liberal use of Cntrl+F is encouraged.

Setting Up

Run the following code to ensure you have the required packages installed.

pacakge_list <- c("igraph", "igraphdata", "visNetwork")
new_packages <- pacakge_list[!(pacakge_list %in% installed.packages()[,"Package"])]
if(length(new_packages)) install.packages(new_packages)

Then, load the packages.

library(igraph)
library(igraphdata)
library(visNetwork)
data("UKfaculty") ## example network
UKfaculty
## IGRAPH 6f42903 D-W- 81 817 -- 
## + attr: Type (g/c), Date (g/c), Citation (g/c), Author (g/c),
## | Group (v/n), weight (e/n)
## + edges from 6f42903:
##  [1] 57->52 76->42 12->69 43->34 28->47 58->51  7->29 40->71  5->37 48->55
## [11]  6->58 21-> 8 28->69 43->21 67->58 65->42  5->67 52->75 37->64  4->36
## [21] 12->49 19->46 37-> 9 74->36 62-> 1 15-> 2 72->49 46->62  2->29 40->12
## [31] 22->29 71->69  4-> 3 37->69  5-> 6 77->13 23->49 52->35 20->14 62->70
## [41] 34->35 76->72  7->42 37->42 51->80 38->45 62->64 36->53 62->77 17->61
## [51]  7->68 46->29 44->53 18->58 12->16 72->42 52->32 58->21 38->17 15->51
## [61] 22-> 7 22->69  5->13 29-> 2 77->12 37->35 18->46 10->71 22->47 20->19
## + ... omitted several edges

Using Attributes

Before plotting, in particular with data with which you are unfamiliar, get a sense of the attributes of the graph, vertices, and edges.

get_igraph_attrs <- function(igraph){
  if(!is_igraph(igraph)) stop("Not a graph object")
  list(graph_attrs = list.graph.attributes(igraph),
       vertex_attrs = list.vertex.attributes(igraph),
       edge_attrs = list.edge.attributes(igraph))
}

get_igraph_attrs(UKfaculty)
## $graph_attrs
## [1] "Type"     "Date"     "Citation" "Author"  
## 
## $vertex_attrs
## [1] "Group"
## 
## $edge_attrs
## [1] "weight"

A Note on Syntax

You may have noticed that igraph has a lot of functions with very similar names.

is.directed(UKfaculty)              ## check if a graph is direted
## [1] TRUE
is_directed(UKfaculty)
## [1] TRUE
identical(is.directed, is_directed)
## [1] TRUE

They’re the same thing, so which one do you use?

Generally if given a choice and you’re unsure, use the function with _ as . is better left to a specific object oriented system.

For our purposes, the technical details aren’t important, but you can learn more here:

igraph plotting

The Basics

set.seed()

For reproducibility, you can set.seed().

  • The seed is the starting point from which random numbers are generated, such as those in igraph’s layout_* algorithms.
set.seed(1234)
## the code for which you wanted to set the seed

plot() vs plot.igraph()

While both plot() and plot.igraph() will do the same thing, plot.igraph() is preferable as it offers tab completion (at least in RStudio).

  • if you’re inside the parentheses of plot.igraph(), hit tab to see arguments you can use
set.seed(1234)                             ## set.seed() for reproducibility
plot(UKfaculty)                            ## base R plot()
title("set.seed()")                        ## add a title to a single plot

set.seed(1234)
plot.igraph(UKfaculty)
title("set.seed()",
      sub = "Use the argument `sub =` for a subtitle")          ## add a subtitle

Multiple Plots

Titles for Multiple Plots

In order to title() multiple plots, you need to modify the size of the outer margins.

You can accomplish this using par()’s oma = argument, which takes a vector of four numbers which correspond with the four sides of a rectangle.

  • the order of the vector that you pass to oma = is c(bottom, left, top, right)
  • if we wanted the outer margins of the top and bottom to be of size 2 and the outer margins of the left and right sides to remain as 0, we would use :
    • par(oma = c(2, 0, 2, 0))
par(mfrow=c(1, 2))                          ## par(mfrow=c(1,2)) = 1 row, 2 columns
par(oma=c(2,0,2,0))       ## adjust top and bottom outer margin for the title and subtitle

plot.igraph(UKfaculty)
title("Inner Plot Title for Plot 1", sub = "Subtitle for Plot 1") ## internal titles
plot.igraph(UKfaculty)
title("Inner Plot Title for Plot 2", sub = "Subtitle for Plot 2") ## internal titles

title("Default plot.igraph()", outer = TRUE)

par(mfrow=c(1, 1))                    ## par(mfrow=c(1,1)) resets to 1 row, 1 column

Adding subtitles to multiple plots gets trickier, but we can do so by simply using mtext(), which is short for margin text.

For a subtitle, use the arguments:

  • text = “the text you will display”
  • side = 1 (bottom), 2 (left), 3 (top), or 4 (right)

Here’s an example that shows:

  • setting up a grid of 2 rows and 2 columns: par(mfrow=c(2, 2))
  • expanding the outer boundaries on all four sides: par(oma=c(2,2,2,2))
  • plotting 4 inner plots: plot.igraph(UKfaculty) \(\times\) 4 with
    • title and subtile: title("Inner Plot Title for Plot 1", sub = "Subtitle for Plot 1")
    • left side text: mtext(text = "left side inner plot", side = 2)
    • right side text: mtext(text = "right side inner plot", side = 4)
par(mfrow=c(2, 2))                          ## par(mfrow=c(2,2)) = 2 row, 2 columns
par(oma=c(2,2,2,2))                         ## adjust outer margin for title

plot.igraph(UKfaculty)
title("Inner Plot Title for Plot 1", sub = "Subtitle for Plot 1") ## internal titles
mtext(text = "left side inner plot", side = 2)                    ## internal side text
mtext(text = "right side inner plot", side = 4)                   ## internal side text

plot.igraph(UKfaculty)
title("Inner Plot Title for Plot 2", sub = "Subtitle for Plot 2") ## internal titles
mtext(text = "left side inner plot", side = 2)                    ## internal side text
mtext(text = "right side inner plot", side = 4)                   ## internal side text

plot.igraph(UKfaculty)
title("Inner Plot Title for Plot 3", sub = "Subtitle for Plot 3") ## internal titles
mtext(text = "left side inner plot", side = 2)                    ## internal side text
mtext(text = "right side inner plot", side = 4)                   ## internal side text

plot.igraph(UKfaculty)
title("Inner Plot Title for Plot 4", sub = "Subtitle for Plot 4") ## internal titles
mtext(text = "left side inner plot", side = 2)                    ## internal side text
mtext(text = "right side inner plot", side = 4)                   ## internal side text

title("So. Many. Plots.", outer = TRUE)
mtext(text = "a subtitle", side = 1, outer = TRUE)
mtext(text = "left side outer plot", side = 2, outer = TRUE)
mtext(text = "right side outer plot", side = 4, outer = TRUE)

par(mfrow=c(1, 1))

layout_*()s

Layouts can be a challenge, especially as graphs grow in size and density. Unfortunately, you will often see network visualizations that are best described as hariballs.

Here’s a graph of 100 nodes where there is a 25% chance of nodes having a tie.

plot.igraph(erdos.renyi.game(100, 1/4), 
            main = "Great, Another Useless Hairball",
            sub = "This is not informative")

Even with a network of less than 100 edges we can see that the default options don’t render very meaningful visualizations.

layout_*() Options

Luckily, we have a suite of layouts algorithms we can use.

  • FYI: plot.igraph() defaults to layout_nicely

Most of the time, these are the ones you will want to use.

## list igraph's functions that start with `layout_with_`
layouts <- grep("^layout_with_.*[^[sugiyama]]*", ls("package:igraph"), value = TRUE)
layouts
## [1] "layout_with_dh"       "layout_with_drl"      "layout_with_fr"      
## [4] "layout_with_gem"      "layout_with_graphopt" "layout_with_kk"      
## [7] "layout_with_lgl"      "layout_with_mds"
par(mfrow=c(1, 2))
par(oma=c(0,0,2,0))                         ## adjust outer margin for title

invisible(lapply(layouts, function(x){
  plot.igraph(UKfaculty, edge.arrow.size = 0, layout = eval(as.name(x)), main = x)
  title("Layout Variations", outer = TRUE)
}))

par(mfrow=c(1, 1))



### layout_*() Arguments

The layout_*()s seen above have the following arguments that you can tweak.

lapply(layouts, function(x) formalArgs(x)) %>% `names<-`(layouts)
## $layout_with_dh
##  [1] "graph"                 "coords"               
##  [3] "maxiter"               "fineiter"             
##  [5] "cool.fact"             "weight.node.dist"     
##  [7] "weight.border"         "weight.edge.lengths"  
##  [9] "weight.edge.crossings" "weight.node.edge.dist"
## 
## $layout_with_drl
## [1] "graph"    "use.seed" "seed"     "options"  "weights"  "fixed"   
## [7] "dim"     
## 
## $layout_with_fr
##  [1] "graph"      "coords"     "dim"        "niter"      "start.temp"
##  [6] "grid"       "weights"    "minx"       "maxx"       "miny"      
## [11] "maxy"       "minz"       "maxz"       "coolexp"    "maxdelta"  
## [16] "area"       "repulserad" "maxiter"   
## 
## $layout_with_gem
## [1] "graph"     "coords"    "maxiter"   "temp.max"  "temp.min"  "temp.init"
## 
## $layout_with_graphopt
## [1] "graph"           "start"           "niter"           "charge"         
## [5] "mass"            "spring.length"   "spring.constant" "max.sa.movement"
## 
## $layout_with_kk
##  [1] "graph"   "coords"  "dim"     "maxiter" "epsilon" "kkconst" "weights"
##  [8] "minx"    "maxx"    "miny"    "maxy"    "minz"    "maxz"    "niter"  
## [15] "sigma"   "initemp" "coolexp" "start"  
## 
## $layout_with_lgl
## [1] "graph"      "maxiter"    "maxdelta"   "area"       "coolexp"   
## [6] "repulserad" "cellsize"   "root"      
## 
## $layout_with_mds
## [1] "graph"   "dist"    "dim"     "options"

In practice, the argument you most likely want to adjust niter, short for number of iterations.

These layouts have adjustable iteration parameters.

sapply(layouts, function(x) formalArgs(x)) %>% 
  `names<-`(layouts) %>%
  grep("niter", ., value = TRUE) %>%
  names()
## [1] "layout_with_fr"       "layout_with_graphopt" "layout_with_kk"

How’s it work?

par(mfrow=c(2, 2))                          ## par(mfrow=c(2,2)) = 2 row, 2 columns
par(oma=c(0,0,2,0))                         ## adjust outer margin for title

## layout_with_fr
plot.igraph(UKfaculty, layout = layout_with_fr(UKfaculty, niter = 5), main = 5)
plot.igraph(UKfaculty, layout = layout_with_fr(UKfaculty, niter = 50), main = 50)
plot.igraph(UKfaculty, layout = layout_with_fr(UKfaculty, niter = 500), main = 500)
plot.igraph(UKfaculty, layout = layout_with_fr(UKfaculty, niter = 10000), main = 10000)
title("No. of Iterations (layout_with_fr)", outer = TRUE)


## layout_with_kk
plot.igraph(UKfaculty, layout = layout_with_kk(UKfaculty, niter = 5), main = 5)
plot.igraph(UKfaculty, layout = layout_with_kk(UKfaculty, niter = 50), main = 50)
plot.igraph(UKfaculty, layout = layout_with_kk(UKfaculty, niter = 500), main = 500)
plot.igraph(UKfaculty, layout = layout_with_kk(UKfaculty, niter = 10000), main = 10000)
title("No. of Iterations (layout_with_kk)", outer = TRUE)


## layout_with_graphopt
plot.igraph(UKfaculty, layout = layout_with_graphopt(UKfaculty, niter = 5), main = 5)
plot.igraph(UKfaculty, layout = layout_with_graphopt(UKfaculty, niter = 50), main = 50)
plot.igraph(UKfaculty, layout = layout_with_graphopt(UKfaculty, niter = 500), main = 500)
plot.igraph(UKfaculty, layout = layout_with_graphopt(UKfaculty, niter = 10000), main = 10000)
title("No. of Iterations (layout_with_graphopt)", outer = TRUE)

par(mfrow=c(1, 1))

It looks like layout_with_fr and layout_with_graphopt are more promising than layout_with_kk. Let’s compare just those two.

par(mfrow=c(1, 2))
par(oma=c(0,0,2,0))

plot.igraph(UKfaculty,
            layout = layout_with_fr(UKfaculty, niter = 100000), 
            main = "layout_with_fr",
            sub = "100000 iterations")

plot.igraph(UKfaculty, 
            layout = layout_with_graphopt(UKfaculty, niter = 100000), 
            main = "layout_with_graphopt",
            sub = "100000 iterations")

par(mfrow=c(1, 1))

layout_with_fr is capturing more defined clustering than layout_with_graphopt, so let’s select it and continue tweaking.

Keeping Coordinates

It is often very useful to be able to reuse the exact same coordinates across multiple plots.

While you can use set.seed() as we saw earlier, this will not let you transfer those coordinates to plots that are not constructed with igraph.

You can asign the coordinates to a variable like so:

coords <- layout_with_fr(UKfaculty, niter = 100000)

Then, you can use the variable coords as the argument for plot.igraph()’s layout =

par(mfrow=c(1, 2))
par(oma=c(0,0,2,0))

plot.igraph(UKfaculty, layout = coords, main = "layout = coords #1")

plot.igraph(UKfaculty, layout = coords, main = "layout = coords #2")

title("Using the Same Layout Across Multiple Graphs", outer = TRUE)

par(mfrow=c(1, 1))

plot.igraph(UKfaculty, layout = coords, main = "layout = coords (Single Plot)")

plot.igraph(UKfaculty, layout = coords, main = "layout = coords (Single Plot)")


Aspect Ratio

plot.igraph()’s aspect ratio defaults to 1, but often setting it to 0 is more useful so that we can exploit the maximum available plot area.

plot.igraph(UKfaculty, layout = coords, asp = 0, main = "asp = 0")

Vertices/Nodes

Now that we have a promising layout, let’s adjust our nodes.

What are our options?

plot.igraph(UKfaculty, layout = coords, asp = 0, main = "Modifying Nodes",
    ## colors =======================================
    vertex.color = rgb(0.8,0.4,0.3,0.8),      ## color
    vertex.frame.color = "white",             ## border color
    ## shapes =======================================
    vertex.shape = "circle",                  ## none, circle, square, csquare, 
                                              ## vrectangle, pie, raster, sphere
                                              ## rectangle, crectangle
    ## sizes =======================================
    vertex.size = 6,                         ## size, default = 15
    vertex.size2 = NA                        ## second dimension size (for parallelograms)
    )

Let’s set our node size to 6 for now.

V(UKfaculty)$size <- 6

Using Attributes to Modify Nodes

Generally, node attributes are either continuous or categorical and your plotting decisions should take into account the kind of attribute you wish to display

Categorical Variables

Categorical attributes are best displayed by node shapes and colors

list.vertex.attributes(UKfaculty) ## what attributes are in the graph?
## [1] "Group" "size"
unique(vertex_attr(UKfaculty, "Group")) ## how many different Group categories?
## [1] 3 1 2 4

In order to color the nodes by their Group, it simplest to set a color attribute base on each node’s Group.

V(UKfaculty)$color = "black"  ## start by setting all `shape`s to a default of "circle"
V(UKfaculty)$color = ifelse(V(UKfaculty)$Group == 1, ## if group is 1
                            "lightblue",                  ## color it "blue"
                            V(UKfaculty)$color)      ## or else leave it as "black"

V(UKfaculty)$color = ifelse(V(UKfaculty)$Group == 2, "salmon", V(UKfaculty)$color)
V(UKfaculty)$color = ifelse(V(UKfaculty)$Group == 3, "yellow", V(UKfaculty)$color)
V(UKfaculty)$color = ifelse(V(UKfaculty)$Group == 4, "green", V(UKfaculty)$color)
plot.igraph(UKfaculty, layout = coords, asp = 0,
            main = "Node Color by Categorical Atribute")

Following the same pattern, we can modify node shapes.

V(UKfaculty)$shape = "circle"    ## start by setting all `shape`s to a default of "circle"
V(UKfaculty)$shape = ifelse(V(UKfaculty)$Group == 1, ## if group is 1
                            "rectangle",                  ## shape it "blue"
                            V(UKfaculty)$shape)      ## or else leave it as "black"

V(UKfaculty)$shape = ifelse(V(UKfaculty)$Group == 2, "square", V(UKfaculty)$shape)
V(UKfaculty)$shape = ifelse(V(UKfaculty)$Group == 3, "sphere", V(UKfaculty)$shape)
V(UKfaculty)$shape = ifelse(V(UKfaculty)$Group == 4, "circle", V(UKfaculty)$shape)
plot.igraph(UKfaculty, layout = coords, asp = 0,
            main = "Node Color and Shape by Categorical Atribute")

If you don’t like a vertex attribute, you cangraph <- remove.vertex.attribute()

UKfaculty <- delete_vertex_attr(UKfaculty, "shape")

Continuous Variables

Our network doesn’t currently have any continuous attributes, but in network analysis we deal with plenty of metrics, so we can assign our own.

Let’s size our nodes based on their degree.

V(UKfaculty)$size <- degree(UKfaculty)
plot.igraph(UKfaculty, layout = coords, asp = 0,
            main = "Node Size by Continuous Atribute (degree)")

Yikes, that’s not very helpful!

We can scale values by normalizing them and adding 0.25 (to avoid 0 values)

normalize_01 <- function(x) (x - min(x)) / (max(x) - min(x)) + 0.25

After normalizing the values, you will need to scale them with some arithmetic. 10 is a safe value to test, but your needs will differ based on the network.

V(UKfaculty)$size <- normalize_01(degree(UKfaculty)) * 10

plot.igraph(UKfaculty, layout = coords, asp = 0,
            main = "Node Size by Normalized Degree")

Labels

plot.igraph(UKfaculty, layout = coords, asp = 0, main = "Customizing Node Labels",
            ## color =======================================
            vertex.label.color = "black",
            ## font family =======================================
            vertex.label.family = "Times",
            ## font face =======================================
            vertex.label.font = 2, ## 1 = plain, 2 = bold, 3 = italic, 4 = bold/italic
            ## font expansion factor =======================================
            vertex.label.cex = normalize_01(degree(UKfaculty)) * 1.5) ## normalized

Edges

plot.igraph(UKfaculty, layout = coords, asp = 0, main = "Customizing Edges",
            ## color =======================================
            edge.color = "lightgray",      
            ## width =======================================
            edge.width = 0.5,             ## default = 1
            ## arrows =======================================
            edge.arrow.size = 0.5,        ## default = 1
            edge.arrow.width = 2,         ## default = 1
            ## lines =======================================
            edge.lty = "solid",           ## linetype: blank, solid, dashed, dotted,
                                          ## dotdash, longdash, or twodash
            ## shape =======================================
            edge.curved = 0.05            ## 0 to 1 or TRUE (0.5)
    )

Legend

legend_cats <- data.frame(attr = unique(vertex_attr(UKfaculty, "Group")),
                          color = unique(V(UKfaculty)$color))
legend_cats <- legend_cats[order(legend_cats$attr), c(1, 2)]

plot.igraph(UKfaculty, layout = coords, asp = 0, 
            main = "Customizing Legends", sub = "Legend of the Cats!",
            ## nodes =======================================
            vertex.label = NA,
            ## edges =======================================
            edge.color = "lightgray",
            edge.width = 0.5,             ## default = 1
            edge.arrow.size = 0.5,        ## default = 1
            edge.arrow.width = 2,         ## default = 1
            edge.lty = "solid",           ## linetype: blank, solid, dashed, dotted,
                                          ## dotdash, longdash, or twodash
            edge.curved = 0.05            ## 0 to 1 or TRUE (0.5)
)

legend(x = "bottomleft",      ## position, also takes x,y coordinates
       legend = legend_cats$attr,
       pch = 19,              ## legend symbols see ?points
       col = legend_cats$color,
       bty = "n",
       title = "Faculty Groups")

Saving your work

There are multiple formats that you can use to save your work (e.g., bitmap, png, jpeg, tiff, PDF). For a rundown on some of these, check the help section: ?png. In the meantime, we’ll focus on using the jpeg format. This is pretty universal, and popular with publishers. It should also load well into a Google doc.

The procedure itself is actually relatively simple. At its minimum, you will call the graphics saving device and provide a name of the file that you want to save. jpeg("visualization1..jpg")
Next, you will plot the visualization. plot.igraph(g)
Finally, you turn the device back off and the plot will save.
dev.off()

This procedure will work with any R plot. For an example, we are using the plot above.

jpeg("Figure_1.jpg", 
     width=6.8, height=6.8, 
     units='in',res=300)

plot.igraph(UKfaculty, layout = coords, asp = 0, 
            main = "Customizing Legends", 
            sub = "Legend of the Cats!",
            vertex.label = NA,
            edge.color = "lightgray",      
            edge.width = 0.5,             
            edge.arrow.size = 0.5,        
            edge.arrow.width = 2,         
            edge.lty = "solid",           
            edge.curved = 0.05)

legend(x = "bottomleft",      
       legend = legend_cats$attr,
       pch = 19,              
       col = legend_cats$color,
       bty = "n",
       title = "Faculty Groups")

dev.off()

You will notice that we also set the height and width of the plot area in inches (units='in'), and saved this in 300 dpi resolution. Publicaiton quality is generally around 300 dpi. You can get away with much lower numbers if you are just making a report, or saving the plot for reference.

Beyond Base R

visNetwork()

Learn more with ?visNetwork and going here and here.

# it tends to work better to reset size 
UKfaculty <- remove.vertex.attribute(UKfaculty, "size")
UKfaculty_vis <- toVisNetworkData(UKfaculty) # convert the graph (or use visIgraph)

names <- sort(UKfaculty_vis$nodes$id) # for our dropdown box

UK_vis_plot <- visNetwork(nodes = UKfaculty_vis$nodes,
           edges = UKfaculty_vis$edges,
           main = "So Interactive",
           submain = "Much JavaScript",
           footer = "Wow") %>%
  visIgraphLayout(layout = "layout_with_kk", # or use igraph's `layout_*`s in quotes
                  # layout = "layout.norm",  # using saved coords? set this!
                  # layoutMatrix = coords,   # our previous coords
                  smooth = FALSE,            # set to F when bogged by bigger graphs
                  physics = TRUE             # set to F when bogged by bigger graphs
                ) %>%
  visNodes(size = 50) %>%
  visEdges(color = list(highlight = "lightgray")) %>%
  visOptions(selectedBy = "Group",
             highlightNearest = list(enabled = TRUE,
                                     degree = 1,
                                     hover = TRUE,
                                     labelOnly = TRUE),
             nodesIdSelection = list(enabled = TRUE,
                                     values = names)) %>%
  visGroups(groupname = "1", color = "lightblue") %>%
  visGroups(groupname = "2", color = "salmon") %>%
  visGroups(groupname = "3", color = "yellow") %>%
  visGroups(groupname = "4", color = "green") %>%
  visLegend(width = 0.1) %>%
  visPhysics(repulsion = list(springlength = 50), # usually will take some tweaking
             maxVelocity = 2,
             solver = "forceAtlas2Based",
             forceAtlas2Based = list(gravitationalConstant = -1000),
             timestep = 0.25)

UK_vis_plot

sessionInfo()

Sys.time() - start_time
## Time difference of 51.12729 secs
sessionInfo()
## R version 3.4.3 (2017-11-30)
## Platform: x86_64-w64-mingw32/x64 (64-bit)
## Running under: Windows 10 x64 (build 15063)
## 
## Matrix products: default
## 
## locale:
## [1] LC_COLLATE=English_United States.1252 
## [2] LC_CTYPE=English_United States.1252   
## [3] LC_MONETARY=English_United States.1252
## [4] LC_NUMERIC=C                          
## [5] LC_TIME=English_United States.1252    
## 
## attached base packages:
## [1] stats     graphics  grDevices utils     datasets  methods   base     
## 
## other attached packages:
## [1] visNetwork_2.0.1 igraphdata_1.0.1 igraph_1.1.2    
## 
## loaded via a namespace (and not attached):
##  [1] Rcpp_0.12.14    digest_0.6.12   rprojroot_1.2   jsonlite_1.5   
##  [5] backports_1.1.1 magrittr_1.5    evaluate_0.10.1 stringi_1.1.6  
##  [9] rmarkdown_1.8   tools_3.4.3     stringr_1.2.0   htmlwidgets_0.9
## [13] yaml_2.1.15     compiler_3.4.3  pkgconfig_2.0.1 htmltools_0.3.6
## [17] knitr_1.17.20