Twitter: raymondcamden


Address: Lafayette, LA, USA

Snap.svg demo - Census Data

10-28-2013 8,128 views Development, jQuery, JavaScript, HTML5 1 Comment

Last week I blogged about the release of Snap.svg, a new library for modern browsers to simplify the creation, usage, and animation of SVG assets. Over the past few days I've worked on a new demo of Snap.svg that I'd like to share with you.

My demo makes use of the American Census data API, specifically a collection of data from 2010. The Census API has - in my opinion - pretty poor documentation. It was pretty painful figuring stuff out. Their support forum though is active and I was able to get help there. Once I got over a few hurdles it was relatively simple to figure out the rest.

I began with a simple SVG map of America (you can view this here). I found this on Wikipedia (and unfortunately forgot to copy the source URL) and while it was pretty good as is, for some reason the ID for Virginia was left out. I then dug up a JSON file for American states (credit: https://gist.github.com/mshafrir/2646763) to give me a list of states I could work with.

At this point, I had everything I needed. My blank map. My list of states. And finally - Snap.svg itself to give me a library to work with it together. Next, I began work on a simple Census wrapper. I only built the bare minimum of what I needed for the demo and it is focused on getting data for every state, but in theory I could take this and expand upon it later. While you can obviously view source, here is the library.

/* global $ */
var CensusLib = function() {
	var apikey;
	
	function getBaseURL() {
		return "http://api.census.gov/data/2010/acs5?key="+apikey;
	}
	function setAPIKey(key) {
		apikey = key;
	}
	
	//Data translator:
	//I do a few things to the result set.
	function translate(d,valuekey) {
		var result = [];
		var i,len;
		//the 'header' is the first row
		var header = [];
		for(i=0; i<d[0].length; i++) {
			header.push(d[0][i]);
		}
		for(i=1,len=d.length; i<len; i++) {
			var item = {};
			for(var j=0;j<header.length;j++) {
				if(header[j] === valuekey) {
					item.value = Number(d[i][j]);
				} else {
					item[header[j]] = d[i][j];
				}
			}
			result.push(item);
		}
		return result;
	}
	
	function getMedianAge(cb) {
		var url = getBaseURL() + "&get=B01002_001E,NAME&for=state:*";
		$.get(url, {}, function(res,code) {
			var data = translate(res,"B01002_001E");
			cb(data);
		}, "JSON");
		
	}
	
	function getMedianIncome(cb) {
		var url = getBaseURL() + "&get=B19013_001E,NAME&for=state:*";
		$.get(url, {}, function(res,code) {
			var data = translate(res,"B19013_001E");
			cb(data);
		}, "JSON");		
	}
	
	//Utility function that returns min/max based on census data
	function getRangeFromData(d) {
		var result = {min:Number.POSITIVE_INFINITY, max:Number.NEGATIVE_INFINITY};
		for(var i=0, len=d.length; i<len; i++) {
			if(d[i].value > result.max) result.max = d[i].value;
			if(d[i].value < result.min) result.min = d[i].value;
		}
		return result;
	}
	
	return {
		setAPI:setAPIKey,
		getMedianAge:getMedianAge,
		getMedianIncome:getMedianIncome,
		getRangeFromData:getRangeFromData
	};
	
}();

For the most part, the Snap.svg portion is pretty trivial (which I think is a sign of a good library). On startup, I load in my map. I also make a list of State fragments (essentially pointers) so I can use them later.

Snap.load("Blank_US.svg", function(f) {

	for(var s in states) {
		stateFragments[s] = f.select("#"+s);
	}

	snapOb.append(f);

	start();

});

Finally, here is an example of one of the datasets. This is called when the button is clicked to load the data. Note I'm using local storage to cache the census data.

function doMedianAge() {
	var cachedData = window.localStorage.getItem("census_median");
	
	if(cachedData) {
		renderMedianAge(JSON.parse(cachedData));
	} else {
		CensusLib.getMedianAge(function(data) {
			console.log('back from census');
			window.localStorage.setItem("census_median", JSON.stringify(data));
			renderMedianAge(data);
		});
	}
	
	
	function renderMedianAge(data) {
		//First - figure out min/max values
		var range = CensusLib.getRangeFromData(data);
		var lowerBound = Math.floor(range.min/10)*10;
		var upperBound = Math.ceil(range.max/10)*10;
		//To Do: Handle tooltips for data
		//Begin rendering
		var diff = upperBound-lowerBound;

		for(var state in stateFragments) {
			//some states (territories) not there
			if(stateFragments[state]) {
				//console.log("Doing "+state);
				var value = findValueInData(data, state);
				//color is from white to black with white == lowerBound, black == upperBound
				var perc = Math.round(((value-lowerBound)/diff)*100);
				stateFragments[state].animate({"fill":"000000","fill-opacity":perc/100},500);
			} 
		}
		
		legendBlock.children().hide();
		comparitiveAgeText.show();
		$(".leftLabel").text(lowerBound);
		$(".rightLabel").text(upperBound);
		
	}
	
}

And that's it. As I said above - the Snap.svg portion of this isn't terribly exciting - but that's the way it should be in my opinion. There is one thing missing here that I'm currently researching. SVG elements support a title attribute that gives hover text on mouseover. Currently though I've not been able to get Snap.svg to handle adding this dynamically. Anyway, check it out. Take special note of California and Florida. CA has a younger population with a higher income while Florida is almost the exact opposite.

p.s. Yes, this works on mobile, at least iOS. The buttons aren't optimized for touch on a small screen so they are a pain to click, but outside of that the demo works great.

Related Blog Entries

1 Comment

  • John #
    Commented on 10-29-2013 at 12:44 PM
    Thanks again for your help. By turning on some of the web developer tools I was able to trace the problem to my IIS web server not being configured to serve SVG. Now, I can better appreciate these samples you're creating. I'll be back.
    jg

Post Reply

Please refrain from posting large blocks of code as a comment. Use Pastebin or Gists instead. Text wrapped in asterisks (*) will be bold and text wrapped in underscores (_) will be italicized.

Leave this field empty