Skip to content

Instantly share code, notes, and snippets.

@newsroomdev
Last active May 4, 2017 09:56
Show Gist options
  • Save newsroomdev/92248ec5049ba86bf125 to your computer and use it in GitHub Desktop.
Save newsroomdev/92248ec5049ba86bf125 to your computer and use it in GitHub Desktop.
State Grid Choropleth
<!DOCTYPE html>
<meta charset="utf-8">
<style>
.state rect {
fill: #dedede;
stroke: #efefef;
}
.state text {
font: 12px sans-serif;
text-anchor: middle;
}
.q0-5 rect { fill: #fffafa; }
.q1-5 rect { fill: #ffd2c4; }
.q2-5 rect { fill: #ffa68e; }
.q3-5 rect { fill: #fc7657; }
.q4-5 rect { fill: #ef4123; }
#legend {
padding: 1.5em 0 0 1.5em;
}
.list-inline {
padding-left: 0;
list-style: none;
}
.list-inline > li {
display: inline-block;
}
li.key {
border-top-width: 15px;
border-top-style: solid;
font-size: .75em;
width: 10%;
padding-left: 0;
padding-right: 0;
}
li.q0-5 { color: #fffafa; }
li.q1-5 { color: #ffd2c4; }
li.q2-5 { color: #ffa68e; }
li.q3-5 { color: #fc7657; }
li.q4-5 { color: #ef4123; }
</style>
<body>
<svg width="960" height="500"></svg>
<script id="grid" type="text/plain">
ME
WI VT NH
WA ID MT ND MN IL MI NY MA RI
OR NV WY SD IA IN OH PA NJ CT
CA UT CO NE MO KY WV VA MD DE
AZ NM KS AR TN NC SC
OK LA MS AL GA
HI AK TX FL
</script>
<script src="jenks.js"></script>
<script src="//cdnjs.cloudflare.com/ajax/libs/lodash.js/3.10.1/lodash.js"></script>
<script src="//d3js.org/d3.v3.min.js"></script>
<script>
var states = [],
statesObj = {
"AK": 12,
"AL": 56,
"AR": 27,
"AZ": 41,
"CA": 99,
"CO": 61,
"CT": 27,
"DC": 1,
"DE": 6,
"FL": 390,
"GA": 49,
"HI": 4,
"IA": 20,
"ID": 13,
"IL": 57,
"IN": 45,
"KS": 14,
"KY": 36,
"LA": 15,
"MA": 20,
"MD": 41,
"ME": 14,
"MI": 47,
"MN": 32,
"MO": 71,
"MS": 21,
"MT": 9,
"NC": 93,
"ND": 2,
"NE": 12,
"NH": 9,
"NJ": 36,
"NM": 11,
"NV": 23,
"NY": 83,
"OH": 76,
"OK": 13,
"OR": 47,
"PA": 73,
"PR": 6,
"RI": 6,
"SC": 22,
"SD": 5,
"TN": 79,
"TX": 70,
"UT": 19,
"VA": 89,
"VI": 1,
"VT": 9,
"WA": 88,
"WI": 29,
"WV": 20,
"WY": 5
};
d3.select("#grid").text().split("\n").forEach(function(line, i) {
var re = /\w+/g, m;
while (m = re.exec(line)) {
states.push({
name: m[0],
count: statesObj[m[0]],
x: m.index / 3,
y: i
});
}
});
var svg = d3.select("svg"),
width = +svg.attr("width"),
height = +svg.attr("height");
var gridWidth = d3.max(states, function(d) { return d.x; }) + 1,
gridHeight = d3.max(states, function(d) { return d.y; }) + 1,
cellSize = 40;
var jenks5 = d3.scale.quantile()
.domain(jenks(_.values(statesObj), 4))
.range(d3.range(5).map(function(i) { return "q" + i + "-5"; }));
var state = svg.append("g")
.attr("transform", "translate(" + width / 2 + "," + height / 2 + ")")
.selectAll(".state")
.data(states)
.enter().append("g")
.attr("class", function(d) { return "state " + jenks5(d.count) })
.attr("transform", function(d) { return "translate(" + (d.x - gridWidth / 2) * cellSize + "," + (d.y - gridHeight / 2) * cellSize + ")"; });
state.append("rect")
.attr("x", -cellSize / 2)
.attr("y", -cellSize / 2)
.attr("width", cellSize - 1)
.attr("height", cellSize - 1);
state.append("text")
.attr("dy", ".35em")
.text(function(d) { return d.name; });
var legend = d3.select('body')
.insert('div', ":first-child")
.attr("id", "legend")
.append('ul')
.attr('class', 'list-inline');
var keys = legend.selectAll('li.key')
.data(jenks5.range());
keys.enter().append('li')
.attr('class', function(d) { console.log(d); return'key ' + d; })
.style('border-top-color', String);
</script>
// # [Jenks natural breaks optimization](http://en.wikipedia.org/wiki/Jenks_natural_breaks_optimization)
//
// Implementations: [1](http://danieljlewis.org/files/2010/06/Jenks.pdf) (python),
// [2](https://github.com/vvoovv/djeo-jenks/blob/master/main.js) (buggy),
// [3](https://github.com/simogeo/geostats/blob/master/lib/geostats.js#L407) (works)
var jenks = function jenks(data, n_classes) {
// Compute the matrices required for Jenks breaks. These matrices
// can be used for any classing of data with `classes <= n_classes`
function getMatrices(data, n_classes) {
// in the original implementation, these matrices are referred to
// as `LC` and `OP`
//
// * lower_class_limits (LC): optimal lower class limits
// * variance_combinations (OP): optimal variance combinations for all classes
var lower_class_limits = [],
variance_combinations = [],
// loop counters
i, j,
// the variance, as computed at each step in the calculation
variance = 0;
// Initialize and fill each matrix with zeroes
for (i = 0; i < data.length + 1; i++) {
var tmp1 = [],
tmp2 = [];
for (j = 0; j < n_classes + 1; j++) {
tmp1.push(0);
tmp2.push(0);
}
lower_class_limits.push(tmp1);
variance_combinations.push(tmp2);
}
for (i = 1; i < n_classes + 1; i++) {
lower_class_limits[1][i] = 1;
variance_combinations[1][i] = 0;
// in the original implementation, 9999999 is used but
// since Javascript has `Infinity`, we use that.
for (j = 2; j < data.length + 1; j++) {
variance_combinations[j][i] = Infinity;
}
}
for (var l = 2; l < data.length + 1; l++) {
// `SZ` originally. this is the sum of the values seen thus
// far when calculating variance.
var sum = 0,
// `ZSQ` originally. the sum of squares of values seen
// thus far
sum_squares = 0,
// `WT` originally. This is the number of
w = 0,
// `IV` originally
i4 = 0;
// in several instances, you could say `Math.pow(x, 2)`
// instead of `x * x`, but this is slower in some browsers
// introduces an unnecessary concept.
for (var m = 1; m < l + 1; m++) {
// `III` originally
var lower_class_limit = l - m + 1,
val = data[lower_class_limit - 1];
// here we're estimating variance for each potential classing
// of the data, for each potential number of classes. `w`
// is the number of data points considered so far.
w++;
// increase the current sum and sum-of-squares
sum += val;
sum_squares += val * val;
// the variance at this point in the sequence is the difference
// between the sum of squares and the total x 2, over the number
// of samples.
variance = sum_squares - (sum * sum) / w;
i4 = lower_class_limit - 1;
if (i4 !== 0) {
for (j = 2; j < n_classes + 1; j++) {
// if adding this element to an existing class
// will increase its variance beyond the limit, break
// the class at this point, setting the lower_class_limit
// at this point.
if (variance_combinations[l][j] >=
(variance + variance_combinations[i4][j - 1])) {
lower_class_limits[l][j] = lower_class_limit;
variance_combinations[l][j] = variance +
variance_combinations[i4][j - 1];
}
}
}
}
lower_class_limits[l][1] = 1;
variance_combinations[l][1] = variance;
}
// return the two matrices. for just providing breaks, only
// `lower_class_limits` is needed, but variances can be useful to
// evaluage goodness of fit.
return {
lower_class_limits: lower_class_limits,
variance_combinations: variance_combinations
};
}
// the second part of the jenks recipe: take the calculated matrices
// and derive an array of n breaks.
function breaks(data, lower_class_limits, n_classes) {
var k = data.length - 1,
kclass = [],
countNum = n_classes;
// the calculation of classes will never include the upper and
// lower bounds, so we need to explicitly set them
kclass[n_classes] = data[data.length - 1];
kclass[0] = data[0];
// the lower_class_limits matrix is used as indexes into itself
// here: the `k` variable is reused in each iteration.
while (countNum > 1) {
kclass[countNum - 1] = data[lower_class_limits[k][countNum] - 2];
k = lower_class_limits[k][countNum] - 1;
countNum--;
}
return kclass;
}
if (n_classes > data.length) return null;
// sort data in numerical order, since this is expected
// by the matrices function
data = data.slice().sort(function(a, b) {
return a - b;
});
// get our basic matrices
var matrices = getMatrices(data, n_classes),
// we only need lower class limits here
lower_class_limits = matrices.lower_class_limits;
// extract n_classes out of the computed matrices
return breaks(data, lower_class_limits, n_classes);
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment