|
<!DOCTYPE html> |
|
<meta charset="utf-8"> |
|
<style> |
|
|
|
body { |
|
width: 960px; |
|
margin: 0; |
|
position: relative; |
|
font-size: 14px; |
|
font-family: sans-serif; |
|
text-transform: uppercase; |
|
} |
|
|
|
.counties { |
|
fill: #ccc; |
|
} |
|
|
|
.county-borders { |
|
fill: none; |
|
/*stroke: #ccc; |
|
stroke-width: .5px;*/ |
|
stroke-linejoin: round; |
|
stroke-linecap: round; |
|
} |
|
|
|
.labels text { |
|
fill: black; |
|
} |
|
|
|
button { |
|
position: absolute; |
|
font-size: 14px; |
|
font-family: sans-serif; |
|
text-transform: uppercase; |
|
} |
|
|
|
path.trendline { |
|
stroke-width: 2; |
|
stroke: black; |
|
} |
|
|
|
</style> |
|
<body> |
|
<script src="//d3js.org/d3.v4.min.js"></script> |
|
<script src="//d3js.org/topojson.v1.min.js"></script> |
|
<script src="//d3js.org/queue.v1.min.js"></script> |
|
<script> |
|
|
|
var width = 960, |
|
height = 500; |
|
|
|
var projection = d3.geoAlbersUsa() |
|
.translate([width / 2, height / 2]); |
|
|
|
var path = d3.geoPath() |
|
.projection(projection); |
|
|
|
var hue = d3.scaleLinear() |
|
.domain([0,1]) |
|
.range([180,360]) |
|
.clamp(true); |
|
|
|
var saturation = d3.scaleLinear() |
|
.domain([0,1]) |
|
.range([0,100]) |
|
.clamp(true); |
|
|
|
var lightness = d3.scaleLinear() |
|
.domain([0,9]) |
|
.range([80,20]) |
|
.clamp(true); |
|
|
|
var colorScale = d3.scaleLinear() |
|
.domain([0,1]) |
|
.range(['blue', 'red']) |
|
var color = function(d) { |
|
return d.trumpFraction !== null ? colorScale(d.trumpFraction) : 'none' |
|
} |
|
|
|
var svg = d3.select("body").append("svg") |
|
.attr("width", width) |
|
.attr("height", height); |
|
|
|
queue() |
|
.defer(d3.json, "us.json") |
|
.defer(d3.tsv, "coastal-counties.tsv") |
|
.defer(d3.tsv, "results.tsv", parseElection) |
|
.await(ready); |
|
|
|
function ready(error, us, coastalsArray, election) { |
|
if (error) throw error; |
|
|
|
var counties = topojson.feature(us, us.objects.counties), |
|
neighbors = topojson.neighbors(us.objects.counties.geometries), |
|
coastals = d3.set(coastalsArray.map(function(d) { return d.id; })), |
|
nexts = [], |
|
nexts2 = [], |
|
distance = 0; |
|
|
|
counties.features.forEach(function(county, i) { |
|
if (coastals.has(county.id)) nexts.push(county); |
|
county.distance = Infinity; |
|
county.neighbors = neighbors[i].map(function(j) { return counties.features[j]; }); |
|
|
|
// trump! |
|
var countyResults = election.filter(function(d) { |
|
return d.fips === county.id; |
|
}); |
|
var hillary = countyResults.filter(function(d) { |
|
return d.cand == "Hillary Clinton" |
|
})[0] |
|
var trump = countyResults.filter(function(d) { |
|
return d.cand == "Donald Trump" |
|
})[0] |
|
if(hillary && trump) { |
|
county.trumpFraction = trump.votes / (hillary.votes + trump.votes); |
|
} else { |
|
county.trumpFraction = null; |
|
} |
|
|
|
// centroid |
|
county.centroid = path.centroid(county); |
|
}); |
|
|
|
while (nexts.length) { |
|
nexts.forEach(function(county) { |
|
if (county.distance > distance) { |
|
county.distance = distance; |
|
county.neighbors.forEach(function(neighbor) { nexts2.push(neighbor); }); |
|
} |
|
}); |
|
nexts = nexts2, nexts2 = [], ++distance; |
|
} |
|
|
|
var county = svg.append("g") |
|
.attr("class", "counties") |
|
.selectAll("path") |
|
.data(counties.features) |
|
.enter().append("path") |
|
.style("fill", color) |
|
.attr("d", path); |
|
|
|
svg.append("path") |
|
.attr("class", "county-borders") |
|
.datum(topojson.mesh(us, us.objects.counties, function(a, b) { return a !== b; })) |
|
.attr("d", path); |
|
|
|
// SCATTERING |
|
|
|
var topMargin = 50; |
|
var sideMargin = 100; |
|
|
|
var distanceScale = d3.scaleLinear() |
|
.domain(d3.extent(counties.features, function(d) { return d.distance; })) |
|
.range([0 + sideMargin, width - sideMargin]); |
|
|
|
var trumpScale = d3.scaleLinear() |
|
.domain([0,1]) |
|
.range([height - topMargin, 0 + topMargin]); |
|
|
|
var labels = svg.append('g') |
|
.classed('labels', true) |
|
.style('opacity', 0); |
|
labels.append('text') |
|
.attr('x', width/2) |
|
.attr('y', 0) |
|
.text('100% for Trump') |
|
.style('text-anchor', 'middle') |
|
.attr('dy', '2em'); |
|
labels.append('text') |
|
.attr('x', width/2) |
|
.attr('y', height) |
|
.text('100% for Clinton') |
|
.style('text-anchor', 'middle') |
|
.attr('dy', '-2em'); |
|
labels.append('text') |
|
.attr('x', 0) |
|
.attr('y', height/2) |
|
.text('Coastal') |
|
.style('text-anchor', 'beginning') |
|
.attr('dx', '1em'); |
|
labels.append('text') |
|
.attr('x', width) |
|
.attr('y', height/2) |
|
.text('Interior') |
|
.style('text-anchor', 'end') |
|
.attr('dx', '-1em'); |
|
|
|
var geoButton = d3.select('body').append('button') |
|
.style('top', '1em') |
|
.style('left', '1em') |
|
.text("Geographify") |
|
.on('click', geographify); |
|
|
|
var scatButton = d3.select('body').append('button') |
|
.style('top', '1em') |
|
.style('right', '1em') |
|
.text("Scatterify") |
|
.on('click', scatterify); |
|
|
|
// TRENDLINE |
|
// http://bl.ocks.org/benvandyke/8459843 |
|
|
|
// get the x and y values for least squares |
|
var eligibleCounties = counties.features.filter(function(d) { return d.trumpFraction !== null; }); |
|
var xSeries = eligibleCounties.map(function(d) { return d.distance; }); |
|
var ySeries = eligibleCounties.map(function(d) { return d.trumpFraction; }); |
|
var leastSquaresCoeff = leastSquares(xSeries, ySeries); |
|
|
|
var trendlineData = [ |
|
[0, leastSquaresCoeff.intercept], |
|
[d3.max(eligibleCounties, function(d) { return d.distance}), leastSquaresCoeff.intercept + d3.max(eligibleCounties, function(d) { return d.distance}) * leastSquaresCoeff.slope] |
|
]; |
|
var trendlinePath = d3.line() |
|
.x(function(d) { return distanceScale(d[0]); }) |
|
.y(function(d) { return trumpScale(d[1]); }); |
|
var trendline = svg.append("path") |
|
.classed("trendline", true) |
|
.style('opacity', 0) |
|
.datum(trendlineData) |
|
.attr('d', trendlinePath); |
|
labels.append("text") |
|
.attr('x', width) |
|
.attr('y', height) |
|
.attr('dx', '-1em') |
|
.attr('dy', '-2em') |
|
.text('R² = ' + Math.round(leastSquaresCoeff.rSquare * 1000)/1000) |
|
.style('text-anchor', 'end'); |
|
|
|
// FLASHING LOL |
|
|
|
d3.timer(function(t) { |
|
county |
|
.style('opacity', d => .2 + .4*(Math.sin(t/500 - d.distance/4)+1)) |
|
}) |
|
|
|
// START WITH A TRANSITION |
|
|
|
var transitionDuration = 3000 |
|
setTimeout(scatterify, 3000) |
|
|
|
function scatterify() { |
|
county.transition() |
|
.duration(transitionDuration) |
|
.attr("transform", function(d) { |
|
var x = distanceScale(d.distance) - d.centroid[0]; |
|
var y = trumpScale(d.trumpFraction) - d.centroid[1]; |
|
return "translate("+x+","+y+")"; |
|
}) |
|
labels.transition() |
|
.duration(transitionDuration) |
|
.style('opacity', 1); |
|
trendline.transition() |
|
.duration(transitionDuration) |
|
.style('opacity', 1); |
|
scatButton.attr('disabled', 'disabled'); |
|
geoButton.attr('disabled', null); |
|
} |
|
|
|
function geographify() { |
|
county.transition() |
|
.duration(transitionDuration) |
|
.attr("transform", function(d) { |
|
return "translate("+0+","+0+")"; |
|
}); |
|
labels.transition() |
|
.duration(transitionDuration) |
|
.style('opacity', 0); |
|
trendline.transition() |
|
.duration(transitionDuration) |
|
.style('opacity', 0); |
|
scatButton.attr('disabled', null); |
|
geoButton.attr('disabled', 'disabled'); |
|
} |
|
|
|
} |
|
|
|
function parseElection (d) { |
|
if((d.cand !== "Donald Trump" && d.cand !== "Hillary Clinton") || isNaN(d.fips)) { |
|
return null; |
|
} |
|
|
|
return { |
|
fips: parseInt(d.fips), |
|
cand: d.cand, |
|
votes: parseInt(d.votes) |
|
}; |
|
} |
|
|
|
// returns slope, intercept and r-square of the line |
|
// from http://bl.ocks.org/benvandyke/8459843 |
|
function leastSquares(xSeries, ySeries) { |
|
var reduceSumFunc = function(prev, cur) { return prev + cur; }; |
|
|
|
var xBar = xSeries.reduce(reduceSumFunc) * 1.0 / xSeries.length; |
|
var yBar = ySeries.reduce(reduceSumFunc) * 1.0 / ySeries.length; |
|
|
|
var ssXX = xSeries.map(function(d) { return Math.pow(d - xBar, 2); }) |
|
.reduce(reduceSumFunc); |
|
|
|
var ssYY = ySeries.map(function(d) { return Math.pow(d - yBar, 2); }) |
|
.reduce(reduceSumFunc); |
|
|
|
var ssXY = xSeries.map(function(d, i) { return (d - xBar) * (ySeries[i] - yBar); }) |
|
.reduce(reduceSumFunc); |
|
|
|
var slope = ssXY / ssXX; |
|
var intercept = yBar - (xBar * slope); |
|
var rSquare = Math.pow(ssXY, 2) / (ssXX * ssYY); |
|
|
|
return { |
|
slope: slope, |
|
intercept: intercept, |
|
rSquare: rSquare |
|
}; |
|
} |
|
|
|
</script> |