Inspired by Lorcan’s tweet positing a positive correlation between distance from the sea and support for Donald Trump, which — based on the primary, which is all I could find county data for — doesn’t appear to be the case!
Based on Bostock's Coastal Graph Distance map. Using this 2016 Republican primary data.
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.v3.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.geo.albersUsa()
.translate([width / 2, height / 2]);
var path = d3.geo.path()
.projection(projection);
// var colorCoast = d3.scale.ordinal()
// .domain(d3.range(9).reverse())
// .range(["#ffffd9","#edf8b1","#c7e9b4","#7fcdbb","#41b6c4","#1d91c0","#225ea8","#253494","#081d58"]);
// var colorTrump = d3.scale.linear()
// .domain([0,1])
// .range(['gray', 'red']);
var hue = d3.scale.linear()
.domain([0,9])
.range([180,360])
.clamp(true);
var saturation = d3.scale.linear()
.domain([0,1])
.range([0,100])
.clamp(true);
var lightness = d3.scale.linear()
.domain([.2,.8])
.range([80,20])
.clamp(true);
var color = function(d) {
if(d.trumpFraction === null) return "rgba(255,255,255,0)";
return "hsl("+hue(d.distance)+","+100+"%, "+lightness(d.trumpFraction)+"%)";
}
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.csv, "primary_results.csv", parsePrimary)
.await(ready);
function ready(error, us, coastalsArray, primary) {
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 countyPrimary = primary.filter(function(d) {
return d.fips === county.id;
});
county.trumpFraction = countyPrimary[0] ? countyPrimary[0].fraction_votes : 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.scale.linear()
.domain(d3.extent(counties.features, function(d) { return d.distance; }))
.range([0 + sideMargin, width - sideMargin]);
var trumpScale = d3.scale.linear()
.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% of Republican primary for Trump')
.style('text-anchor', 'middle')
.attr('dy', '2em');
labels.append('text')
.attr('x', width/2)
.attr('y', height)
.text('0% of Republican primary for Trump')
.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);
console.log(leastSquaresCoeff);
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.svg.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');
// START WITH A TRANSITION
scatterify();
function scatterify() {
county.transition()
.duration(2000)
.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(2000)
.style('opacity', 1);
trendline.transition()
.duration(2000)
.style('opacity', 1);
scatButton.attr('disabled', 'disabled');
geoButton.attr('disabled', null);
}
function geographify() {
county.transition()
.duration(2000)
.attr("transform", function(d) {
return "translate("+0+","+0+")";
});
labels.transition()
.duration(2000)
.style('opacity', 0);
trendline.transition()
.duration(2000)
.style('opacity', 0);
scatButton.attr('disabled', null);
geoButton.attr('disabled', 'disabled');
}
}
function parsePrimary (d) {
if(d.candidate !== "Donald Trump") return null;
d.fips = parseInt(d.fips);
d.fraction_votes = parseFloat(d.fraction_votes);
return d;
}
// 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.v3.min.js
https://d3js.org/topojson.v1.min.js
https://d3js.org/queue.v1.min.js