block by tophtucker 01c65df41d3776429f55b7132f9446cd

Trump support vs. distance from major city

Full Screen

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.

index.html

<!DOCTYPE html>
<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
  // //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 //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>