-
-
Save internaut/a9a274c72181eaa7f5c3ab3a5f54b996 to your computer and use it in GitHub Desktop.
# Plot a network graph of nodes with geographic coordinates on a map. | |
# | |
# Author: Markus Konrad <markus.konrad@wzb.eu> | |
# May 2018 | |
# | |
# This script shows three ways of plotting a network graph on a map. | |
# The following information should be visualized (with the respective | |
# aestethics added): | |
# | |
# * graph nodes with: | |
# * position on map -> x,y position of the node | |
# * node weight (degree of the node) -> node size | |
# * node label -> also x,y position of the node | |
# * edges between nodes with: | |
# * edge weight -> edge width | |
# * edge category -> edge color | |
library(assertthat) | |
library(dplyr) | |
library(purrr) | |
library(igraph) | |
library(ggplot2) | |
library(ggraph) | |
library(ggmap) | |
# -------------------------------------- # | |
# Preparation: generate some random data # | |
# -------------------------------------- # | |
set.seed(123) | |
N_EDGES_PER_NODE_MIN <- 1 | |
N_EDGES_PER_NODE_MAX <- 4 | |
N_CATEGORIES <- 4 | |
country_coords_txt <- " | |
1 3.00000 28.00000 Algeria | |
2 54.00000 24.00000 UAE | |
3 139.75309 35.68536 Japan | |
4 45.00000 25.00000 'Saudi Arabia' | |
5 9.00000 34.00000 Tunisia | |
6 5.75000 52.50000 Netherlands | |
7 103.80000 1.36667 Singapore | |
8 124.10000 -8.36667 Korea | |
9 -2.69531 54.75844 UK | |
10 34.91155 39.05901 Turkey | |
11 -113.64258 60.10867 Canada | |
12 77.00000 20.00000 India | |
13 25.00000 46.00000 Romania | |
14 135.00000 -25.00000 Australia | |
15 10.00000 62.00000 Norway" | |
# nodes come from the above table and contain geo-coordinates for some | |
# randomly picked countries | |
nodes <- read.delim(text = country_coords_txt, header = FALSE, | |
quote = "'", sep = "", | |
col.names = c('id', 'lon', 'lat', 'name')) | |
# edges: create random connections between countries (nodes) | |
edges <- map_dfr(nodes$id, function(id) { | |
n <- floor(runif(1, N_EDGES_PER_NODE_MIN, N_EDGES_PER_NODE_MAX+1)) | |
to <- sample(1:max(nodes$id), n, replace = FALSE) | |
to <- to[to != id] | |
categories <- sample(1:N_CATEGORIES, length(to), replace = TRUE) | |
weights <- runif(length(to)) | |
data_frame(from = id, to = to, weight = weights, category = categories) | |
}) | |
edges <- edges %>% mutate(category = as.factor(category)) | |
# create the igraph graph object | |
g <- graph_from_data_frame(edges, directed = F, vertices = nodes) | |
# --------------------------------------------------------------------- # | |
# Common data structures and ggplot objects for all the following plots # | |
# --------------------------------------------------------------------- # | |
# create a data frame for plotting the edges | |
# join with nodes to get start and end positions for each | |
# edge (x, y and xend, yend) | |
edges_for_plot <- edges %>% | |
inner_join(nodes %>% select(id, lon, lat), by = c('from' = 'id')) %>% | |
rename(x = lon, y = lat) %>% | |
inner_join(nodes %>% select(id, lon, lat), by = c('to' = 'id')) %>% | |
rename(xend = lon, yend = lat) | |
assert_that(nrow(edges_for_plot) == nrow(edges)) | |
# use the node degree for scaling the node sizes | |
nodes$weight = degree(g) | |
# common plot theme | |
maptheme <- theme(panel.grid = element_blank()) + | |
theme(axis.text = element_blank()) + | |
theme(axis.ticks = element_blank()) + | |
theme(axis.title = element_blank()) + | |
theme(legend.position = "bottom") + | |
theme(panel.grid = element_blank()) + | |
theme(panel.background = element_rect(fill = "#596673")) + | |
theme(plot.margin = unit(c(0, 0, 0.5, 0), 'cm')) | |
# common polygon geom for plotting the country shapes | |
country_shapes <- geom_polygon(data = map_data('world'), aes(x = long, y = lat, group = group), | |
fill = "#CECECE", color = "#515151", size = 0.15) | |
# common coordinate system for all the following plots | |
mapcoords <- coord_fixed(xlim = c(-150, 180), ylim = c(-55, 80)) | |
# ------------------------------- # | |
# Solution 1: ggplot + ggmap only # | |
# ------------------------------- # | |
# try to plot with scaled edge widths and node sizes | |
# this will fail because we can only use the "size" aesthetic twice | |
ggplot(nodes) + country_shapes + | |
geom_curve(aes(x = x, y = y, xend = xend, yend = yend, # draw edges as arcs | |
color = category, size = weight), | |
data = edges_for_plot, curvature = 0.33, alpha = 0.5) + | |
scale_size_continuous(guide = FALSE, range = c(0.25, 2)) + # scale for edge widths | |
geom_point(aes(x = lon, y = lat, size = weight), # draw nodes | |
shape = 21, | |
fill = 'white', color = 'black', stroke = 0.5) + | |
scale_size_continuous(guide = FALSE, range = c(1, 6)) + # scale for node size | |
geom_text(aes(x = lon, y = lat, label = name), # draw text labels | |
hjust = 0, nudge_x = 1, nudge_y = 4, | |
size = 3, color = "white", fontface = "bold") + | |
mapcoords + maptheme | |
# Results in warning: "Scale for 'size' is already present. Adding another scale for | |
# 'size', which will replace the existing scale." | |
# now a plot with static node size: | |
ggplot(nodes) + country_shapes + | |
geom_curve(aes(x = x, y = y, xend = xend, yend = yend, # draw edges as arcs | |
color = category, size = weight), | |
data = edges_for_plot, curvature = 0.33, alpha = 0.5) + | |
scale_size_continuous(guide = FALSE, range = c(0.25, 2)) + # scale for edge widths | |
geom_point(aes(x = lon, y = lat), # draw nodes | |
shape = 21, size = 3, | |
fill = 'white', color = 'black', stroke = 0.5) + | |
geom_text(aes(x = lon, y = lat, label = name), # draw text labels | |
hjust = 0, nudge_x = 1, nudge_y = 4, | |
size = 3, color = "white", fontface = "bold") + | |
mapcoords + maptheme | |
# ------------------------------------ # | |
# Solution 2: ggplot2 + ggmap + ggraph # | |
# ------------------------------------ # | |
# prepare layout: use "manual" layout with geo-coordinates | |
node_pos <- nodes %>% select(lon, lat) %>% rename(x = lon, y = lat) | |
lay <- create_layout(g, 'manual', node.positions = node_pos) | |
assert_that(nrow(lay) == nrow(nodes)) | |
# use the node degree for scaling the node sizes | |
lay$weight <- degree(g) | |
ggraph(lay) + country_shapes + | |
geom_edge_arc(aes(color = category, edge_width = weight, # draw edges as arcs | |
circular = FALSE), | |
data = edges_for_plot, curvature = 0.33, alpha = 0.5) + | |
scale_edge_width_continuous(range = c(0.5, 2), # scale for edge widths | |
guide = FALSE) + | |
geom_node_point(aes(size = weight), shape = 21, # draw nodes | |
fill = "white", color = "black", | |
stroke = 0.5) + | |
scale_size_continuous(range = c(1, 6), guide = FALSE) + # scale for node widths | |
geom_node_text(aes(label = name), repel = TRUE, size = 3, | |
color = "white", fontface = "bold") + | |
mapcoords + maptheme | |
# --------------------------------------------------------------- # | |
# Solution 3: the hacky way (overlay several ggplot "plot grobs") | |
# --------------------------------------------------------------- # | |
theme_transp_overlay <- theme( | |
panel.background = element_rect(fill = "transparent", color = NA), | |
plot.background = element_rect(fill = "transparent", color = NA) | |
) | |
# the base plot showing only the world map | |
p_base <- ggplot() + country_shapes + mapcoords + maptheme | |
# first overlay: edges as arcs | |
p_edges <- ggplot(edges_for_plot) + | |
geom_curve(aes(x = x, y = y, xend = xend, yend = yend, # draw edges as arcs | |
color = category, size = weight), | |
curvature = 0.33, alpha = 0.5) + | |
scale_size_continuous(guide = FALSE, range = c(0.5, 2)) + # scale for edge widths | |
mapcoords + maptheme + theme_transp_overlay + | |
theme(legend.position = c(0.5, -0.1), legend.direction = "horizontal") | |
# second overlay: nodes as points | |
p_nodes <- ggplot(nodes) + | |
geom_point(aes(x = lon, y = lat, size = weight), | |
shape = 21, fill = "white", color = "black", # draw nodes | |
stroke = 0.5) + | |
scale_size_continuous(guide = FALSE, range = c(1, 6)) + # scale for node size | |
geom_text(aes(x = lon, y = lat, label = name), # draw text labels | |
hjust = 0, nudge_x = 1, nudge_y = 4, | |
size = 3, color = "white", fontface = "bold") + | |
mapcoords + maptheme + theme_transp_overlay | |
# combine the overlays to a full plot | |
# proper positioning of the grobs can be tedious... I found that | |
# using `ymin` works quite well but manual tweeking of the | |
# parameter seems necessary | |
p <- p_base + | |
annotation_custom(ggplotGrob(p_edges), ymin = -74) + | |
annotation_custom(ggplotGrob(p_nodes), ymin = -74) | |
print(p) | |
Hi, many thanks for the awesome script. I'm using it to show migration routes in Australia. However, I am trying to let the curves not start/end in the specified locations to make it easier for the reader to see where the arrows go. I tried a lot but nothing quite seems to work. I used the hacky way to accomplish the below plot (Solution 3). Any help is highly appreciated.
2022 here: For anyone else getting an error on node.positions, it seems to be a breaking change since this version (from 2018). I changed "node.positions" to "layout" in the code which helped.
Another error for the ggraph (method number 2 here) also gave an error that it could not find the object "edge.id". I just added that column manually to the edges_for_plot dataframe before running the geom_edge_arc like this:
edges_for_plot$edge.id <- c(1:38) #so now it has a column edge.id
Then ran the final ggraph plot:
ggraph(lay) + country_shapes +
geom_edge_arc(aes(color = category, edge_width = weight, # draw edges as arcs
circular = FALSE),
data = edges_for_plot, strength = 0.33, ###curvature is apparently changed to strength in the new ggraph version
alpha = 0.5) # and you can add the rest of the options in the original code here
Very helpful! Is there a way to control the position of arrows, say put it at the middle of a line? This can avoid too many arrows overlaying with each other when they point to the same location. Thanks!
very helpful. like it. thank you very much.