Visualizationifyizing the weak positive correlation between distance from the nearest major city and the support for Trump vs. Clinton in the general election. This continues my translating-counties-around-in-silly-ways series: distance from coast vs. primary result, distance from coast vs. general result, and this nonsense.
The result is uninteresting; I just wanted to try the chained transition illustrating the "methodology", such as it is. All data graphics should be data+code graphics! Since there's ultimately no difference between data and code. :) You could even visualize provenance etc. Object constancy baby. Every graphic a geometric proof of itself. Etc etc etc.
You can view up to the 1,000 largest cities (actually 998 because I exclude Alaska and Hawaii...) by appending a ?n=50
query string to the raw version (default is 50). E.g.: 2 cities; 998 cities.
TODO: Also figure out how these cities voted, or just align them with their counties. Hm. Obvious oversight, sorry.
xxxxxxxxxx
<meta charset="utf-8">
<style>
body {
width: 960px;
margin: 0;
position: relative;
font-size: 14px;
font-family: sans-serif;
text-transform: uppercase;
}
svg {
overflow: visible;
}
.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;
}
circle.city {
fill: white;
stroke: black;
stroke-width: 1;
}
</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 urlParams = new URLSearchParams(window.location.search);
var n = urlParams.has('n') ? urlParams.get('n') : 50,
width = 960,
height = 500,
margin = [100, 50]; //side, top
var projection = d3.geoAlbers()
.translate([width / 2, height / 2]);
var path = d3.geoPath()
.projection(projection);
var color = function(d) {
if(d.trumpFraction === null) return 'none';
return d3.scaleLinear()
.domain([0,1])
.range(['#3c3b6e', '#b22234'])
(d.trumpFraction)
}
var x = d3.scaleLinear(),
y = d3.scaleLinear();
var svg = d3.select("body").append("svg")
.attr("width", width)
.attr("height", height);
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('Urban')
.style('text-anchor', 'beginning')
.attr('dx', '1em');
labels.append('text')
.attr('x', width)
.attr('y', height/2)
.text('Rural')
.style('text-anchor', 'end')
.attr('dx', '-1em');
var geoButton = d3.select('body').append('button')
.style('top', '1em')
.style('left', '1em')
.text("Geographify")
var scatButton = d3.select('body').append('button')
.style('top', '1em')
.style('right', '1em')
.text("Scatterify")
queue()
.defer(d3.json, "us.json")
.defer(d3.tsv, "results.tsv", parseElection)
.defer(d3.json, "cities.json")
.await(ready);
function ready(error, us, election, cities) {
if (error) throw error;
var counties = topojson.feature(us, us.objects.counties);
cities = cities
.filter(d => d.state !== "Hawaii" && d.state !== "Alaska")
.slice(0,n);
counties.features = counties.features.filter(function(county, i) {
var fips = county.id.toString();
return !(fips.substr(0,1) == '2' && fips.length == 4) && //Alaska
!(fips.substr(0,2) == '15' && fips.length == 5) && //Hawaii
!(fips.substr(0,2) == '72' && fips.length == 5) && // Puerto Rico
!(fips.substr(0,2) == '60' && fips.length == 5) && // American Samoa
!(fips.substr(0,2) == '66' && fips.length == 5) && // Guam
!(fips.substr(0,2) == '78' && fips.length == 5) // Virgin Islands
})
counties.features.forEach(function(county, i) {
// 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);
// find nearest city (slowly)
var citydistances = cities.map((c,ci) =>
d3.geoDistance(
projection.invert(county.centroid),
[c.longitude, c.latitude]
)
)
county.nearestCity = cities[citydistances.indexOf(Math.min(...citydistances))]
// calculate distance and angle
if(county.nearestCity) {
var city = projection([county.nearestCity.longitude, county.nearestCity.latitude]);
county.theta = Math.atan2(city[1] - county.centroid[1], county.centroid[0] - city[0])
county.distance = dist(city, county.centroid)
county.geoDistance = Math.min(...citydistances)
}
});
x
.domain(d3.extent(counties.features.map(d => d.geoDistance)))
.range([0 + margin[0], width - margin[0]])
y
.domain([0,1])
.range([height - margin[1], 0 + margin[1]])
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);
var city = svg.selectAll("circle.city")
.data(cities)
.enter()
.append("circle")
.classed("city", true)
.attr("fill", "white")
.attr("r", 3)
.attr("cx", function(d) { return projection([d.longitude, d.latitude])[0]; })
.attr("cy", function(d) { return projection([d.longitude, d.latitude])[1]; })
geoButton.on('click', geographify);
scatButton.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.geoDistance; });
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.geoDistance}), leastSquaresCoeff.intercept + d3.max(eligibleCounties, function(d) { return d.geoDistance}) * leastSquaresCoeff.slope]
];
var trendlinePath = d3.line()
.x(function(d) { return x(d[0]); })
.y(function(d) { return y(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');
setTimeout(scatterify, 3000);
function scatterify() {
city.transition()
.delay(1000)
.duration(2000)
.attr("transform", (c,i) => "translate(" + getDisplacement(c) + ")")
.transition()
.duration(2000)
.attr("transform", (c,i) => "translate(" + getCollapse(c) + ")")
.transition()
.delay(2000)
.duration(2000)
.attr("transform", (c,i) => "translate(" + getToOrigin(c) + ")")
.transition()
.style("opacity", 0);
county.transition()
.delay(1000)
.duration(2000)
.attr("transform", (d,i) => !d.nearestCity ? null : "translate(" + getDisplacement(d.nearestCity) + ")")
.transition()
.duration(2000)
.attr("transform", (d,i) => !d.nearestCity ? null : "translate(" + getCollapse(d.nearestCity) + ")")
.style("opacity", .25)
.transition()
.duration(2000)
.attrTween("transform", function(d,i) {
return function(t) {
if(!d.nearestCity) return null;
return "translate(" + get1Distance(d,t) + ")"
}
})
.transition()
.duration(2000)
.attr("transform", (d,i) => !d.nearestCity ? null : "translate(" + getScatterX(d) + ")")
.style("opacity", .5)
.transition()
.duration(2000)
.attr("transform", (d,i) => !d.nearestCity ? null : "translate(" + getScatter(d) + ")")
labels.transition()
.delay(8000)
.duration(1000)
.style('opacity', 1);
trendline.transition()
.delay(10000)
.duration(1000)
.style('opacity', 1);
scatButton.attr('disabled', 'disabled');
geoButton.attr('disabled', null);
}
function geographify() {
city.transition()
.duration(2000)
.attr("transform", "translate(0,0)")
.style("opacity", 1)
county.transition()
.duration(2000)
.attr("transform", "translate(0,0)")
.style("opacity", 1)
labels.transition()
.duration(2000)
.style('opacity', 0);
trendline.transition()
.duration(2000)
.style('opacity', 0);
scatButton.attr('disabled', null);
geoButton.attr('disabled', 'disabled');
}
}
function getDisplacement(d) {
var p = projection([d.longitude, d.latitude]);
var dx = p[0] - width/2
var dy = p[1] - height/2
return [dx/2,dy/2]
}
function getCollapse(d) {
var p = projection([d.longitude, d.latitude]);
var dx = p[0] - width/2
var dy = p[1] - height/2
return [-dx,-dy]
}
function getToOrigin(d) {
var p = projection([d.longitude, d.latitude]);
var dx = p[0] - margin[0]
var dy = p[1] - (height - margin[1])
return [-dx,-dy]
}
function get1Distance(d, t) {
var city = projection([d.nearestCity.longitude, d.nearestCity.latitude]);
var dx = -city[0] + width/2 + (city[0] - d.centroid[0])
var dy = -city[1] + height/2 + (city[1] - d.centroid[1])
var i = d3.interpolateNumber(d.theta, 0);
return [
dx + d.distance * Math.cos(i(t)),
dy - d.distance * Math.sin(i(t))
]
}
function getScatterX(d) {
return [
x(d.geoDistance) - d.centroid[0],
height - margin[1] - d.centroid[1]
]
}
function getScatter(d) {
return [
x(d.geoDistance) - d.centroid[0],
y(d.trumpFraction) - d.centroid[1]
]
}
function dist(a,b) {
return Math.sqrt(
Math.pow(b[0] - a[0], 2) +
Math.pow(b[1] - a[1], 2)
)
}
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