Noncontiguous cartograms in OpenLayers and Polymaps

I’m happy to be doing less Flash and more JavaScript development these days. In particular, I’ve been investigating two open-source JavaScript web mapping platforms: one old, OpenLayers, and one new, Polymaps.

OpenLayers has been around a while, but still performs remarkably well as a slippy map framework while allowing easy thematic map customization. Polymaps is brand new (from Stamen so you know it’s going to blow your mind), but is remarkable for enabling web standards-based thematic customization of geographic layers loaded via GeoJSON or KML onto “vector tiles that are rendered with SVG”.

In this post I show how either OpenLayers or Polymaps can be used to create dynamic and customizable noncontiguous cartograms with very little code.

Idea

NY Times noncontiguous cartogram example from 2007

The above is a cartogram of state electoral influence from 2007 by the NY Times

I’ve written about these gals before. The form involves resizing features (like states or countries) relative to the units’ attribute values in a given field (often population). Unlike the more common, contiguous form of the cartogram, noncontiguous cartograms don’t attempt to maintain topology, but are therefore free to maintain shape perfectly and experiment with position, as in the above example.

Here’s an example from Judy Olson’s original 1976 article on this cartogram form.

noncontiguous cartogram from Olson\

See my older post or Olson’s article for more info on the technique and the theory behind it. Olson produced the above image semi-manually using a projector — each state was projected at a precise scaling factor and then traced. Below I show how to do more or less the same thing, but with JavaScript and either OpenLayers or Polymaps.

Implementation

I wanted to be able to load in any polygonal geodata file (supported by the chosen web mapping framework) and resize the features based on any numerical attribute in order to form a noncontiguous cartogram. The advantage of implementing this within a web mapping framework is obviously that additional data layers from various sources can easily be over or underlain.

As a test and proof of concept for both frameworks, I wanted to reproduce Olson’s graphic (above) as best as I fairly easily could. Olson used 1970 Census data to show the number of people aged 65+ by state; here I’m updating it with estimated 2009 data. Specifically, I’ll be loading this Geocommons data layer uploaded last year.

I’ve been working with OpenLayers for about half a year so I implemented cartograms there first.

OpenLayers

Implementing noncontiguous cartograms in OpenLayers is fairly straightforward, thanks to the helpful methods provided by this comprehensive framework. The first step is loading the geodata.

Load geodata

OpenLayers makes loading geodata quite easy; the library can parse WKT, GML, KML, GeoJSON, GeoRSS, etc. For all we’re gonna use the Layer.Vector class. In this case I’ll load in the KML version from Geocommons, and therefore OpenLayer’s Format.KML parser.

var kmlLayer = new OpenLayers.Layer.Vector( "KML", {
    projection : new OpenLayers.Projection("EPSG:4326"),
    strategies: [ new OpenLayers.Strategy.Fixed() ],
    protocol: new OpenLayers.Protocol.HTTP( {
        url: "http://geocommons.com/overlays/55629.kml",
        format: new OpenLayers.Format.KML( {
            extractStyles: false,
            extractAttributes: true,
            maxDepth: 2
        } )
    } ),
    style : {
        'fillColor' : '#dddddd', 'fillOpacity' : 1,
        'strokeColor' : '#666666', 'strokeWidth' : 1
    }
} );

Noncontiguous cartograms don’t require a geographic projection — regardless of the projection of the original linework the features can still be scaled up or down accurately to form the cartogram. So the only reasons for projection are aesthetics and to enhance recognizability of features. I’m unsure of what projection Olson used, but I just went for that old classic, Albers Equal Area. Specifically, I used Proj4js and the following definition:

Proj4js.defs["MY_ALBERS"] = “+proj=aea +lat_1=32 +lat_2=58 +lat_0=45 +lon_0=-97 +x_0=0 +y_0=0 +ellps=WGS84 +datum=WGS84 +units=m +no_defs”;

To apply it, I just create a new OpenLayers.Projection which gets applied to the map when it’s instantiated. Thematicmapping.org has more info on projections and OpenLayers.

Unless you already know the maximum value of whatever attribute you’re mapping, you’ll have to loop through them all once before you can loop through them again to scale. Attributes are accessible via the attributes property of each OpenLayers.Feature.Vector (accessible via the features property of the OpenLayers.Layer.Vector).

Scale features

In order to scale a polygonal feature for a noncontiguous cartogram, we must know:

  1. the feature’s value for the chosen thematic attribute (see above)
  2. the feature’s area and centroid as rendered

    OpenLayers makes these easily accessible. Each feature’s geometry object has getArea and getCentroid methods. As far as I can tell, the getCentroid function returns the true polygonal center of mass, and not just the center of the feature’s bounding box.

  3. the feature’s desired area (in pixels) given the maximum area provided and the feature’s value as a percentage of the layer’s maximum value

    desiredArea = ( value / maxValue ) * maxArea;

  4. finally, the feature’s scale which is just a function of its original and desired area

    desiredScale = Math.sqrt( desiredArea / originalArea );

    This scale is then applied via the resize method of each feature’s geometry.

    feature.geometry.resize( desiredScale, centroid );

Result

Here’s an image from the example you can find on this page. All source can just be accessed from there.

noncontiguous cartogram from Olson redrawn in OpenLayers

Hey, that looks pretty great. Michigan’s kinda off-center, but I believe that’s because the polygon is only defined by one ring of coordinates, though it should be a multipolygon. Perhaps more noticeable is the overlap in the Northeast. In Judy Olson’s original example, states were blown up by a visual projector and then traced. But the projector could be aimed before tracing, thus avoiding overlap. In this case I simply scale the states and keep them at their original centroids.

I could avoid overlap by determining appropriate positioning and setting this within OpenLayers (but overlap would be very difficult to determine and then address dynamically) or by significantly reducing the configurable maximum area allowable on the resultant cartogram (but in order to be sure you’re preventing overlap features would have to be scaled quite small, which would detract from the readability of the cartogram).

Polymaps

Polymaps is a fairly new JavaScript mapping library by Stamen and SimpleGeo. Geocommons recently introduced Polymaps to their online mapping service, creating a quite powerful Flash-free online thematic mapping tool. Creating noncontiguous cartograms in Polymaps was a bit tougher than the process detailed above, just because the library is so light-weight.

Load geodata

The first step is quite easy. As far as vector geodata formats, Polymaps only has built-in support for GeoJSON, though they do provide a KML example that takes advantage of an optional fetch method specified in the GeoJSON layer constructor.

But I’ll just go with GeoJSON for this one. I found out in this post from GeoIQ that I can access a GeoJSON version of features in any Geocommons dataset by going to the “features.json” endpoint. So for my 2009 estimated census dataset I’ll be loading in http://geocommons.com/overlays/55629/features.json?geojson=1 using the following simple method:

var url = "http://geocommons.com/overlays/55629/features.json?geojson=1";                     
map.add(po.geoJson()
    .url(url)
    .on("load", load)
    .tile( false )
    .id("states"));

Note that loading directly from Geocommons only works while developing locally because of cross-domain policy. So in my finished example I end up loading a local version of the JSON.

Polymaps is limited to the Web Mercator projection for display, but we can still produce a passable reproduction of Olson’s original.

As in the OpenLayers example above, unless you already know the maximum value of your attribute, you’ll need to first loop through the features to determine it. In the above code, you can see I’m using the layer’s on method to listen for the “load” event. And in there I can get my features off the event object’s features property. Attributes are stored on each feature’s data.properties property.

Scale features

To scale each feature we must first know it’s value in the chosen attribute (see above). Then we need to determine it’s current area (in pixels) in order to figure the feature’s desired area on the eventual cartogram. OpenLayers provides a convenient method for this but in Polymaps we have to roll our own; for this Mike Bostock (one of the primary authors of Polymaps) was of much help. To calculate the area of each feature I just needed access to the list of projected coordinates (then I could employ the basic technique detailed here). Mr. Bostock pointed me to the pathSegList property of the SVGPathElement interface. The pathSegList exposes a list of path segments with the SVGPathSeg interface. Mike said I could count on these segments being one of types “M” (move to), “L” (line to), or “Z” (end line). With this information I quickly put together a method that should return the projected area of any SVGPathElement that Polymaps may produce.

function getPathArea( segList )
{               
    var area = 0;
    var seg1, seg2;                 
    var nPts = segList.numberOfItems;
     
    // let's see if the last item is a 'Z' (it should be)
    var lastLetter =
        segList.getItem( nPts - 1 ).pathSegTypeAsLetter;
    if ( lastLetter.toLowerCase() == 'z' )
        nPts --;
    var j = nPts - 1;
    segItem_list:
    for ( var i = 0; i < nPts; j=i++ )
    {
        seg1 = segList.getItem( i );
        seg2 = segList.getItem( j );
        area += seg1.x * seg2.y;
        area -= seg1.y * seg2.x;
    }
    area /= 2;
    return Math.abs( area );
}

I could easily create a similar centroid method, but I got lazy and decided to just use the center of each feature’s bounding box (accessible via each feature SVG element’s getBBox method).

The desired area and scale are calculated just as we did above in OpenLayers:

desiredArea = ( value / maxValue ) * maxArea;
desiredScale = Math.sqrt( desiredArea / originalArea );

The scale is then applied via the ‘transform’ attribute of each SVG element; both scale and x-y translation must be defined in the ‘transform’ attribute:

feature.element.setAttribute(
    "transform",
    "scale(" + scale + " " + scale + ")" + " " +             
    "translate(" +
        -( ( centerX * scale - centerX ) / scale ) +
        " " +
        -( ( centerY * scale - centerY ) / scale ) +
    ")"
);
Result

As before, here’s an image captured from the example you can find on this page. You’ll see a bit of the code there, but please ‘view source’ to see all the code and markup.

noncontiguous cartogram from Olson redrawn in OpenLayers

Aside from the necessary difference of projection, the main thing that stands out is the overlap in the Northeast. I discussed possible ways to avoid this in the OpenLayers example above.

Conclusion

Nothing here is new. I’ve been doing this in Flash for a while — see here and here; and of course Judy Olson was doing it some 35 years ago. But it’s nice to see it working dynamically in a couple of open source, web standards-compliant libraries. Thanks to OpenLayers, Polymaps, and Geocommmons for making it possible!

8 Comments

  1. You can do the same thing using SLD and a simple Java filter using GeoServer. See http://ian01.geog.psu.edu/geoserver/www/cartogram/discontinous.html for an example and details.

    Posted February 23, 2011 at 10:11 am | Permalink
  2. Hi Ian,

    Did you take down the first link to the server for testing? Where else could we test or are there instructions on how to set up this environment locally and test?

    Thanks

    Jason
    Posted March 7, 2013 at 2:53 pm | Permalink
  3. One other question. How does one set up an environment to test and run this? thanks

    Jason
    Posted March 7, 2013 at 4:06 pm | Permalink
  4. Hi thankyou for your guidance on this post it is actually helpful, we have been trying to find information for a while and this is good for us to comprehend, looking for a radio to suit what we need isn’t simple. Thanks again

    Fiona
    Posted May 26, 2013 at 7:31 pm | Permalink
  5. Write more, thats all I have to say. Literally, it seems as though you relied on the video to make your point. You definitely know what youre talking about, why waste your intelligence on just posting videos to your site when you could be giving us something enlightening to read?

    Posted September 22, 2013 at 7:06 pm | Permalink
  6. The text is certainly very powerful which is most likely the reason why I am making the effort to comment. I do not make it a regular habit of doing that. Secondly, even though I can easily notice a jumps in reason you come up with, I am not really confident of how you seem to connect your details which inturn help to make your conclusion. For right now I will, no doubt subscribe to your position however wish in the foreseeable future you connect your facts much better.

    Posted September 22, 2013 at 7:06 pm | Permalink
  7. We are not specific the area you will be having your data, having said that beneficial theme. I needs to take some time mastering more as well as realizing additional. Thank you for excellent information I’m seeking these details for my goal.

    Posted January 21, 2014 at 3:42 pm | Permalink
  8. Just curious if you have tried something simpler - indexing the values that you want to show in the cartogram so the state with the largest value is 100% (i.e. retains current size) and adjusting all other states size relative percentage to this base, then buffering inwards? States with smallest percentages might disappear, but would maybe maintain position and no overlap cause you’re decreasing size. But maybe if there was large magnitudes of difference between highest/lowest values, I guess might just show one state and all others disappear…become miniscule…?

    J Bow
    Posted May 14, 2014 at 3:19 pm | Permalink

5 Trackbacks

  1. [...] indiemaps.com/blog » Noncontiguous cartograms in OpenLayers and Polymaps indiemaps.com/blog/2011/02/noncontiguous-cartograms-in-openlayers-and-polymaps/ – view page – cached I’m happy to be doing less Flash and more JavaScript development these days. In particular, I’ve been investigating two open-source JavaScript web mapping platforms: one old, OpenLayers, and one new, Polymaps. Show influential only (1) $(’#filter-infonly’).change(function() { var el = $(this); var url = document.location.href; var checked = el.attr(’checked’); if (checked) { document.location.href = url + ((/?/.test(url)) ? ‘&’ : ‘?’) + ‘infonly=1′; } else { document.location.href = url.replace(/[?&]?infonly=1/,”); } }); [...]

  2. [...] of freaking amazing, how about this?  Noncontiguous cartograms in OpenLayers and Polymaps 1.  OpenLayers + Polymaps 2 is a winning combination.  God bless Ian Turton for pushing a [...]

  3. By Define stamen | Upriverranch on March 6, 2011 at 12:34 am

    [...] Noncontiguous cartograms in OpenLayers and Polymaps [...]

  4. By Zachary Johnson joins GeoIQ on June 6, 2011 at 10:28 am

    [...] I came across his work three years ago, and it never hurts to flatter us with blogging about some cool hacks using our API. I’m very excited to be joining the GeoIQ development team as a Visualization Engineer. The [...]

  5. [...] saw OL used for thematic mapping in choropleth and proportional symbol applications in 2008. I added noncontiguous cartograms to the mix last [...]

Post a Comment

Your email is never published nor shared. Required fields are marked *