Based on @mbostock’s Coastal Graph Distance map. So the major caveat is that it measures distance by how many counties away you are from a coastal county, not by actual physical as-the-crow-flies distance; notice how the pulsing isoline races across the county-sparse West and crawls along the county-dense East.
Using @mkearney’s election result data. Inspired by Lorcan’s tweet positing a positive correlation between distance from the sea and support for Donald Trump, which appeared weak in the final election. An update of my earlier block based on the Republican primary.
County-level Alaska data is missing and it’s not clear that it’s anywhere (not NYT, not Guardian) and I’m not super inclined to try to read all those pdfs and map the corresponding precincts to counties and whatever from the Alaska elections website because it’s 4 a.m. and other reasons.
Also I had to hand-enter Oglala Lakota County née Shannon County, fips code 46113, from the NYT results. I should either figure out why it’s missing from that Github repo or add it? Note to self. Whatever it doesn’t matter anymore ha ha h hah h aha
xxxxxxxxxx
<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
// https://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 https://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>
https://d3js.org/d3.v4.min.js
https://d3js.org/topojson.v1.min.js
https://d3js.org/queue.v1.min.js