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
SVG or <canvas/>?
Why Raphaël?
The nuts and bolts
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))
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);