A state grid inspired by an Allison McCann graphic on state taxes. (See also David Mimno’s implementation.)
forked from mbostock's block: State Grid
A state grid inspired by an Allison McCann graphic on state taxes. (See also David Mimno’s implementation.)
forked from mbostock's block: State Grid
<!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); | |
} |