Lightweight Redistricting with OpenLayers

Next year the 2010 Census data will be coming out, and shortly after that redistricting will be in full swing across the US.

Redistricting for our County Commisioner districts is a very simple affair. The districts are composed of voting precincts, so redistricting in this context means assigning a voting precinct polygon a new commissioner district number. No linework changes are involved, and a quick glance at our current districts will tell you SCOTUS’ idea of “compactness” of districts is not a consideration. We pre-calculate whatever stats we need by voting precinct, and the rest is basic arithmetic.

There are a number of redistricting software packages around, including lots of Esri extensions and a particularly cool open source project that’s in alpha. But they’re all a bit heavy for what we need, not to mention pricey for the Esri ones, so I figured I’d take a stab at it and see what I could come up with. After ~6 hours or so over two days, here’s what I’ve got.

Redistricting Demo

Yes, I know it’s ugly. If we really use it I’ll pretty it up. And the data is all randomly generated garbage. But just as a test of the idea, by golly I think it’ll work.

It’s OpenLayers, jQuery, the Google Charts API, and our voting precincts as geojson. I simplified the bejeesus out of our voting precincts layer, since all I’ll care about in the end is the precinct number and the district assignment. That droped the geojson down to a web-friendly 140kb. Then it was a matter of loading it as a vector, setting a select control, and iterating through the vectors when something changes. It’s all client side code so you can look at the (somewhat rough) source, but I’ll just point out a few of the salient bits.

First, the vector layer setup. I’m using a StyleMap with a lookup to set the colors of the precincts. I’m loading the geojson as a file and then adding the vectors to the layer rather than loading it with the vector layer. No particular reason for that - it just seems cleaner to me.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39

var styleMap = new OpenLayers.StyleMap({
"default": new OpenLayers.Style({
strokeColor: "#ff9933",
fillOpacity: 0.9,
strokeWidth: 2,
graphicZIndex: 1
}),
"select": new OpenLayers.Style({
strokeColor: "#3399ff",
strokeWidth: 4,
graphicZIndex: 2
})
});
// images for vector marker layer
var lookup = {
1: {fillColor: "#7ADBC9"},
2: { fillColor: "#E7DAA6" },
3: {fillColor: "#F1C58B" },
4: {fillColor: "#855A47"},
5: { fillColor: "#1E8E07" },
6: {fillColor: "#9EB1B7" }
}
styleMap.addUniqueValueRules("default", "cc", lookup);


vectors = new OpenLayers.Layer.Vector("Vector Layer", { styleMap: styleMap, isBaseLayer: true });

map.addLayers([vectors]);


// Load vectors from geojson
url = "data/cc.json";
OpenLayers.loadURL(url, {}, null, function(r) {
var p = new OpenLayers.Format.GeoJSON();
var f = p.read(r.responseText);
vectors.addFeatures(f);
updateValues();
});

For the vector layer’s select event, make sure you tell the feature explicitly to draw. Otherwise it won’t change colors to the new district until another select or unselect event fires.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
function onFeatureSelect(feature) {
selectedFeature = feature;

// Set CC value
feature.attributes.cc = parseInt($('#radio_form input:radio:checked').val());
vectors.drawFeature(feature);

// Set identify table
$("#precinct").html(feature.attributes.precno);
$("#population").html(addCommas(feature.attributes.population));
$("#republican").html(addCommas(feature.attributes.republican));
$("#democrat").html(addCommas(feature.attributes.democrat));
$("#black").html(addCommas(feature.attributes.black));
$("#white").html(addCommas(feature.attributes.white));
$("#hispanic").html(addCommas(feature.attributes.hispanic));

// Calculate values

updateValues();

}

You’ll be running through the whole features array in the updateValues function, so it pays to think this through. Never make a long loop with a jQuery lookup, like $(“#myelement”), as it will have to do a DOM search each and every time and performance will suffer. Here I’m using arrays to hold the summary stats until the end. The red/green bit is for potential constraints, like the district populations have to be within 10% of each other.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
function updateValues() {

// Set some vars so we're not doing a dom search a hundreds of times

var popSummary = new Array();
var repSummary = new Array();
var demSummary = new Array();
var blackSummary = new Array();
var whiteSummary = new Array();
var hispanicSummary = new Array();

for (i = 1; i < 7; i++) {
popSummary[i] = 0;
repSummary[i] = 0;
demSummary[i] = 0;
blackSummary[i] = 0;
whiteSummary[i] = 0;
hispanicSummary[i] = 0;
}



// loop through vectors
feats = vectors.features;
for(i = 0; i < feats.length; i++) {
feature = feats[i];

popSummary[parseInt(feature.attributes.cc)] += parseInt(feature.attributes.population);
repSummary[parseInt(feature.attributes.cc)] += parseInt(feature.attributes.republican);
demSummary[parseInt(feature.attributes.cc)] += parseInt(feature.attributes.democrat);
blackSummary[parseInt(feature.attributes.cc)] += parseInt(feature.attributes.black);
whiteSummary[parseInt(feature.attributes.cc)] += parseInt(feature.attributes.white);
hispanicSummary[parseInt(feature.attributes.cc)] += parseInt(feature.attributes.hispanic);

}

// Set table values from arrays
for (i = 1; i < 7; i++) {
$("#pop-"+i).html(popSummary[i]);
$("#rep-"+i).html(repSummary[i]);
$("#dem-"+i).html(demSummary[i]);
$("#black-"+i).html(blackSummary[i]);
$("#white-"+i).html(whiteSummary[i]);
$("#hispanic-"+i).html(hispanicSummary[i]);
}


// add red/bold style if criteria not met
$("#summary-stats .population").each( function(intIndex) {
if (parseInt( $( this ).html() ) > 189061 || parseInt( $( this ).html() ) < 154687 ) $(this).addClass("badValue").removeClass("goodValue");
else $(this).addClass("goodValue").removeClass("badValue");
});

// add commas
$("#summary-stats tbody td").each( function(intIndex) {
$( this ).html( addCommas($(this).html()) ) ;
});

}