This post is motivated by a recent article by Vivek Patil, where he showed various ways to generate and animate choropleth maps from R. Here is the end result that we are shooting for :)
Crime Rates (per 100, 000) by State across Years
Get Data
Let us fetch data 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 |
Reshape Data
Our dataset is in the wide-form. So let us first convert it into the long-form, as it is usually more convenient for visualization purposes. We remove data for the US as a whole, as well as for DC, so that the crime rates 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
For choropleth maps, we would like to discretize crime rates. We do this by dividing crime rates 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 |
Choose Fill Colors
We use ColorBrewer to choose a palette for our choropleth map. 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')
)
Create Payload for DataMaps
library(plyr)
library(rCharts)
options(rcharts.cdn = TRUE)
dat2 <- dlply(na.omit(datm2), "Year", function(x){
y = toJSONArray2(x, json = F)
names(y) = lapply(y, '[[', 'State')
return(y)
})
Create Simple Choropleth
map <- Datamaps$new()
map$set(
dom = 'chart_1',
scope = 'usa',
fills = fills,
data = dat2[[1]],
legend = TRUE,
labels = TRUE
)
map
Animated Choropleth
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"
)
map2$setTemplate(chartDiv = "
<div class='container'>
<input id='slider' type='range' min=1960 max=2010 ng-model='year' width=200>
<div id='chart_1' class='rChart datamaps'></div>
</div>
<script>
function rChartsCtrl($scope){
$scope.year = 1960;
$scope.$watch('year', function(newYear){
mapchart_1.updateChoropleth(chartParams.newData[newYear]);
})
}
</script>"
)
map2$set(newData = dat2)
map2
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 by using the code below.
<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 = map2$copy()
<div class='container'>