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'
)