2014-02-06

Animated Choropleths

The dataset and maps used in this post are motivated by a recent article by Vivek Patil, where he showed various ways to generate and animate choropleth maps from R.

In this post, I will demonstrate a step-by-step approach to creating an animated, interactive choropleth map using rMaps and DataMaps. I will also show how the entire sequence of steps can be combined into a simple function, that would allow the same choropleth to be generated with a single line of R code!

Before we go ahead, here is what we are shooting for.


Crime Rates (per 100, 000) by State across Years


Get Data

The first step to creating any visualization is getting the data. Let us fetch time-series data on violet crime in the US, from Quandl, which is an excellent source of fascinating datasets. I would strongly recommend that you check it out, if you haven't already :).

library(Quandl)
vcData = Quandl("FBI_UCR/USCRIME_TYPE_VIOLENTCRIMERATE")
kable(head(vcData[,1:9]), format = 'html', table.attr = "class=nofluid")
Year Alabama Alaska Arizona Arkansas California Colorado Connecticut Delaware
2010-12-31 377.8 638.8 408.1 505.3 440.6 320.8 281.4 620.9
2009-12-31 450.1 633.4 426.5 515.8 473.3 338.8 300.9 645.4
2008-12-31 451.3 650.9 481.2 504.6 506.2 347.1 306.5 706.1
2007-12-31 447.9 662.3 514.5 532.6 522.6 350.6 301.1 711.5
2006-12-31 425.2 686.8 545.4 557.2 533.3 394.8 298.6 701.0
2005-12-31 433.0 632.0 512.0 528.0 526.0 397.0 273.0 633.0

Now that we have our data, we need to process it so that it is in the right shape for us to visualize it. In my (limited) experience, this step of getting the data in the right shape is the most challenging part of the visualization process, and can easily consume 50-60% of the overall effort.

Reshape Data

Our dataset is in the wide-form. So, our first step is to convert it into the long-form, as it is usually more convenient for visualization purposes. Additionally, we remove data for the US as a whole, as well as for DC, so that the crime rates across entities (states) are comparable.

library(reshape2)
datm <- melt(vcData, 'Year', 
  variable.name = 'State',
  value.name = 'Crime'
)
datm <- subset(na.omit(datm), 
  !(State %in% c("United States", "District of Columbia"))
)
kable(head(datm), format = 'html', table.attr = "class=nofluid")
Year State Crime
2010-12-31 Alabama 377.8
2009-12-31 Alabama 450.1
2008-12-31 Alabama 451.3
2007-12-31 Alabama 447.9
2006-12-31 Alabama 425.2
2005-12-31 Alabama 433.0

Discretize Crime Rates

Crime rates are continuous numbers and so we first need to discretize them. One way to do this is to divide them into sextiles.

datm2 <- transform(datm,
  State = state.abb[match(as.character(State), state.name)],
  fillKey = cut(Crime, quantile(Crime, seq(0, 1, 1/5)), labels = LETTERS[1:5]),
  Year = as.numeric(substr(Year, 1, 4))
)
kable(head(datm2), format = 'html', table.attr = "class=nofluid")
Year State Crime fillKey
2010 AL 377.8 C
2009 AL 450.1 D
2008 AL 451.3 D
2007 AL 447.9 D
2006 AL 425.2 D
2005 AL 433.0 D

Now that we have discretized crime rates, we need to associate each sextile with a fill color chosen from a palette.

Associate Fill Colors

We use the excellent colorbrewer palettes provided by the RColorBrewer package!. The colors are mapped to the fillKey that we created earlier. We also set a defaultFill, which is used by datamaps to fill entities with no fillKey data.

fills = setNames(
  c(RColorBrewer::brewer.pal(5, 'YlOrRd'), 'white'),
  c(LETTERS[1:5], 'defaultFill')
)

We have one final step before we can start visualizing the data.

Create Payload for DataMaps

The data frame needs to be converted into a list of lists, as it is the default data structure that the DataMaps library accepts. Thanks to Hadley's plyr package, this only requires a few lines of code.

library(plyr); library(rMaps)
dat2 <- dlply(na.omit(datm2), "Year", function(x){
  y = toJSONArray2(x, json = F)
  names(y) = lapply(y, '[[', 'State')
  return(y)
})

After all the hard work (phew!), we are finally ready to start visualizing our data.

Create Simple Choropleth

Let us first create a simple choropleth map of crime rates for a given year. We use the Datamaps reference class, which provides us with simple bindings to the DataMaps library. The code below is fairly self-explanatory.

options(rcharts.cdn = TRUE)
map <- Datamaps$new()
map$set(
  dom = 'chart_1',
  scope = 'usa',
  fills = fills,
  data = dat2[[1]],
  legend = TRUE,
  labels = TRUE
)
map

Having visualized the data for a specific year, the challenge now is to be able to visualize the entire time series. We have several options to do this. One option is to throw this into a Shiny application, which controls the visualization from the server side. The other option is to integrate this with an MVC framework like AngularJS so that all interactivity occurs on the client (read browser!) side. We will explore the second option.

Animated Choropleth

The next few lines of code might look very cryptic to those of you who are not familiar with javascript. But bear with me, and I will try my best to explain it in simpler terms.

Our first step here is to add the requisite javascript files to the page and designate it as an AngularJS application, to be controlled by a javascript function named rChartsCtrl.

map2 = map$copy()
map2$set(
  bodyattrs = "ng-app ng-controller='rChartsCtrl'"
)
map2$addAssets(
  jshead = "http://cdnjs.cloudflare.com/ajax/libs/angular.js/1.2.1/angular.min.js"
)

We then invoke the setTemplate method to modify the default layout in the rCharts template. There are two parts to the modified layout. The first is a div container to hold the map and a slider to control the year. Second, is an rChartsCtrl function that specifies how to update the map when the user interacts the the slider.

map2$setTemplate(chartDiv = "
  <div class='container'>
    <input id='slider' type='range' min=1960 max=2010 ng-model='year' width=200>
    <span ng-bind='year'></span>
    <div id='' class='rChart datamaps'></div>  
  </div>
  <script>
    function rChartsCtrl($scope){
      $scope.year = 1960;
      $scope.$watch('year', function(newYear){
        map.updateChoropleth(chartParams.newData[newYear]);
      })
    }
  </script>"
)

AngularJS makes it really easy to create two-way data bindings. Let me explain how this code works. The input element on the page is a slider input, whose value is bound to the variable year. rChartsCtrl identifies this variable as $scope.year and they are always in sync.

When the user slides the slider, the value of year changes. rChartsCtrl watches for changes in value of year and when it does change, it calls the updateChoropleth function to update the map.

map2$set(newData = dat2)
map2

Note that you can easily modify my code to display the year controls as a dropdown menu or select button group dropdown menu of the years, or display them as select button groups. You can also get very fancy and use an enhanced slider like this one here.

Dropdown

Button Groups

AutoPlay

Now suppose, we want to provide the user with a play button that would automatically animate the choropleth map. We can use a bit of AngularJS magic again and achieve this using the code below. In short, we create an animateMap function that automatically updates the year every 1000 milliseconds, and also updates the choropleth. I added a few more enhancements (not shown in the code) to get the nice play button and continuous legend on top.

map3 = map2$copy()
map3$setTemplate(chartDiv = "
  <div class='container'>
    <button ng-click='animateMap()'>Play</button>
    <div id='chart_1' class='rChart datamaps'></div>  
  </div>
  <script>
    function rChartsCtrl($scope, $timeout){
      $scope.year = 1960;
      $scope.animateMap = function(){
        if ($scope.year > 2010){
          return;
        }
        mapchart_1.updateChoropleth(chartParams.newData[$scope.year]);
        $scope.year += 1
        $timeout($scope.animateMap, 1000)
      }
    }
  </script>"
)
map3

Now you might be thinking that while all this is nice, it still involves writing a lot of code, and more importantly being able to write code in javascript. My primary intention behind presenting all these steps was to show you the flexibility of rMaps, in being able to absorb custom js code. However, it is easy to wrap all of what I did into a simple ichoropleth function that can do all of this in one line of R code.

source('ichoropleth.R')
ichoropleth(Crime ~ State,
  data = datm2[,1:3],
  pal = 'PuRd',
  ncuts = 5,
  animate = 'Year'
)