Animated Directional Chord Diagrams

Background

A little while ago my paper in International Migration Review on global migration flow estimates came out online. The paper includes a number of directional chord diagrams to visualize the estimates.

Recently I have been playing around tweenr and the magick packages for animated population pyramids. In this post I attempt to show how to use these packages to produce animated directional chord diagrams of global migration flow estimates

Data Prep

The first step is to read into R two data frames (these are in my migest R package if you wish to replicate the code below).

  1. Time series of bilateral migration flow estimates:
# install.packages("migest")
library(tidyverse)
d0 <- read_csv(system.file("imr", "reg_flow.csv", package = "migest"))
d0 
## # A tibble: 891 x 4
##    year0     orig_reg                      dest_reg    flow
##    <int>        <chr>                         <chr>   <dbl>
##  1  1960       Africa                        Africa 1377791
##  2  1960       Africa                  Eastern Asia    5952
##  3  1960       Africa Eastern Europe & Central Asia    7303
##  4  1960       Africa                        Europe  919252
##  5  1960       Africa     Latin America & Caribbean   15796
##  6  1960       Africa              Northern America   82463
##  7  1960       Africa                       Oceania   32825
##  8  1960       Africa                 Southern Asia   35603
##  9  1960       Africa                  Western Asia  106580
## 10  1960 Eastern Asia                        Africa   37301
## # ... with 881 more rows
  1. Some regional meta data for chord diagram plots:
d1 <- read_csv(system.file("vidwp", "reg_plot.csv", package = "migest"))
d1
## # A tibble: 9 x 5
##                          region order1    col1           reg1
##                           <chr>  <int>   <chr>          <chr>
## 1              Northern America      1 #40A4D8       Northern
## 2                        Africa      2 #33BEB7         Africa
## 3                        Europe      3 #B2C224         Europe
## 4 Eastern Europe & Central Asia      4 #FECC2F Eastern Europe
## 5                  Western Asia      5 #FBA127        Western
## 6                 Southern Asia      6 #F66320       Southern
## 7                  Eastern Asia      7 #DB3937        Eastern
## 8                       Oceania      8 #A463D7        Oceania
## 9     Latin America & Caribbean      9 #0C5BCE  Latin America
## # ... with 1 more variables: reg2 <chr>

Tween Data

The next step is to tween the data by migration corridor.

library(tweenr)

d2 <- d0 %>%
  mutate(corridor = paste(orig_reg, dest_reg, sep = " -> ")) %>%
  select(corridor, year0, flow) %>%
  mutate(ease = "linear") %>%
  tween_elements(time = "year0", group = "corridor", ease = "ease", nframes = 100) %>%
  tbl_df()
d2
## # A tibble: 8,181 x 4
##    year0    flow .frame                                  .group
##  * <dbl>   <dbl>  <int>                                  <fctr>
##  1  1960 1377791      0                        Africa -> Africa
##  2  1960    5952      0                  Africa -> Eastern Asia
##  3  1960    7303      0 Africa -> Eastern Europe & Central Asia
##  4  1960  919252      0                        Africa -> Europe
##  5  1960   15796      0     Africa -> Latin America & Caribbean
##  6  1960   82463      0              Africa -> Northern America
##  7  1960   32825      0                       Africa -> Oceania
##  8  1960   35603      0                 Africa -> Southern Asia
##  9  1960  106580      0                  Africa -> Western Asia
## 10  1960   37301      0                  Eastern Asia -> Africa
## # ... with 8,171 more rows

This creates larger data frame d2, with 100 observations for each corridor, one for each frame in the animation. In the original data d0 there are only 11 observations for each corridor, one for each five-year period.

Then some further minor data wrangling is required to ready the data for plotting using the chordDiagram function; namely the first three columns in the data must correspond to the origin, destination and flow.

d2 <- d2 %>%
  separate(col = .group, into = c("orig_reg", "dest_reg"), sep = " -> ") %>%
  select(orig_reg, dest_reg, flow, everything()) %>%
  mutate(flow = flow/1e06)
d2
## # A tibble: 8,181 x 5
##        orig_reg                      dest_reg     flow year0 .frame
##           <chr>                         <chr>    <dbl> <dbl>  <int>
##  1       Africa                        Africa 1.377791  1960      0
##  2       Africa                  Eastern Asia 0.005952  1960      0
##  3       Africa Eastern Europe & Central Asia 0.007303  1960      0
##  4       Africa                        Europe 0.919252  1960      0
##  5       Africa     Latin America & Caribbean 0.015796  1960      0
##  6       Africa              Northern America 0.082463  1960      0
##  7       Africa                       Oceania 0.032825  1960      0
##  8       Africa                 Southern Asia 0.035603  1960      0
##  9       Africa                  Western Asia 0.106580  1960      0
## 10 Eastern Asia                        Africa 0.037301  1960      0
## # ... with 8,171 more rows

Plots for Each Frame

Now the data is in the correct format, chord diagrams can be produced for each frame of the eventual GIF. To do this, I used a for loop to cycle through the tweend data. The arguments I used in the circos.par, chordDiagram and circos.track functions to produce each plot are explained in more detail in the comments of the migest demo.

# create a directory to store the individual plots
dir.create("./plot-gif/")

library(circlize)
for(f in unique(d2$.frame)){
  # open a PNG plotting device
  png(file = paste0("./plot-gif/globalchord", f, ".png"), height = 7, width = 7, 
      units = "in", res = 500)
  
  # intialise the circos plot
  circos.clear()
  par(mar = rep(0, 4), cex=1)
  circos.par(start.degree = 90, track.margin=c(-0.1, 0.1), 
             gap.degree = 4, points.overflow.warning = FALSE)

  # plot the chord diagram
  chordDiagram(x = filter(d2, .frame == f), directional = 1, order = d1$region,
               grid.col = d1$col1, annotationTrack = "grid",
               transparency = 0.25,  annotationTrackHeight = c(0.05, 0.1),
               direction.type = c("diffHeight", "arrows"), link.arr.type = "big.arrow",
               diffHeight  = -0.04, link.sort = TRUE, link.largest.ontop = TRUE)
  
  # add labels and axis
  circos.track(track.index = 1, bg.border = NA, panel.fun = function(x, y) {
    xlim = get.cell.meta.data("xlim")
    sector.index = get.cell.meta.data("sector.index")
    reg1 = d1 %>% filter(region == sector.index) %>% pull(reg1)
    reg2 = d1 %>% filter(region == sector.index) %>% pull(reg2)
    
    circos.text(x = mean(xlim), y = ifelse(is.na(reg2), 3, 4),
                labels = reg1, facing = "bending", cex = 1.1)
    circos.text(x = mean(xlim), y = 2.75, labels = reg2, facing = "bending", cex = 1.1)
    circos.axis(h = "top", labels.cex = 0.8
                labels.niceFacing = FALSE, labels.pos.adjust = FALSE)
  })
  
  # close plotting device
  dev.off()
}

Creating a GIF

Using the magick package a GIF can be created by using the code below to

  1. Read in an initial plot and then combine together all other images created above.
  2. Scale the combined images.
  3. Animate the combined images and save as a .gif.
library(magick)

img <- image_read(path = "./plot-gif/globalchord0.png")
for(f in unique(d2$.frame)[-1]){
  img0 <- image_read(path = paste0("./plot-gif/globalchord",f,".png"))
  img <- c(img, img0)
  message(f)
}

img1 <- image_scale(image = img, geometry = "720x720")

ani0 <- image_animate(image = img1, fps = 10)
image_write(image = ani0, path = "./plot-gif/globalchord.gif")

This gives an output much like this minus the additional details in the corners:

(might take a few seconds to fully load)

Fixing Scales in Chord Diagrams

Whilst the plot above allows comparisons of the distributions of flows overtime it is more difficult to compare volumes. For such comparisons, Zuguang Gu suggests scaling the gaps between the sectors on the outside of the chord diagram. I wrote a little function that can do this for flow data arranged in a tidy format;

scale_gap <- function(flow_m, flow_max, gap_at_max = 1, gaps = NULL) {
  p <- flow_m / flow_max
  if(length(gap_at_max) == 1 & !is.null(gaps)) {
    gap_at_max <- rep(gap_at_max, gaps)
  }
  gap_degree <- (360 - sum(gap_at_max)) * (1 - p)
  gap_m <- (gap_degree + sum(gap_at_max))/gaps
  return(gap_m)
}

where

  • flow_m is the size of total flows in the matrix for the given year being re-scaled.
  • flow_max is the maximum size of the flow matrix over all years
  • gap_at_max is the size in degrees of the gaps in the flow matrix in the year where the flows are at their all time maximum.
  • gaps is the number of gaps in the chord diagram (i.e.┬áthe number of regions).

The function can be used to derive the size of gaps in each frame for a new animated GIF.

d3 <- d2 %>%
  group_by(.frame) %>%
  summarise(flow = sum(flow)) %>%
  mutate(gaps = scale_gap(flow_m = flow, flow_max = max(.$flow), 
                          gap_at_max = 4, gaps = 9))

d3
## # A tibble: 101 x 3
##    .frame     flow     gaps
##     <int>    <dbl>    <dbl>
##  1      0 17.63682 25.91515
##  2      1 17.84989 25.74499
##  3      2 18.06297 25.57483
##  4      3 18.27604 25.40467
##  5      4 18.48911 25.23451
##  6      5 18.70219 25.06435
##  7      6 18.91526 24.89418
##  8      7 19.12834 24.72402
##  9      8 19.34141 24.55386
## 10      9 19.55448 24.38370
## # ... with 91 more rows

The calculations in d3 can then be plugged into the for loop above, where the circos.par() function is replaced by

circos.par(start.degree = 90, track.margin = c(-0.1, 0.1),
           gap.degree = filter(d3, .frame == f)$gaps, 
           points.overflow.warning = FALSE)

Once the for loop has produced a new set of images, the same code to produce the GIF file can be run to obtain the animated chord diagrams with changing gaps;

Whilst the sector axes are now fixed, I am not convinced that changing the relative gaps is the best way to compare volumes when using animated chord diagrams. The sectors of all regions - bar Northern America - are rotating making it hard follow their changes over time.

Fortunately there is new xmax option in chordDiagram that can be used to fix the lengths of the x-axis for each sector using a named vector. In the context of producing an animation, the historic maximum migration flows (of combined immigration and emigration flows) in each region can be used, calculated from the original data d0

library(magrittr)

reg_max <- d0 %>%
  group_by(year0, orig_reg) %>%
  mutate(tot_out = sum(flow)) %>%
  group_by(year0, dest_reg) %>%
  mutate(tot_in = sum(flow)) %>%
  filter(orig_reg == dest_reg) %>%
  mutate(tot = tot_in + tot_out) %>%
  mutate(reg = orig_reg) %>%
  group_by(reg) %>%
  summarise(tot_max = max(tot)/1e06) %$%
  'names<-'(tot_max, reg)

reg_max
##                        Africa                  Eastern Asia 
##                     17.429942                     14.805479 
## Eastern Europe & Central Asia                        Europe 
##                      8.361300                     15.536978 
##     Latin America & Caribbean              Northern America 
##                      7.697638                     10.416927 
##                       Oceania                 Southern Asia 
##                      2.968412                     15.067631 
##                  Western Asia 
##                     15.072561

The reg_max object can then be used in the chordDiagram function in the for loop above, replacing the original call with

chordDiagram(x = filter(d2, .frame == f), directional = 1, order = d1$region,
             grid.col = d1$col1, annotationTrack = "grid",
             transparency = 0.25,  annotationTrackHeight = c(0.05, 0.1),
             direction.type = c("diffHeight", "arrows"), link.arr.type = "big.arrow",
             diffHeight  = -0.04, link.sort = TRUE, link.largest.ontop = TRUE, 
             xmax = reg_max)

Running the complete code - the adapted for loop to produce the images and then the magick functions to compile the GIF - results in the following animation:

comments powered by Disqus