Writing about visualization, demographics, dashboards, and spatial data science.

Interested in learning more? Hire me for a workshop or to consult on your next project. See the Services page for more details.

Interactive flow visualization in R

· by Kyle Walker · Read in about 5 min · (1004 Words)
R

Exploring flows between origins and destinations visually is a common task, but can be difficult to get right. In R, there are many tutorials on the web that show how to produce static flow maps (see here, here, here, and here, among others).

Over the past couple years, R developers have created an infrastructure to bridge R with JavaScript using the htmlwidgets package, allowing for the generation of interactive web visualizations straight from R. I’d like to demonstrate here a few examples for exploratory interactive flow graphics that use this infrastructure.

To start, let’s make a random dataset that links countries with US states.

library(dplyr)

set.seed(1983)

df <- data_frame(origins = sample(c('Portugal', 'Romania', 'Nigeria', 'Peru'), 
                                  size = 100, replace = TRUE), 
                 destinations = sample(c('Texas', 'New Jersey', 'Colorado', 'Minnesota'), 
                                       size = 100, replace = TRUE))

head(df)
## # A tibble: 6 × 2
##    origins destinations
##      <chr>        <chr>
## 1  Romania    Minnesota
## 2 Portugal        Texas
## 3 Portugal    Minnesota
## 4  Nigeria    Minnesota
## 5     Peru     Colorado
## 6 Portugal     Colorado

We can use dplyr to get counts of the unique origin-destination pairs as follows:

df2 <- df %>%
  group_by(origins, destinations) %>%
  summarize(counts = n()) %>%
  ungroup() %>%
  arrange(desc(counts))

df2
## # A tibble: 16 × 3
##     origins destinations counts
##       <chr>        <chr>  <int>
## 1  Portugal     Colorado      9
## 2   Romania   New Jersey      9
## 3   Romania    Minnesota      8
## 4   Nigeria     Colorado      7
## 5      Peru     Colorado      7
## 6      Peru    Minnesota      7
## 7  Portugal    Minnesota      7
## 8  Portugal        Texas      7
## 9      Peru   New Jersey      6
## 10  Romania        Texas      6
## 11  Nigeria    Minnesota      5
## 12  Nigeria   New Jersey      5
## 13     Peru        Texas      5
## 14  Romania     Colorado      5
## 15 Portugal   New Jersey      4
## 16  Nigeria        Texas      3

Now, we’ll want to plot the connections. While maps are often a first choice for visualizing geographic flows, they are not the only option. For example, with a little data formatting, the networkD3 package allows for network visualizations like the following:

library(networkD3)

name_vec <- c(unique(df2$origins), unique(df2$destinations))

nodes <- data.frame(name = name_vec, id = 0:7)

links <- df2 %>%
  left_join(nodes, by = c('origins' = 'name')) %>%
  rename(origin_id = id) %>%
  left_join(nodes, by = c('destinations' = 'name')) %>%
  rename(dest_id = id)


forceNetwork(Links = links, Nodes = nodes, Source = 'origin_id', Target = 'dest_id', 
             Value = 'counts', NodeID = 'name', Group = 'id', zoom = TRUE)

Use the scroll wheel on your mouse to zoom in; the width of the links are proportional to the size of the flow. A more appropriate visualization in this circumstance, however, might be a Sankey diagram, which is also available in the networkD3 package:

sankeyNetwork(Links = links, Nodes = nodes, Source = 'origin_id', Target = 'dest_id', 
              Value = 'counts', NodeID = 'name', fontSize = 16)

A similar representation is available in the parsetR package by Kenton Russell, available on GitHub.

library(parsetR) # devtools::install_github("timelyportfolio/parsetR")

parset(df2, dimensions = c('origins', 'destinations'), 
       value = htmlwidgets::JS("function(d){return d.counts}"), 
       tension = 0.5)

Now, let’s create a couple interactive flow maps. To do this, we need to have some sense of where the places are located in geographic space, requiring some spatial data; we’ll use the rnaturalearth package for this, available on GitHub.

library(rnaturalearth) # devtools::install_github('ropenscilabs/rnaturalearth')

countries <- ne_countries()

states <- ne_states(iso_a2 = 'US')

The states data have long/lat information already, but the countries data do not, so we’ll need to calculate it with the rgdal package.

library(rgdal)

countries$longitude <- coordinates(countries)[,1]

countries$latitude <- coordinates(countries)[,2]

countries_xy <- countries@data %>%
  select(admin, longitude, latitude)

states_xy <- states@data %>%
  select(name, longitude, latitude)

Now that we have the XY data, we can merge it to our pre-existing data frame.

df3 <- df2 %>%
  left_join(countries_xy, by = c('origins' = 'admin')) %>%
  left_join(states_xy, by = c('destinations' = 'name'))

df3$longitude.y <- as.numeric(as.character(df3$longitude.y))

df3$latitude.y <- as.numeric(as.character(df3$latitude.y))

head(df3)
## # A tibble: 6 × 7
##    origins destinations counts longitude.x latitude.x longitude.y
##      <chr>        <chr>  <int>       <dbl>      <dbl>       <dbl>
## 1 Portugal     Colorado      9   -8.055766  39.634050   -105.5430
## 2  Romania   New Jersey      9   24.943252  45.857101    -74.4653
## 3  Romania    Minnesota      8   24.943252  45.857101    -93.3640
## 4  Nigeria     Colorado      7    7.995128   9.548318   -105.5430
## 5     Peru     Colorado      7  -74.391806  -9.191563   -105.5430
## 6     Peru    Minnesota      7  -74.391806  -9.191563    -93.3640
## # ... with 1 more variables: latitude.y <dbl>

Looks good. Now, we can use the gcIntermediate function in the geosphere package to calculate great circles.

library(geosphere)

flows <- gcIntermediate(df3[,4:5], df3[,6:7], sp = TRUE, addStartEnd = TRUE)

flows$counts <- df3$counts

flows$origins <- df3$origins

flows$destinations <- df3$destinations

For interactive web maps in R, the leaflet package is a great option. It’ll allow for some interactive exploration of the data, such as the ability to turn on and off layers to see specific flows more clearly.

library(leaflet)
library(RColorBrewer)

hover <- paste0(flows$origins, " to ", 
                flows$destinations, ': ', 
                as.character(flows$counts))

pal <- colorFactor(brewer.pal(4, 'Set2'), flows$origins)

leaflet() %>%
  addProviderTiles('CartoDB.Positron') %>%
  addPolylines(data = flows, weight = ~counts, label = hover, 
               group = ~origins, color = ~pal(origins)) %>%
  addLayersControl(overlayGroups = unique(flows$origins), 
                   options = layersControlOptions(collapsed = FALSE))

The default Mercator projection of most web maps is not ideal for visualizing great circles, however, especially for longer distances. As such, you might want to try an alternative representation of the Earth, such as a three-dimensional globe. This can be accomplished withe the threejs package (available on GitHub), and doesn’t even require the great circle objects we created.

library(threejs) # devtools::install_github("bwlewis/rthreejs")

df4 <- arrange(df3, origins)

df4$colors <- rep(brewer.pal(4, 'Set2'), each = 4)

weights <- 1.5 * df4$counts

arcs <- data.frame(lat1 = df4$latitude.x, lon1 = df4$longitude.x, 
                   lat2 = df4$latitude.y, lon2 = df4$longitude.y)

globejs(arcsLwd = weights, arcs = arcs, arcsColor = df4$colors)