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.
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
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"
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
plottingset.seed()
For reproducibility, you can set.seed()
.
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).
plot.igraph()
, hit tab to see arguments you can useset.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
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.
vector
that you pass to oma =
is c(bottom, left, top, right)
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:
par(mfrow=c(2, 2))
par(oma=c(2,2,2,2))
plot.igraph(UKfaculty)
\(\times\) 4 with
title("Inner Plot Title for Plot 1", sub = "Subtitle for Plot 1")
mtext(text = "left side inner plot", side = 2)
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_*()
sLayouts 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_*()
OptionsLuckily, we have a suite of layouts algorithms we can use.
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.
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)")
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")
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
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 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")
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")
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
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_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")
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.
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