Thursday, August 2, 2012

Visualizing data from a PDF

In my previous post, I scraped the data from a PDF and wrote it to a CSV file. I uploaded the data to CartoDB to make a quick map using the "Share This Map" function on the CartoDB site.



I received a number of comments on the map, mostly about making it more legible by separating out the years and providing more information about each incident. In the spirit of Brian Timoney's post on how web maps are actually used, I decided to keep the map as simple as possible.



After reading the API documentation and looking at the examples, I used the Data Interaction example as the starting point for my map. One of the comments I received was to separate the incidents by year. The interesting part of the example is that it shows how to dynamically change what is presented on a map by changing the values in a SQL query. That's very powerful because you have the data handling and geoprocessing capabilities of PostgreSQL and PostGIS available in your map client. Here's the map with a control to show incidents by year. (The blog form squeezes the map, the see the full map here.)



The script for the map is below.


<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01//EN"
"http://www.w3.org/TR/html4/strict.dtd">
<html lang="en">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<title>Major Shootings from 2005 to 2012</title>
<meta name="generator" content="TextMate http://macromates.com/">
<meta name="author" content="Sophia Parafina">
<!-- Date: 2012-08-01 -->
<link href="./css/example.css" media="screen" rel="stylesheet" type="text/css" />
<noscript><meta http-equiv="refresh" content="0;url=/no_javascript.html"></noscript>
</head>
<body>
<br />
<h1>Major Shootings from 2005 - 2012</h1>
<p><a href="http://www.bradycampaign.org/xshare/pdf/major-shootings.pdf">Data</a> from the Brady Campaign To Prevent Gun Violence.</p>
<script src="http://ajax.googleapis.com/ajax/libs/jquery/1.7.1/jquery.min.js" type="text/javascript"></script>
<script src="http://maps.googleapis.com/maps/api/js?sensor=false&libraries=drawing" type="text/javascript"></script>
<script type="text/javascript">
var overlay, cartodb_imagemaptype,
map = null,
user = "spara",
table = "brady_campaign_shootings",
year = '2012',
zoom = 4,
lat = 36.828175,
lng = -98.5795;
var resetLayer = function() {
// Add the cartodb tiles
map.overlayMapTypes.insertAt(0, new google.maps.ImageMapType(cartoDBLayer));
map.overlayMapTypes.pop(1);
}
var cartoDBLayer = {
getTileUrl: function(coord, zoom) {
var sql = encodeURIComponent("SELECT * FROM brady_campaign_shootings where year like '" + year +"'");
return "http://"+user+".cartodb.com/tiles/"+table+"/"+zoom+"/"+coord.x+"/"+coord.y+".png?sql=" + sql;
},
tileSize: new google.maps.Size(256, 256)
};
$(function() {
// Define the basic map options
var cartodbMapOptions = {
zoom: zoom,
zoomControl: true,
zoomControlOptions: {
style: google.maps.ZoomControlStyle.SMALL,
position: google.maps.ControlPosition.TOP_LEFT
},
center: new google.maps.LatLng( lat, lng ),
disableDefaultUI: true,
mapTypeId: google.maps.MapTypeId.ROADMAP
}
// Init the map
map = new google.maps.Map(document.getElementById("map"), cartodbMapOptions);
var mapStyle = [{
stylers: [{ saturation: -65 }, { gamma: 1.52 }] }, {
featureType: "administrative", stylers: [{ saturation: -95 }, { gamma: 2.26 }] }, {
featureType: "water", elementType: "labels", stylers: [{ visibility: "off" }] }, {
featureType: "administrative.locality", stylers: [{ visibility: 'off' }] }, {
featureType: "road", stylers: [{ visibility: "simplified" }, { saturation: -99 }, { gamma: 2.22 }] }, {
featureType: "poi", elementType: "labels", stylers: [{ visibility: "off" }] }, {
featureType: "road.arterial", stylers: [{ visibility: 'off' }] }, {
featureType: "road.local", elementType: "labels", stylers: [{ visibility: 'off' }] }, {
featureType: "transit", stylers: [{ visibility: 'off' }] }, {
featureType: "road", elementType: "labels", stylers: [{ visibility: 'off' }] }, {
featureType: "poi", stylers: [{ saturation: -55 }]
}];
// Set the map style
map.setOptions({ styles: mapStyle });
// Add the CartoDB tiles
map.overlayMapTypes.insertAt(0, new google.maps.ImageMapType(cartoDBLayer));
// Bind the buttons
$('.buttons button').click(function(){
$(this).focus();
year = $(this).val();
$(this).closest('div').find('button.selected').removeClass('selected');
$(this).addClass('selected');
resetLayer();
});
});
</script>
<div class="buttons">
<b>Year</b> and Number of Shootings<br />
<button type="button" value="2005" class="first"><b>2005:</b> 5</button>
<button type="button" value="2006"><b>2006:</b> 11</button>
<button type="button" value="2007"><b>2007:</b> 27</button>
<button type="button" value="2008"><b>2008:</b> 126</button>
<button type="button" value="2009"><b>2009:</b> 158</button>
<button type="button" value="2010"><b>2010:</b> 70</button>
<button type="button" value="2011"><b>2011:</b> 39</button>
<button class="selected" autofocus type="button" value="2012"><b>2012:</b> 22</button>
<button type="button" value="%20%" class="last"><b>All:</b> 458</button>
</div>
<div id="map"></div>
</body>
</html>

One of the nice things about the map is that it doesn't use external mapping client libraries. It's simple and shows the user where the incidents occurred. However, I would like to show more information about each incident through a popup. CartoDB plays well with leaflet, modestmaps.js, and wax, and it's easy to extend the basic map with these libraries. In the following map, I used leaflet and the CartoDB popup script to make a popup containing the description of each incident when a marker is clicked. (As with the other map, the full size map can be viewed here.)



The script for the clickable map is below:


<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01//EN"
"http://www.w3.org/TR/html4/strict.dtd">
<html lang="en">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<title>untitled</title>
<meta name="generator" content="TextMate http://macromates.com/">
<meta name="author" content="Sophia Parafina">
<!-- Date: 2012-08-02 -->
<link rel="shortcut icon" href="http://cartodb.com/assets/favicon.ico" />
<style>body,html {width:100%; height:100%; margin:0; padding:0;} #map {position:relative; margin:0; width:100%; height:100%;}</style>
<link rel="stylesheet" href="http://cdn.leafletjs.com/leaflet-0.3.1/leaflet.css" />
<!--[if IE]><link rel="stylesheet" href="http://cdn.leafletjs.com/leaflet-0.3.1/leaflet.ie.css" /><![endif]-->
<link href="./css/example.css" media="screen" rel="stylesheet" type="text/css" />
<link href="./css/style.css" rel="stylesheet" type="text/css">
<link href="./css/cartodb-leaflet.css" rel="stylesheet" type="text/css">
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.7.2/jquery.min.js"></script>
<script src="./scripts/leaflet.js"></script>
<script type="text/javascript" src="./scripts/wax.leaf-7.0.0dev1.js"></script>
<script type="text/javascript" src="./scripts/cartodb-leaflet-min.js"></script>
</head>
<body>
<h1>Major Shootings from 2005 - 2012</h1>
<p><a href="http://www.bradycampaign.org/xshare/pdf/major-shootings.pdf">Data</a> from the Brady Campaign To Prevent Gun Violence.</p>
<div class="buttons">
<b>Year</b> and Number of Shootings<br />
<button type="button" value="= 2005" class="first"><b>2005:</b> 5</button>
<button type="button" value="= 2006"><b>2006:</b> 11</button>
<button type="button" value="= 2007"><b>2007:</b> 27</button>
<button type="button" value="= 2008"><b>2008:</b> 126</button>
<button type="button" value="= 2009"><b>2009:</b> 158</button>
<button type="button" value="= 2010"><b>2010:</b> 70</button>
<button type="button" value="= 2011"><b>2011:</b> 39</button>
<button class="selected" autofocus type="button" value="= 2012"><b>2012:</b> 22</button>
<button type="button" value="> 2004" class="last"><b>All:</b> 458</button>
</div>
<div id="map"></div>
<script type="text/javascript" src="./scripts/cartodb-popup-min.js"></script>
<script type="text/javascript">
var resetLayer = function() {
// Add the cartodb tiles
var qry = "SELECT * FROM {{table_name}} WHERE date_part('YEAR',date) " + year;
brady.setOptions({query: qry, interactivity: "description"});
}
var map = new L.Map('map').setView(new L.LatLng(37.828175, -98.5795), 4)
, mapboxUrl = 'http://{s}.tiles.mapbox.com/v3/cartodb.map-1nh578vv/{z}/{x}/{y}.png'
, mapbox = new L.TileLayer(mapboxUrl, {maxZoom: 18, attribution: "Powered by Leaflet and Mapbox"});
map.addLayer(mapbox,true);
// Create a CartoDB popup
var popup = new L.CartoDBPopup()
// First cartodb layer, countries
var year = '';
var brady = new L.CartoDBLayer({
map: map,
user_name:'spara',
table_name: 'brady_campaign_shootings',
query: "SELECT * FROM {{table_name}} WHERE date_part('YEAR',date) = 2012",
opacity:0.8,
interactivity: "description, date",
featureOver: function(ev,latlng,pos,data) {
document.body.style.cursor = "pointer";
},
featureOut: function() {
document.body.style.cursor = "default";
},
featureClick: function(ev,latlng,pos,data) {
if (typeof( window.event ) != "undefined" ) {
// IE
ev.cancelBubble=true;
} else {
// Rest
ev.preventDefault();
ev.stopPropagation();
}
// Set popup content
popup.setContent(data);
// Set latlng
popup.setLatLng(latlng);
// Show it!
map.openPopup(popup);
},
auto_bound: false,
debug: true
});
// Adding layer to map
map.addLayer(brady);
// Bind the buttons
$('.buttons button').click(function(){
$(this).focus();
year = $(this).val();
$(this).closest('div').find('button.selected').removeClass('selected');
$(this).addClass('selected');
resetLayer();
});
</script>
</body>
</html>

The script essentially the same as the previous script but it uses leaflet.js to add a base map from MapBox and the leaflet-carto.js to add the Major Shootings layer from CartoDB. Switching between years is handled differently from the previous example. The resetLayer function uses the setOptions function to update the query for each year. Note that I cheated a little and put the operator as part of the value, e.g. '= 2005". It was the most direct solution to handling the 'all' case in the SQL statement, which required that query for all the records as part of a SQL WHERE clause. I admit that putting the number of incidents on the buttons is cheesy; and if I had more time, I would put a label that would change on top of the map that read "x incidents in xxxx" when a different year is selected.. 


I like CartoDB because it's flexible and agnostic with regards to client mapping libraries. Building a simple mapHowever, what I find exciting is that CartoDB puts an API in front of PostgreSQL and PostGIS. This opens up all sorts of possibilities and removes the need to preprocess or reformat data to create a visualization.


The project file is available here.