Notes on svg_mapper, our new vector mapping library

Last Friday, we posted what I suppose we'll call a beta version of svg_mapper, a library of Python code for drawing vector-based maps that will work in both modern and legacy browsers.

A flattened example of a choropleth map

It's code I've been working on, off and on, for more than a year, and you can see earlier incarnations in several CIR and California Watch projects. Anthony Pesce gave a great Lightning Talk at NICAR 2012 in St. Louis a few weeks ago about a similar approach they're using at the Los Angeles Times, which inspired me to finally get off my duff and publish some code.

The open-source version of svg_mapper is more generalized than our earlier projects and is built to handle a range of mapping features. You can have one or many layers, and polygon, linestring and point layers are all supported. You also can use any projection you choose, a big advantage over many tools that allow only Google's Spherical Mercator projection. And a big selling point for me is the ability to use arbitrary sets of spatial data, rather than just standard views like all U.S. states or all California counties.

We've released the code as part of a sample Django project, which includes some sample data to play with.

The code is designed to work with GeoDjango models, but the math inside the heart of the code, svg_map.py, will work with most any coordinates you could throw at it.

The process of building a map should be easy to understand for those already familiar with GeoDjango. More on that below.

When to use it

Put simply: when tiles are overkill.
 
Anyone who works with maps for even a little while is likely to confront a common dilemma. The go-to tools for interactive mapping – the Google Maps JavaScript API and OpenLayers – are great, but for many datasets, all the roads, lakes, city names and more aren't just extraneous – they get in the way of the story you're trying to tell.
 
Lately, we're truly blessed with the additional option of TileMill, which allows users with minimal spatial experience to precisely control what does and doesn't go on their map tiles. But if all you really need is a colored-in map of counties, it seems like a waste of hosting space and processing resources to host multiple layers of tiles that all show the same level of detail. It would be great if in web mapping, we could harness the power of vector graphics – infinite scalability with a much smaller processing and storage footprint.
 

SVG or <canvas/>?

A flattened example of a layered map with polygon, linestring and point layers

Luckily, both SVG and the HTML <canvas/> tag are widely adopted in modern browsers, and both can handle fairly complicated geometries. But sadly, we don't live in a thoroughly modern world. Most sites still have a sizable percentage of users running Internet Explorer 8 and 7, which don't support SVG natively. (There are always workarounds, but they tend to be quirky.)
 
<canvas/> is more widely supported and can do amazing things. But to me, it seems like SVG is a more suitable option for future development of online mapping. First and foremost, I like SVG because SVG has DOM – the elements can be styled with CSS and interacted with through JavaScript. As cool as <canvas/> is, I tend to think of using it for interactivity like playing a game of Battleship.
 

Why Raphaël?

Of all the JavaScript libraries I've tried (Protovis, d3.js, flot), I've come to love Raphaël. It has a full feature set that can be used for mapping, charting or just general SVG drawing. But the best feature is that it has a VML backup system for legacy Internet Explorer users that actually works. With the exception of some type styling, I find I really don't need to write two sets of code – Raphaël handles it. And in the rare cases in which I do need to write VML-specific code, Raphaël has some nice built-in functions for detecting VML versus SVG.
 

The nuts and bolts

The best way to get spatial data into Django is with GeoDjango. You can upload shapefiles into your PostGIS-enabled database and interact with the data as Django objects.
 
The real action in svg_mapper starts in your app's views.py. Our goal is to take our GeoDjango-fied shapefile data and send it to a JSON feed that Raphaël can process in either SVG or VML mode.
 

def choropleth_map_json(request):
    statelist = WisconsinCounty.objects.all()

    themap = SVGMap()
    themap.mapPixelWidth = 1000
    themap.paddingPct = 0.01
    themap.sigdigs = 4

    #build layers in the order you'd like to display them. You could override this at the JS level if you wanted to.
    themap.buildSVGPolygonLayer('wi_counties', statelist, 'simple_mpoly_utm15n', 'countyfp10')

    #Get the maximum extent of all layers
    viewbox = themap.buildSVGMapViewBox()
    #Use the map's extent info to translate all the points to fit inside your pixel width specified above.
    map_layers = themap.translateLayers()

    #return the JSON
    return render_to_response('svg.json', {
        'viewbox': viewbox,
        'map_layers': map_layers,
        },
        context_instance=RequestContext(request))
 
This will spit out JSON that you can parse in JavaScript with Raphaël:
 

var numMainMapWidth = 570;
var numMainMapHeight;

//Used to translate the SVG dimensions to your desired final width/height, and to scale type or other sizes
var numMainMapScale;

//figure proportional size based on viewbox
numMainMapHeight = (numMainMapWidth*arrMainMapViewBox[3])/arrMainMapViewBox[2];

//instantiate the Raphael canvas (canvas is a bit misleading -- it's the target element where the SVG will be added)
objMainMap = Raphael(strTargetCanvas, numMainMapWidth, numMainMapHeight);

//set scale factors
numMainMapScale = getScaleFactor(numMainMapWidth,arrMainMapViewBox[2] + (numMapHorizPadding*2));

objMainMapSet = {};

$.each(objMainMapData, function(numLayerKey, objLayer) {

	//loop through geometries
	$.each(objLayer.geometries, function(numGeomKey, objGeom) {

		//Multipolygon layer style
		var objAttr = {
			"fill": strGeomFill,
			"stroke": "#CCC",
			"stroke-width": 1*numMainMapScale,
			"stroke-linejoin": "round",
			"cursor":"pointer"
		};

		objMainMapSet[objGeom.id] = objMainMap.path(objGeom.svgstring).attr(objAttr)
			.data('slug',objGeom.id);

	});
});

arrMapSets = [objMainMapSet];
objMainMap.setViewBox(arrMainMapViewBox[0], arrMainMapViewBox[1], arrMainMapViewBox[2] + numMapHorizPadding*numMainMapScale, arrMainMapViewBox[3], false);

 

Why not just use GeoDjango's SVG features?

You could, but Internet Explorer will have problems with it. As far as I can tell, VML gets cranky about large negative coordinates, which as any cartographer knows rules out the Western Hemisphere pretty completely. 
 

What's next?

We'll be building some tools to streamline some of our common mapping needs using svg_mapper as the backbone. We'd love to hear feedback on what you wish it could do that it doesn't do now and how it could be improved. And please do let use know if you use some or all of the code in your own projects.

 

Like our content? Help us do more.

Support Us

Leave a Comment

via Twitter