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
forked from geraldarthur's block: State Grid Choropleth
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
forked from geraldarthur's block: State Grid Choropleth
<!DOCTYPE html> | |
<meta charset="utf-8"> | |
<style> | |
.state rect { | |
fill: #a6a8ab; | |
} | |
.state text { | |
font: 12px monospace; | |
font-weight: bold; | |
text-anchor: middle; | |
fill: white; | |
} | |
.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 = [], | |
// | |
// EDIT! | |
// | |
dataKey = 'overallGrade', | |
// | |
// DON'T EDIT! | |
// | |
stateKey = 'stateAbbrev', | |
rowsArr = [ | |
{ | |
"stateName": "VERMONT", | |
"stateAbbrev": "VT", | |
"extremeHeat": 76, | |
"drought": null, | |
"wildfires": null, | |
"inlandFlooding": null, | |
"coastalFlooding": null, | |
"overallGrade": 60 | |
}, | |
{ | |
"stateName": "WEST VIRGINIA", | |
"stateAbbrev": "WV", | |
"extremeHeat": 52, | |
"drought": null, | |
"wildfires": null, | |
"inlandFlooding": 36, | |
"coastalFlooding": null, | |
"overallGrade": 52 | |
}, | |
{ | |
"stateName": "NORTH DAKOTA", | |
"stateAbbrev": "ND", | |
"extremeHeat": 76, | |
"drought": null, | |
"wildfires": null, | |
"inlandFlooding": 60, | |
"coastalFlooding": null, | |
"overallGrade": 60 | |
}, | |
{ | |
"stateName": "HAWAII", | |
"stateAbbrev": "HI", | |
"extremeHeat": 12, | |
"drought": null, | |
"wildfires": null, | |
"inlandFlooding": null, | |
"coastalFlooding": 28, | |
"overallGrade": 28 | |
}, | |
{ | |
"stateName": "NEW JERSEY", | |
"stateAbbrev": "NJ", | |
"extremeHeat": 52, | |
"drought": null, | |
"wildfires": null, | |
"inlandFlooding": 52, | |
"coastalFlooding": 28, | |
"overallGrade": 36 | |
}, | |
{ | |
"stateName": "NEW HAMPSHIRE", | |
"stateAbbrev": "NH", | |
"extremeHeat": 60, | |
"drought": null, | |
"wildfires": null, | |
"inlandFlooding": null, | |
"coastalFlooding": 28, | |
"overallGrade": 52 | |
}, | |
{ | |
"stateName": "RHODE ISLAND", | |
"stateAbbrev": "RI", | |
"extremeHeat": 76, | |
"drought": null, | |
"wildfires": null, | |
"inlandFlooding": 76, | |
"coastalFlooding": 60, | |
"overallGrade": 76 | |
}, | |
{ | |
"stateName": "VIRGINIA", | |
"stateAbbrev": "VA", | |
"extremeHeat": 84, | |
"drought": null, | |
"wildfires": null, | |
"inlandFlooding": 76, | |
"coastalFlooding": 60, | |
"overallGrade": 76 | |
}, | |
{ | |
"stateName": "DELAWARE", | |
"stateAbbrev": "DE", | |
"extremeHeat": 84, | |
"drought": null, | |
"wildfires": null, | |
"inlandFlooding": 84, | |
"coastalFlooding": 84, | |
"overallGrade": 84 | |
}, | |
{ | |
"stateName": "ALASKA", | |
"stateAbbrev": "AK", | |
"extremeHeat": 100, | |
"drought": null, | |
"wildfires": null, | |
"inlandFlooding": null, | |
"coastalFlooding": 76, | |
"overallGrade": 84 | |
}, | |
{ | |
"stateName": "CONNECTICUT", | |
"stateAbbrev": "CT", | |
"extremeHeat": 100, | |
"drought": null, | |
"wildfires": null, | |
"inlandFlooding": 100, | |
"coastalFlooding": 76, | |
"overallGrade": 100 | |
}, | |
{ | |
"stateName": "MARYLAND", | |
"stateAbbrev": "MD", | |
"extremeHeat": 100, | |
"drought": null, | |
"wildfires": null, | |
"inlandFlooding": null, | |
"coastalFlooding": 100, | |
"overallGrade": 84 | |
}, | |
{ | |
"stateName": "MASSACHUSETTS", | |
"stateAbbrev": "MA", | |
"extremeHeat": 100, | |
"drought": null, | |
"wildfires": null, | |
"inlandFlooding": 100, | |
"coastalFlooding": 100, | |
"overallGrade": 100 | |
}, | |
{ | |
"stateName": "SOUTH CAROLINA", | |
"stateAbbrev": "SC", | |
"extremeHeat": 52, | |
"drought": null, | |
"wildfires": 76, | |
"inlandFlooding": 52, | |
"coastalFlooding": 28, | |
"overallGrade": 52 | |
}, | |
{ | |
"stateName": "SOUTH DAKOTA", | |
"stateAbbrev": "SD", | |
"extremeHeat": 36, | |
"drought": 12, | |
"wildfires": null, | |
"inlandFlooding": 12, | |
"coastalFlooding": null, | |
"overallGrade": 28 | |
}, | |
{ | |
"stateName": "ARKANSAS", | |
"stateAbbrev": "AR", | |
"extremeHeat": 28, | |
"drought": 12, | |
"wildfires": 12, | |
"inlandFlooding": 12, | |
"coastalFlooding": null, | |
"overallGrade": 12 | |
}, | |
{ | |
"stateName": "NEVADA", | |
"stateAbbrev": "NV", | |
"extremeHeat": 12, | |
"drought": 12, | |
"wildfires": 36, | |
"inlandFlooding": null, | |
"coastalFlooding": null, | |
"overallGrade": 12 | |
}, | |
{ | |
"stateName": "MONTANA", | |
"stateAbbrev": "MT", | |
"extremeHeat": 12, | |
"drought": 12, | |
"wildfires": 52, | |
"inlandFlooding": null, | |
"coastalFlooding": null, | |
"overallGrade": 28 | |
}, | |
{ | |
"stateName": "KENTUCKY", | |
"stateAbbrev": "KY", | |
"extremeHeat": 36, | |
"drought": 36, | |
"wildfires": 36, | |
"inlandFlooding": 12, | |
"coastalFlooding": null, | |
"overallGrade": 28 | |
}, | |
{ | |
"stateName": "ARIZONA", | |
"stateAbbrev": "AZ", | |
"extremeHeat": 60, | |
"drought": 36, | |
"wildfires": 28, | |
"inlandFlooding": null, | |
"coastalFlooding": null, | |
"overallGrade": 52 | |
}, | |
{ | |
"stateName": "LOUISIANA", | |
"stateAbbrev": "LA", | |
"extremeHeat": 52, | |
"drought": 36, | |
"wildfires": 52, | |
"inlandFlooding": null, | |
"coastalFlooding": 76, | |
"overallGrade": 52 | |
}, | |
{ | |
"stateName": "MISSISSIPPI", | |
"stateAbbrev": "MS", | |
"extremeHeat": 28, | |
"drought": 28, | |
"wildfires": 28, | |
"inlandFlooding": null, | |
"coastalFlooding": 12, | |
"overallGrade": 12 | |
}, | |
{ | |
"stateName": "WYOMING", | |
"stateAbbrev": "WY", | |
"extremeHeat": 52, | |
"drought": 28, | |
"wildfires": 28, | |
"inlandFlooding": null, | |
"coastalFlooding": null, | |
"overallGrade": 28 | |
}, | |
{ | |
"stateName": "TEXAS", | |
"stateAbbrev": "TX", | |
"extremeHeat": 12, | |
"drought": 28, | |
"wildfires": 28, | |
"inlandFlooding": null, | |
"coastalFlooding": 36, | |
"overallGrade": 12 | |
}, | |
{ | |
"stateName": "ILLINOIS", | |
"stateAbbrev": "IL", | |
"extremeHeat": 36, | |
"drought": 28, | |
"wildfires": null, | |
"inlandFlooding": 28, | |
"coastalFlooding": null, | |
"overallGrade": 28 | |
}, | |
{ | |
"stateName": "OHIO", | |
"stateAbbrev": "OH", | |
"extremeHeat": 12, | |
"drought": 28, | |
"wildfires": null, | |
"inlandFlooding": 28, | |
"coastalFlooding": null, | |
"overallGrade": 28 | |
}, | |
{ | |
"stateName": "MISSOURI", | |
"stateAbbrev": "MO", | |
"extremeHeat": 52, | |
"drought": 28, | |
"wildfires": 12, | |
"inlandFlooding": 12, | |
"coastalFlooding": null, | |
"overallGrade": 12 | |
}, | |
{ | |
"stateName": "IOWA", | |
"stateAbbrev": "IA", | |
"extremeHeat": 52, | |
"drought": 60, | |
"wildfires": null, | |
"inlandFlooding": 52, | |
"coastalFlooding": null, | |
"overallGrade": 60 | |
}, | |
{ | |
"stateName": "TENNESSEE", | |
"stateAbbrev": "TN", | |
"extremeHeat": 60, | |
"drought": 60, | |
"wildfires": 60, | |
"inlandFlooding": 28, | |
"coastalFlooding": null, | |
"overallGrade": 52 | |
}, | |
{ | |
"stateName": "GEORGIA", | |
"stateAbbrev": "GA", | |
"extremeHeat": 52, | |
"drought": 60, | |
"wildfires": 76, | |
"inlandFlooding": 52, | |
"coastalFlooding": 36, | |
"overallGrade": 52 | |
}, | |
{ | |
"stateName": "WISCONSIN", | |
"stateAbbrev": "WI", | |
"extremeHeat": 76, | |
"drought": 52, | |
"wildfires": null, | |
"inlandFlooding": 60, | |
"coastalFlooding": null, | |
"overallGrade": 76 | |
}, | |
{ | |
"stateName": "ALABAMA", | |
"stateAbbrev": "AL", | |
"extremeHeat": 36, | |
"drought": 52, | |
"wildfires": 12, | |
"inlandFlooding": null, | |
"coastalFlooding": 12, | |
"overallGrade": 28 | |
}, | |
{ | |
"stateName": "KANSAS", | |
"stateAbbrev": "KS", | |
"extremeHeat": 60, | |
"drought": 52, | |
"wildfires": 52, | |
"inlandFlooding": 36, | |
"coastalFlooding": null, | |
"overallGrade": 36 | |
}, | |
{ | |
"stateName": "INDIANA", | |
"stateAbbrev": "IN", | |
"extremeHeat": 52, | |
"drought": 52, | |
"wildfires": null, | |
"inlandFlooding": 52, | |
"coastalFlooding": null, | |
"overallGrade": 52 | |
}, | |
{ | |
"stateName": "NEBRASKA", | |
"stateAbbrev": "NE", | |
"extremeHeat": 28, | |
"drought": 52, | |
"wildfires": null, | |
"inlandFlooding": 52, | |
"coastalFlooding": null, | |
"overallGrade": 36 | |
}, | |
{ | |
"stateName": "IDAHO", | |
"stateAbbrev": "ID", | |
"extremeHeat": 28, | |
"drought": 52, | |
"wildfires": 60, | |
"inlandFlooding": null, | |
"coastalFlooding": null, | |
"overallGrade": 36 | |
}, | |
{ | |
"stateName": "NEW MEXICO", | |
"stateAbbrev": "NM", | |
"extremeHeat": 76, | |
"drought": 52, | |
"wildfires": 76, | |
"inlandFlooding": null, | |
"coastalFlooding": null, | |
"overallGrade": 76 | |
}, | |
{ | |
"stateName": "COLORADO", | |
"stateAbbrev": "CO", | |
"extremeHeat": 76, | |
"drought": 84, | |
"wildfires": null, | |
"inlandFlooding": null, | |
"coastalFlooding": null, | |
"overallGrade": 76 | |
}, | |
{ | |
"stateName": "FLORIDA", | |
"stateAbbrev": "FL", | |
"extremeHeat": 28, | |
"drought": 84, | |
"wildfires": 84, | |
"inlandFlooding": 28, | |
"coastalFlooding": 12, | |
"overallGrade": 52 | |
}, | |
{ | |
"stateName": "NORTH CAROLINA", | |
"stateAbbrev": "NC", | |
"extremeHeat": 84, | |
"drought": 84, | |
"wildfires": 100, | |
"inlandFlooding": 76, | |
"coastalFlooding": 52, | |
"overallGrade": 84 | |
}, | |
{ | |
"stateName": "MINNESOTA", | |
"stateAbbrev": "MN", | |
"extremeHeat": 76, | |
"drought": 76, | |
"wildfires": null, | |
"inlandFlooding": null, | |
"coastalFlooding": null, | |
"overallGrade": 76 | |
}, | |
{ | |
"stateName": "OKLAHOMA", | |
"stateAbbrev": "OK", | |
"extremeHeat": 60, | |
"drought": 76, | |
"wildfires": 52, | |
"inlandFlooding": 28, | |
"coastalFlooding": null, | |
"overallGrade": 60 | |
}, | |
{ | |
"stateName": "UTAH", | |
"stateAbbrev": "UT", | |
"extremeHeat": 28, | |
"drought": 76, | |
"wildfires": 76, | |
"inlandFlooding": null, | |
"coastalFlooding": null, | |
"overallGrade": 60 | |
}, | |
{ | |
"stateName": "MICHIGAN", | |
"stateAbbrev": "MI", | |
"extremeHeat": 76, | |
"drought": 76, | |
"wildfires": null, | |
"inlandFlooding": 76, | |
"coastalFlooding": null, | |
"overallGrade": 76 | |
}, | |
{ | |
"stateName": "MAINE", | |
"stateAbbrev": "ME", | |
"extremeHeat": 28, | |
"drought": 76, | |
"wildfires": null, | |
"inlandFlooding": 52, | |
"coastalFlooding": 52, | |
"overallGrade": 28 | |
}, | |
{ | |
"stateName": "WASHINGTON", | |
"stateAbbrev": "WA", | |
"extremeHeat": 28, | |
"drought": 76, | |
"wildfires": 100, | |
"inlandFlooding": 76, | |
"coastalFlooding": 52, | |
"overallGrade": 84 | |
}, | |
{ | |
"stateName": "OREGON", | |
"stateAbbrev": "OR", | |
"extremeHeat": 12, | |
"drought": 100, | |
"wildfires": 84, | |
"inlandFlooding": 60, | |
"coastalFlooding": 52, | |
"overallGrade": 76 | |
}, | |
{ | |
"stateName": "PENNSYLVANIA", | |
"stateAbbrev": "PA", | |
"extremeHeat": 84, | |
"drought": 100, | |
"wildfires": null, | |
"inlandFlooding": 84, | |
"coastalFlooding": 76, | |
"overallGrade": 100 | |
}, | |
{ | |
"stateName": "NEW YORK", | |
"stateAbbrev": "NY", | |
"extremeHeat": 100, | |
"drought": 100, | |
"wildfires": null, | |
"inlandFlooding": 100, | |
"coastalFlooding": 76, | |
"overallGrade": 100 | |
}, | |
{ | |
"stateName": "CALIFORNIA", | |
"stateAbbrev": "CA", | |
"extremeHeat": 100, | |
"drought": 100, | |
"wildfires": 100, | |
"inlandFlooding": 100, | |
"coastalFlooding": 100, | |
"overallGrade": 100 | |
} | |
]; | |
d3.select("#grid").text().split("\n").forEach(function(line, i) { | |
var re = /\w+/g, m; | |
while (m = re.exec(line)) { | |
console.log(_.findWhere(rowsArr, { 'stateAbbrev': m[0]})) | |
states.push({ | |
name: m[0], | |
data: _.result(_.findWhere(rowsArr, { 'stateAbbrev': m[0]}), dataKey), | |
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 nestedData = d3.nest() | |
.key(function(d) { return d.stateAbbrev; }) | |
.entries(rowsArr); | |
var mappedData = function mappedValues(key) { | |
return rowsArr.map(function(stateObj) { | |
return stateObj[dataKey] | |
}) | |
} | |
var jenks5 = d3.scale.quantile() | |
.domain(jenks(mappedData(stateKey), 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("transform", function(d) { return "translate(" + (d.x - gridWidth / 2) * cellSize + "," + (d.y - gridHeight / 2) * cellSize + ")"; }) | |
.attr("class", function(d) { | |
var currData = d.data, | |
currClassStr = ''; | |
if (currData !== null) { | |
currClassStr += "state " + jenks5(d.data) | |
} else { | |
currClassStr += "state" | |
} | |
return currClassStr; | |
}); | |
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) { 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); | |
} |