Skip to content

Instantly share code, notes, and snippets.

@psthomas
Last active January 7, 2018 05:33
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save psthomas/bc1af938878c6d3f7cceaf9fee443ff5 to your computer and use it in GitHub Desktop.
Save psthomas/bc1af938878c6d3f7cceaf9fee443ff5 to your computer and use it in GitHub Desktop.
Presidential Voter Turnout and Margins by County, 2004-2016

This scatter plot shows voter turnout and margins by county for the 2004-2016 presidential elections.

This visualization includes a search feature to allow users to filter by state or county, an option to explore "what-if" scenarios by clicking and dragging counties, and an option to weight the county bubble sizes by a variety of metrics.

More info here: https://pstblog.com/2017/06/05/national-election-vis.

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>National Elections</title>
<script src="https://d3js.org/d3.v4.min.js"></script>
<style type="text/css">
body {
color: #888;
font-family: serif;
font-size:15px;
}
svg {
/* margin-top: 2.5em;
margin-bottom: 2.5em;*/
}
.axis {
font: 12px sans-serif;
}
.axis path,
.axis line {
fill: none;
stroke: #aaa;
shape-rendering: crispEdges;
}
.axis text {
fill: #858585;
}
.title {
font: 500 100px serif;
fill: #e5e5e5;
}
.party {
font: 500 35px serif;
fill: #e5e5e5;
}
.incr {
font: 500 35px serif;
fill: #e5e5e5;
cursor: pointer;
}
.incr:hover {
fill: #ccc;
}
.circle {
stroke: white;
stroke-width: 0.1px;
/*https://stackoverflow.com/questions/5697067*/
cursor: move; /* grab; pointer; move; -webkit-grab;*/
fill-opacity: 0.9;
}
.circle:hover {
fill-opacity: 0.8;
}
#searchform {
padding-bottom: 5px;
}
#formholder {
position: absolute;
right: 50px;
top: 25px;
/*font: 16px serif;*/
}
#select {
padding:5px 0px;
}
input {
/*font: 14px serif;*/
}
</style>
</head>
<body>
<div id="formholder" >
<div id="select">
<select id="inds">
<option value="county" selected="selected">County</option>
<option value="state">State</option>
<option value="race">Race</option>
<option value="gender">Gender</option>
<option value="age">Age</option>
<option value="education">Education</option>
</select>
</div>
<!-- onSubmit="return handleClick()" -->
<form id="searchform" name="myform" onSubmit="return handleClick()" >
<input type="text" id="myVal" size="30" placeholder="NY, WI, Los Angeles County">
<input id="search" name="Submit" type="button" value="Search" > <!--onclick="handleClick();"-->
<input type="button" value="X" onclick="document.myform.reset(); handleClick();" > <!---->
</form>
<form id="areaform">
Weight:
<input type="radio" name="area" value="vote" checked>Vote
<input type="radio" name="area" value="electoral" > Electoral
<input type="radio" name="area" value="vpi" >VPI<br>
</form>
</div>
<script type="text/javascript">
// {top: 20, right: 20, bottom: 50, left: 30}
var margin = {top: 30, right: 20, bottom: 50, left: 50},
width = 0.95*window.innerWidth - margin.left - margin.right,
height = 0.9*window.innerHeight - margin.top - margin.bottom;
//Globals:
var currentSearch = "";
var searchTerms = "";
var selection = "county";
var year;
var cache = {};
var locurls = {
'county': './US_County_Level_Presidential_Results_04-16.csv',
'state': './US_State_Level_Presidential_Results_04-16.csv',
'demographic': './US_Demographic_Presidential_Results_04-16.csv'
}
var weburls = {
'county': 'https://raw.githubusercontent.com/psthomas/election-vis/master/US_County_Level_Presidential_Results_04-16.csv',
'state': 'https://raw.githubusercontent.com/psthomas/election-vis/master/US_State_Level_Presidential_Results_04-16.csv',
'demographic': 'https://raw.githubusercontent.com/psthomas/election-vis/master/US_Demographic_Presidential_Results_04-16.csv'
}
var local = false;
var urls = local ? locurls : weburls;
//Formatting Functions
var pctFormat = d3.format(".2%");
var thsdFormat = d3.format(",");
function capitalize(string) {
return string.charAt(0).toUpperCase() + string.slice(1);
}
//Create SVG
var svg = d3.select("body").append("svg")
.attr("width", width + margin.left + margin.right)
.attr("height", height + margin.top + margin.bottom)
.append("g")
.attr("transform", "translate(" + margin.left + "," + margin.top + ")");
//Year Title
var title = svg.append("text")
.attr("class", "title")
.attr("dy", height-10)
.attr("dx", ".35em");
var demtext = svg.append("text")
.attr("class", "party")
.attr("dy", height-50)
.attr("dx", 243);
var reptext = svg.append("text")
.attr("class", "party")
.attr("dy", height-14)
.attr("dx", 243);
//Define static scales
var xScale = d3.scaleLinear()
.domain([-100, 100])
.range([0, width]);
var yScale = d3.scaleLinear()
.domain([0, 100])
.range([height, 0]);
//Base the color scale on the democratic margin.
var colorScale = d3.scaleLinear()
.domain([-80, 0, 80])
.range(['#EF3B2C', '#885ead', '#08519C'])
.interpolate(d3.interpolateRgb);
//Define x, y axes
var xAxis = d3.axisBottom(xScale);
var yAxis = d3.axisLeft(yScale);
//Append Axes
svg.append("g")
.attr("class", "axis")
.attr("transform", "translate(0," + height + ")")
.call(xAxis)
.append("text")
.attr("y", "3em")
.attr("x", width/2)
.text("Democratic Margin (%)");
svg.append("g")
.attr("class", "axis")
.call(yAxis)
.attr("transform", "translate(" + (width/2) + ",0)")
.append("text")
.attr("transform", "rotate(-90)")
.attr("y", 6)
.attr("dy", "-3.75em")
.style("text-anchor", "end")
.text("Turnout (%VAP)");
//Statically place tooltip:
var tooltip = d3.select("#formholder")
.append("div")
.style("position", "absolute")
.style("visibility", "hidden")
.attr("class", "tooltip");
d3.select('#inds').on("change", function () {
var sel = document.getElementById("inds");
selection = sel.options[sel.selectedIndex].value; //Sets a global
switch (selection) {
case 'county':
renderCounties();
break;
case 'state':
renderStates();
break;
case 'race':
renderDemographic('race');
//Eventually renderDemographic('race');
break;
case 'gender':
renderDemographic('gender');
break;
case 'age':
renderDemographic('age');
break;
case 'education':
renderDemographic('education');
break;
}
});
// Helper functions:
function getYearData(dataset, year) {
for (var i=0; i<dataset.length; i++) {
if (Number(dataset[i].key) === year) {
return dataset[i].values;
}
}
}
function copyObj(original) {
return JSON.parse(JSON.stringify(original));
}
function handleClick(event){
currentSearch = document.getElementById("myVal").value;
var words = currentSearch.split(",").map(function(s) { return s.trim() });
//Modifies global search variable, also used in update()
searchTerms = RegExp(words.join('|'), "g"); //gi is case insensitive global, g is just global
//svg.selectAll
d3.selectAll("circle").each(function(d) {
var circle = d3.select(this);
switch (selection) {
case 'county':
var field = d.county;
var count = d.county_num;
break;
case 'state':
var field = d.state;
var count = d.num_state;
break;
case 'race':
case 'gender':
case 'age':
case 'education':
var field = d.group;
var count = d.num_group;
break;
}
if ( (field.search(searchTerms) != -1) || (currentSearch == "") ) { //(d.county.search(searchTerms) != -1)
circle.style('pointer-events', 'auto');
circle.attr("fill", colorScale(((d.num_dem-d.num_rep)/count)*100));
circle.style("stroke-opacity", 1);
} else {
circle.style('pointer-events', 'none');
circle.attr("fill", "rgba(192,192,192,0.05)");
circle.style("stroke-opacity", 0);
}
});
return false; //Don't reload page
}
function renderCounties() {
if (cache.counties) {
build(cache.counties);
} else {
d3.csv(urls.county, parseRows, function(error, data) {
if (error) {throw error};
//Update cache:
cache.counties = data;
build(data);
});
}
function parseRows(d) {
return {'county': d.county_name + ', ' + d.state, 'state': d.state, 'county_num': +d.county_num, 'turnout': +d.turnout,
'num_rep': +d.rep_num, 'num_dem': +d.dem_num, 'year': +d.year, 'state_electoral_votes': +d.state_electoral_votes,
'vap': +d.vap, 'id': +d.fips_code, 'dem_margin': +d.dem_margin*100};
}
function tooltipOn(d) {
tooltip.style("visibility", "visible")
.html(
d.county + "<br>" +
"County: D: " + pctFormat(d.num_dem/d.county_num) +
" R: " + pctFormat(d.num_rep/d.county_num) + "<br>" +
"Turnout: " + pctFormat(d.turnout) + "<br>" +
"Voters: " + thsdFormat(Math.round(d.county_num)) + "<br>" +
"State: D: " + pctFormat(d.num_state_dem/d.num_state) +
" R: " + pctFormat(d.num_state_rep/d.num_state) + "<br>" );
}
function build(data) {
// Build state data sums
var stateData = d3.nest()
.key(function(d) { return d.year; })
.key(function(d) { return d.state; })
.rollup(function(v) { return {
'num_state': d3.sum(v, function(d) { return d.county_num; }),
'num_state_dem': d3.sum(v, function(d) { return d.num_dem; }),
'num_state_rep': d3.sum(v, function(d) { return d.num_rep; }),
}; })
.object(data);
// Add the state level data to each county object
data.forEach(function(d) {
d.num_state = stateData[d.year][d.state]['num_state'],
d.num_state_dem = stateData[d.year][d.state]['num_state_dem'],
d.num_state_rep = stateData[d.year][d.state]['num_state_rep']
});
var dataset = d3.nest()
.key(function(d) { return +d.year; })
.sortValues(function(a,b) { return b.county_num - a.county_num; } ) //Bring smallest to front
.object(data);
//.entries(data);
var years = Object.keys(dataset).map(Number);
years.sort()
year = year ? year : years[0];
//Create a copy, so it can be edited on drag:
//var yearData = Object.assign([], getYearData(dataset, year));
//var yearData = copyObj(getYearData(dataset, year));
var yearData = copyObj(dataset[String(year)]);
//Data is just array of all objects from csv, used to find max of all years
var rScale = d3.scaleLinear()
.domain([0, d3.max(data, function(d) {return Math.sqrt(d.county_num/Math.PI); })]) //Area proportional to votes
.range([1, 35]); //2,60 2, 30
var rScale_electoral = d3.scaleLinear()
.domain([0, d3.max(data, function(d) {
return Math.sqrt((d.county_num/d.num_state)*d.state_electoral_votes/Math.PI); //Area proportional to electoral votes
})])
.range([1, 35]);
var rScale_vpi = d3.scaleLinear()
.domain([0, d3.max(data, function(d) {
var county_vpi = (d.county_num/d.num_state) * (d.state_electoral_votes/(Math.abs(d.num_state_dem-d.num_state_rep)));
// Using VAP instead
//var county_vpi = (d.vap/d.num_state) * (d.state_electoral_votes/(Math.abs(d.num_state_dem-d.num_state_rep)));
return Math.sqrt(county_vpi/Math.PI); //Area proportional to VPI
})])
.range([1, 35]); //2,60 2, 30
//Append increment buttons
var incr = svg.append("text")
.attr("class", "incr")
.attr("dy", height-43) // 1em
.attr("dx", 0) //.5em
.html("&#9650;")
.on("click", function() {
year += 4;
if (year > years[years.length - 1]) {
year = years[0]
}
//Assign to new object, update circles:
getYearData(dataset, year);
//yearData = Object.assign([], getYearData(dataset, year));
yearData = copyObj(dataset[String(year)]);
update(yearData, year);
});
var decr = svg.append("text")
.attr("class", "incr")
.attr("dy", height-10)
.attr("dx", 0) //".5em"
.html("&#9660;")
.on("click", function() {
year -= 4;
if (year < years[0]) {
year = years[years.length - 1];
}
//Assign to new object, update:
//yearData = Object.assign([], getYearData(dataset, year));
//yearData = copyObj(getYearData(dataset, year));
yearData = copyObj(dataset[String(year)]);
update(yearData, year);
});
function getRadioScaled(d) {
var checked = document.querySelector('input[name="area"]:checked').value;
if (checked === "electoral") {
return rScale_electoral(Math.sqrt((d.county_num/d.num_state)*d.state_electoral_votes/Math.PI));
// d.county_num Math.pow(d.county_num, 0.6) Math.sqrt(d.county_num/Math.PI) Math.sqrt(d *4/Math.PI)
} else if (checked === "vpi") {
//voter power index:
//http://www.dailykos.com/story/2016/12/19/1612252/-Voter-Power-Index-Just
//-How-Much-Does-the-Electoral-College-Distort-the-Value-of-Your-Vote
// Using county_num
var county_vpi = (d.county_num/d.num_state) * (d.state_electoral_votes/(Math.abs(d.num_state_dem-d.num_state_rep)));
// using VAP:
//var county_vpi = (d.vap/d.num_state) * (d.state_electoral_votes/(Math.abs(d.num_state_dem-d.num_state_rep)));
return rScale_vpi(Math.sqrt(county_vpi/Math.PI));
} else {
//Area proportional to number of voters
return rScale(Math.sqrt(d.county_num/Math.PI));
}
}
function updateAreas(event){
d3.selectAll("circle").transition()
.duration(1000) //750;
.attr("r", function(d) {
return getRadioScaled(d);
});
}
d3.select("#areaform")
.on("click", function () {
return updateAreas();
});
//Dragging behavior
//https://bl.ocks.org/mbostock/6123708
var drag = d3.drag()
.on("drag", dragged)
.on("end", ended);
function dragged(d) {
//Remove transitions temporarily
d3.selectAll("circle").transition();
//Issue when dragged across 0 threshold, county_num = 0
if (d3.event.y >= height) {
return;
}
//Relocate circle with mouse
d3.select(this).attr("cx", d.x = d3.event.x).attr("cy", d.y = d3.event.y);
//Avoid case of no shift
if (d.x === undefined || d.y === undefined) {
return;
}
var newMargin = xScale.invert(d.x)/100,
newTurnout = yScale.invert(d.y)/100, //Math.abs() d.y
oldTurnout = d.turnout,
oldMargin = (d.num_dem-d.num_rep)/d.county_num,
marginChange = newMargin-oldMargin,
oldDfrac = d.num_dem/d.county_num,
oldRfrac = d.num_rep/d.county_num,
oldNumDem = d.num_dem,
oldNumRep = d.num_rep;
//Recalculate fractions based on margin change
//Half goes to each side, zero sum
dfrac = oldDfrac + marginChange/2;
rfrac = oldRfrac - marginChange/2;
//Add increses in turnout to county_num
d.county_num = newTurnout*d.vap;
// Recalculate based on margin change first, assumes
// margin changes are zero sum between parties.
d.num_dem = dfrac*d.county_num;
d.num_rep = rfrac*d.county_num;
d.turnout = newTurnout;
//Change state level data
d.num_state += (newTurnout - oldTurnout)*d.vap //Multiply turnout change times County Voting Age population
d.num_state_dem += d.num_dem - oldNumDem; //Update state dem and rep counts
d.num_state_rep += d.num_rep - oldNumRep;
//Call the tooltip function each time to update.
tooltipOn(d);
//Update all other counties in state
for (var i = 0; i<yearData.length; i++) {
if (yearData[i].state === d.state) {
yearData[i].num_state = d.num_state;
yearData[i].num_state_dem = d.num_state_dem;
yearData[i].num_state_rep = d.num_state_rep;
}
}
//Update score as well:
updateScore(yearData);
}
function ended(d) {
// Modify the yeardata, then call update(yearData, d.year);
for (var i = 0; i<yearData.length; i++) {
if (yearData[i].state === d.state) {
yearData[i].num_state = d.num_state;
yearData[i].num_state_dem = d.num_state_dem;
yearData[i].num_state_rep = d.num_state_rep;
}
}
update(yearData, d.year);
}
function updateScore(yearData) {
//Could get this data directly from dataframe,
//but want to calculate so can be updated easily on drag.
var sums = [0,0,0];
var electoral_sums = [[],0,0];
for (var i=0; i<yearData.length; i++) {
sums[0] += yearData[i].num_dem;
sums[1] += yearData[i].num_rep;
sums[2] += yearData[i].county_num;
//Sum up electoral votes for each unique state
}
// console.log(yearData);
var states = [];
var stateYearData = yearData.filter( function(current) {
//console.log(current);
//return states.indexOf(current.state)
if (states.indexOf(current.state) === -1) {
states.push(current.state);
return true;
}
return false;
});
for (var i = 0; i<stateYearData.length; i++) {
var stateName = stateYearData[i].state;
var currentYear = stateYearData[i].year;
if (currentYear === 2016 && ['ME','NE'].indexOf(stateName) !== -1) { //['ME','NE'].indexOf(stateName) !== -1 (stateName === 'ME' || stateName === 'NE')
//console.log('entered');
//Handle split states
// electoral_sums[1] += 3; // ME + NB
// electoral_sums[2] += 6; // ME + NB
//Will be entered twice, so give 1.5, 3 each entrance instead:
electoral_sums[1] += 1.5; // ME + NB
electoral_sums[2] += 3; // ME + NB
//electoral_sums[0].push(stateName === 'ME' ? 'NE' : 'ME'); //Make sure both states are added
//electoral_sums[0] = electoral_sums[0].concat(['ME','NE']);
} else if (currentYear === 2008 && stateName === 'NE' ) {
electoral_sums[1] += 1;
electoral_sums[2] += 4;
} else if (stateYearData[i].num_state_dem > stateYearData[i].num_state_rep ) {
//electoral_sums[1] is dems
electoral_sums[1] += stateYearData[i].state_electoral_votes;
} else {
electoral_sums[2] += stateYearData[i].state_electoral_votes;
}
}
var dfrac = sums[0]/sums[2],
rfrac = sums[1]/sums[2];
//update dfrac rfrac text, update color background
demtext.text('D ' + pctFormat(dfrac) + ' ' + electoral_sums[1])
reptext.text('R ' + pctFormat(rfrac) + ' ' + electoral_sums[2])
if (electoral_sums[1] > electoral_sums[2]) {
demtext.style('fill', '#bbb');
reptext.style('fill', null);
} else {
demtext.style('fill', null);
reptext.style('fill', '#bbb');
}
//Optional, set background color based on winner
// var backColor = dfrac > rfrac ? colorScale(5) : colorScale(-5);
// //var backColor = colorScale((dfrac-rfrac)*100);
// d3.selectAll('svg')
// .style('background-color', backColor);
}
function update(yearData, year) {
//Change D,R scores:
updateScore(yearData);
//Update title
title.text(year);
var circles = svg.selectAll("circle")
.data(yearData, function(d) { return d.id ; }); // Key for object constancy: https://bost.ocks.org/mike/constancy/
circles.exit().remove();
circles.enter().append("circle")
.on("mouseover", tooltipOn) //Add tooltip before transition
.on("mouseout", function(d){return tooltip.style("visibility", "hidden");})
.call(drag)
.attr("class", "circle")
.merge(circles)
.transition()
.duration(1000) //750; 1000
.attr("cx", function(d) {
return xScale(((d.num_dem-d.num_rep)/d.county_num)*100);
})
.attr("cy", function(d) {
return yScale(d.turnout*100);
})
.attr("r", function(d) {
return getRadioScaled(d);
})
.attr("fill",function(d){
if (d.county.search(searchTerms) != -1) {
return colorScale(((d.num_dem-d.num_rep)/d.county_num)*100);
}else {
return "rgba(192,192,192,0.05)";
}
});
}
function initialize(yearData, year) {
//Change D,R scores:
updateScore(yearData);
//Update title
title.text(year);
var circles = svg.selectAll("circle")
.data(yearData, function(d) { return d.id ; }); // Key for object constancy: https://bost.ocks.org/mike/constancy/
circles.exit().remove();
//Create any new circles
circles.enter().append("circle")
.attr("class", "circle")
.attr("cx", function(d) {
return xScale(((d.num_dem-d.num_rep)/d.county_num)*100);
})
.attr("cy", function(d) {
return yScale(d.turnout*100);
})
.attr("r", function(d) {
//return rScale(Math.sqrt(d.county_num/Math.PI));
return getRadioScaled(d);
})
.attr("fill",function(d){
if (d.county.search(searchTerms) != -1) {
return colorScale(((d.num_dem-d.num_rep)/d.county_num)*100);
}else {
return "rgba(192,192,192,0.05)";
}
})
.call(drag)
.on("mouseover", tooltipOn)
.on("mouseout", function(d){return tooltip.style("visibility", "hidden");});
}
//Initialize scatterplot
initialize(yearData, year);
//update(yearData, year); //Works, but transition is weird.
} //);
}
function renderStates() {
if (cache.states) {
build(cache.states);
} else {
d3.csv(urls.state, parseRows, function(error, data) {
if (error) {throw error};
//Update cache:
cache.states = data;
build(data);
});
}
function tooltipOn(d) {
tooltip.style("visibility", "visible")
.html(
"State: " + d.state + "<br>" +
" D: " + pctFormat(d.num_dem/d.num_state) +
" R: " + pctFormat(d.num_rep/d.num_state) + "<br>" +
"Turnout: " + pctFormat(d.turnout) + "<br>" +
"Voters: " + thsdFormat(Math.round(d.num_state)) + "<br>")
}
function parseRows(d) {
return {'state': d.state, 'id': d.state, 'num_state': +d.state_num, 'turnout': +d.state_num/+d.vap,
'num_rep': +d.rep_num, 'num_dem': +d.dem_num, 'year': +d.year, 'state_electoral_votes': +d.state_electoral_votes,
'vap': +d.vap, 'dem_margin': (+d.dem_num - +d.rep_num) / +d.state_num};
}
function build(data) {
var dataset = d3.nest()
.key(function(d) { return +d.year; })
.sortValues(function(a,b) { return b.num_state - a.num_state; } ) //Bring smallest to front
.object(data);
//.entries(data);
var years = Object.keys(dataset).map(Number);
years.sort()
//var year = years[0];
year = year ? year : years[0];
//Create a copy, so it can be edited on drag:
var yearData = copyObj(dataset[String(year)]);
//Data is just array of all objects from csv, used to find max of all years
var rScale = d3.scaleLinear()
.domain([0, d3.max(data, function(d) {return Math.sqrt(d.num_state/Math.PI); })]) //Area proportional to votes
.range([1, 35]); //2,60 2, 30
var rScale_electoral = d3.scaleLinear()
.domain([0, d3.max(data, function(d) {
return Math.sqrt(d.state_electoral_votes/Math.PI); //Area proportional to electoral votes
})])
.range([1, 35]);
var rScale_vpi = d3.scaleLinear()
.domain([0, d3.max(data, function(d) {
var county_vpi = d.state_electoral_votes/(Math.abs(d.num_dem-d.num_rep));
// Using VAP instead
//var county_vpi = (d.vap/d.num_state) * (d.state_electoral_votes/(Math.abs(d.num_state_dem-d.num_state_rep)));
return Math.sqrt(county_vpi/Math.PI); //Area proportional to VPI
})])
.range([1, 35]); //2,60 2, 30
//Append increment buttons
var incr = svg.append("text")
.attr("class", "incr")
.attr("dy", height-43) // 1em
.attr("dx", 0) //.5em
.html("&#9650;")
.on("click", function() {
year += 4;
if (year > years[years.length - 1]) {
year = years[0]
}
//Assign to new object, update circles:
yearData = copyObj(dataset[String(year)]);
update(yearData, year);
});
var decr = svg.append("text")
.attr("class", "incr")
.attr("dy", height-10)
.attr("dx", 0) //".5em"
.html("&#9660;")
.on("click", function() {
year -= 4;
if (year < years[0]) {
year = years[years.length - 1];
}
//Assign to new object, update:
yearData = copyObj(dataset[String(year)]);
update(yearData, year);
});
function getRadioScaled(d) {
var checked = document.querySelector('input[name="area"]:checked').value;
if (checked === "electoral") {
return rScale_electoral(Math.sqrt(d.state_electoral_votes/Math.PI));
// d.county_num Math.pow(d.county_num, 0.6) Math.sqrt(d.county_num/Math.PI) Math.sqrt(d *4/Math.PI)
} else if (checked === "vpi") {
//voter power index:
//http://www.dailykos.com/story/2016/12/19/1612252/-Voter-Power-Index-Just
//-How-Much-Does-the-Electoral-College-Distort-the-Value-of-Your-Vote
// Using county_num
var county_vpi = d.state_electoral_votes/(Math.abs(d.num_dem-d.num_rep));
// using VAP:
//var county_vpi = (d.vap/d.num_state) * (d.state_electoral_votes/(Math.abs(d.num_state_dem-d.num_state_rep)));
return rScale_vpi(Math.sqrt(county_vpi/Math.PI));
} else {
//Area proportional to number of voters
return rScale(Math.sqrt(d.num_state/Math.PI));
}
}
function updateAreas(event){
d3.selectAll("circle").transition()
.duration(1000) //750;
.attr("r", function(d) {
return getRadioScaled(d);
});
}
d3.select("#areaform")
.on("click", function () {
return updateAreas();
});
//Dragging behavior
//https://bl.ocks.org/mbostock/6123708
var drag = d3.drag()
.on("drag", dragged)
.on("end", ended);
function dragged(d) {
//Remove transitions temporarily
d3.selectAll("circle").transition();
//Issue when dragged across 0 threshold, county_num = 0
if (d3.event.y >= height) {
return;
}
//Relocate circle with mouse
d3.select(this).attr("cx", d.x = d3.event.x).attr("cy", d.y = d3.event.y);
//Avoid case of no shift
if (d.x === undefined || d.y === undefined) {
return;
}
var newMargin = xScale.invert(d.x)/100,
newTurnout = yScale.invert(d.y)/100, //Math.abs() d.y
oldTurnout = d.turnout,
oldMargin = (d.num_dem-d.num_rep)/d.num_state,
marginChange = newMargin-oldMargin,
oldDfrac = d.num_dem/d.num_state,
oldRfrac = d.num_rep/d.num_state,
oldNumDem = d.num_dem,
oldNumRep = d.num_rep;
//Recalculate fractions based on margin change
//Half goes to each side, zero sum
dfrac = oldDfrac + marginChange/2;
rfrac = oldRfrac - marginChange/2;
//Add increses in turnout to county_num
d.num_state = newTurnout*d.vap;
// Recalculate based on margin change first, assumes
// margin changes are zero sum between parties.
d.num_dem = dfrac*d.num_state;
d.num_rep = rfrac*d.num_state;
d.turnout = newTurnout;
//Call the tooltip function each time to update.
tooltipOn(d);
//Update score as well:
updateScore(yearData);
}
function ended(d) {
//No longer needed, as this is at state level?:
// Modify the yeardata, then call update(yearData, d.year);
for (var i = 0; i<yearData.length; i++) {
if (yearData[i].state === d.state) {
yearData[i].num_state = d.num_state;
}
}
update(yearData, d.year);
}
function updateScore(yearData) {
//Could get this data directly from dataframe,
//but want to calculate so can be updated easily on drag.
var sums = [0,0,0];
var electoral_sums = [[],0,0];
for (var i=0; i<yearData.length; i++) {
sums[0] += yearData[i].num_dem;
sums[1] += yearData[i].num_rep;
sums[2] += yearData[i].num_state;
//Sum up electoral votes for each unique state
}
for (var i = 0; i<yearData.length; i++) {
var stateName = yearData[i].state;
var currentYear = yearData[i].year;
//console.log('currentYear')
if (currentYear === 2016 && ['ME','NE'].indexOf(stateName) !== -1) { //['ME','NE'].indexOf(stateName) !== -1 (stateName === 'ME' || stateName === 'NE')
//Handle split states
// electoral_sums[1] += 3; // ME + NB
// electoral_sums[2] += 6; // ME + NB
//Will be entered twice, so give 1.5, 3 each entrance instead:
electoral_sums[1] += 1.5; // ME + NB
electoral_sums[2] += 3; // ME + NB
//electoral_sums[0].push(stateName === 'ME' ? 'NE' : 'ME'); //Make sure both states are added
//electoral_sums[0] = electoral_sums[0].concat(['ME','NE']);
} else if (currentYear === 2008 && stateName === 'NE' ) {
electoral_sums[1] += 1;
electoral_sums[2] += 4;
} else if (yearData[i].num_dem > yearData[i].num_rep ) {
//electoral_sums[1] is dems
electoral_sums[1] += yearData[i].state_electoral_votes;
} else {
electoral_sums[2] += yearData[i].state_electoral_votes;
}
}
var dfrac = sums[0]/sums[2],
rfrac = sums[1]/sums[2];
//update dfrac rfrac text, update color background
demtext.text('D ' + pctFormat(dfrac) + ' ' + electoral_sums[1])
reptext.text('R ' + pctFormat(rfrac) + ' ' + electoral_sums[2])
if (electoral_sums[1] > electoral_sums[2]) {
demtext.style('fill', '#bbb');
reptext.style('fill', null);
} else {
demtext.style('fill', null);
reptext.style('fill', '#bbb');
}
//Optional, set background color based on winner
// var backColor = dfrac > rfrac ? colorScale(5) : colorScale(-5);
// //var backColor = colorScale((dfrac-rfrac)*100);
// d3.selectAll('svg')
// .style('background-color', backColor);
}
function update(yearData, year) {
//Change D,R scores:
updateScore(yearData);
//Update title
title.text(year);
var circles = svg.selectAll("circle")
.data(yearData, function(d) { return d.id ; }); // Key for object constancy: https://bost.ocks.org/mike/constancy/
circles.exit().remove();
circles.enter().append("circle")
.on("mouseover", tooltipOn) //Add tooltip before transition
.on("mouseout", function(d){return tooltip.style("visibility", "hidden");})
.call(drag)
.attr("class", "circle")
.merge(circles)
.transition()
.duration(1000) //750; 1000
.attr("cx", function(d) {
return xScale(((d.num_dem-d.num_rep)/d.num_state)*100);
})
.attr("cy", function(d) {
return yScale(d.turnout*100);
})
.attr("r", function(d) {
return getRadioScaled(d);
})
.attr("fill",function(d){
if (d.state.search(searchTerms) != -1) {
return colorScale(((d.num_dem-d.num_rep)/d.num_state)*100);
}else {
return "rgba(192,192,192,0.05)";
}
});
}
function initialize(yearData, year) {
//console.log(yearData);
//console.log(year)
//Change D,R scores:
updateScore(yearData);
//Update title
title.text(year);
var circles = svg.selectAll("circle")
.data(yearData, function(d) { return d.id ; }); // Key for object constancy: https://bost.ocks.org/mike/constancy/
circles.exit().remove(); //.transition().duration(100);
//Create any new circles
circles.enter().append("circle")
.attr("class", "circle")
.attr("cx", function(d) {
return xScale(((d.num_dem-d.num_rep)/d.num_state)*100);
})
.attr("cy", function(d) {
return yScale(d.turnout*100);
})
.attr("r", function(d) {
//return rScale(Math.sqrt(d.num_state/Math.PI));
return getRadioScaled(d);
})
.attr("fill",function(d){
if (d.state.search(searchTerms) != -1) {
return colorScale(((d.num_dem-d.num_rep)/d.num_state)*100);
}else {
return "rgba(192,192,192,0.05)";
}
})
.call(drag)
.on("mouseover", tooltipOn)
.on("mouseout", function(d){return tooltip.style("visibility", "hidden");});
}
//Initialize scatterplot
initialize(yearData, year);
//update(yearData, year); //Works, but transition is weird.
} //);
}
function renderDemographic(group) {
if (cache.demographics) {
build(cache.demographics);
} else {
d3.csv(urls.demographic, parseRows, function(error, data) {
if (error) {throw error};
//Update cache:
cache.demographics = data;
build(data);
});
}
function tooltipOn(d) {
tooltip.style("visibility", "visible")
.html(
"Group: " + capitalize(d.group) + "<br>" +
" D: " + pctFormat(d.num_dem/d.num_group) +
" R: " + pctFormat(d.num_rep/d.num_group) + "<br>" +
"Turnout: " + pctFormat(d.turnout) + "<br>" +
"Voters: " + thsdFormat(Math.round(d.num_group)) + "<br>")
}
function parseRows(d) {
return {'group': d.group, 'id': d.group, 'num_group': +d.electorate_frac*+d.num_nation, 'turnout': +d.turnout,
'num_rep': +d.rep_frac*+d.electorate_frac*+d.num_nation,
'num_dem': +d.dem_frac*+d.electorate_frac*+d.num_nation, 'year': +d.year,
'vap': +d.electorate_frac*+d.num_nation/+d.turnout, 'dem_margin': +d.dem_margin,
'demographic':d.demographic};
}
function build(data){
var nested = d3.nest()
.key(function(d) { return d.demographic; })
.key(function(d) { return +d.year; })
.sortValues(function(a,b) { return b.num_group - a.num_group; } ) //Bring smallest to front
.object(data);
//.entries(data);
//Select current demographic
var dataset = nested[group];
var years = Object.keys(dataset).map(Number);
years.sort()
year = year ? year : years[0];
//Create a copy, so it can be edited on drag:
var yearData = copyObj(dataset[String(year)]);
//Data is just array of all objects from csv, used to find max of all years
var rScale = d3.scaleLinear()
.domain([0, d3.max(data, function(d) {return Math.sqrt(d.num_group/Math.PI); })]) //Area proportional to votes
.range([1, 35]); //2,60 2, 30
//Append increment buttons
var incr = svg.append("text")
.attr("class", "incr")
.attr("dy", height-43) // 1em
.attr("dx", 0) //.5em
.html("&#9650;")
.on("click", function() {
year += 4;
if (year > years[years.length - 1]) {
year = years[0]
}
//Assign to new object, update circles:
yearData = copyObj(dataset[String(year)]);
update(yearData, year);
});
var decr = svg.append("text")
.attr("class", "incr")
.attr("dy", height-10)
.attr("dx", 0) //".5em"
.html("&#9660;")
.on("click", function() {
year -= 4;
if (year < years[0]) {
year = years[years.length - 1];
}
//Assign to new object, update:
yearData = copyObj(dataset[String(year)]);
update(yearData, year);
});
function getRadioScaled(d) {
return rScale(Math.sqrt(d.num_group/Math.PI));
}
function updateAreas(event){
d3.selectAll("circle").transition()
.duration(1000) //750;
.attr("r", function(d) {
return getRadioScaled(d);
});
}
d3.select("#areaform")
.on("click", function () {
return updateAreas();
});
//Dragging behavior
//https://bl.ocks.org/mbostock/6123708
var drag = d3.drag()
.on("drag", dragged)
.on("end", ended);
function dragged(d) {
//Remove transitions temporarily
d3.selectAll("circle").transition();
//Issue when dragged across 0 threshold, county_num = 0
if (d3.event.y >= height) {
return;
}
//Relocate circle with mouse
d3.select(this).attr("cx", d.x = d3.event.x).attr("cy", d.y = d3.event.y);
//Avoid case of no shift
if (d.x === undefined || d.y === undefined) {
return;
}
var newMargin = xScale.invert(d.x)/100,
newTurnout = yScale.invert(d.y)/100, //Math.abs() d.y
oldTurnout = d.turnout,
oldMargin = (d.num_dem-d.num_rep)/d.num_group,
marginChange = newMargin-oldMargin,
oldDfrac = d.num_dem/d.num_group,
oldRfrac = d.num_rep/d.num_group,
oldNumDem = d.num_dem,
oldNumRep = d.num_rep;
//Recalculate fractions based on margin change
//Half goes to each side, zero sum
dfrac = oldDfrac + marginChange/2;
rfrac = oldRfrac - marginChange/2;
//Add increses in turnout to num_group
d.num_group = newTurnout*d.vap;
// Recalculate based on margin change first, assumes
// margin changes are zero sum between parties.
d.num_dem = dfrac*d.num_group;
d.num_rep = rfrac*d.num_group;
d.turnout = newTurnout;
//Call the tooltip function each time to update.
tooltipOn(d);
//Update score as well:
updateScore(yearData);
}
function ended(d) {
update(yearData, d.year);
}
function updateScore(yearData) {
//Could get this data directly from dataframe,
//but want to calculate so can be updated easily on drag.
var sums = [0,0,0];
var electoral_sums = [[],0,0];
for (var i=0; i<yearData.length; i++) {
// if (yearData[i].group != 'Other') {
// console.log(yearData[i].group)
sums[0] += yearData[i].num_dem;
sums[1] += yearData[i].num_rep;
sums[2] += yearData[i].num_group;
//}
}
var dfrac = sums[0]/sums[2],
rfrac = sums[1]/sums[2];
//update dfrac rfrac text, update color background
demtext.text('D ' + pctFormat(dfrac))
reptext.text('R ' + pctFormat(rfrac))
}
function update(yearData, year) {
//Change D,R scores:
updateScore(yearData);
//Update title
title.text(year);
var circles = svg.selectAll("circle")
.data(yearData, function(d) { return d.id ; }); // Key for object constancy: https://bost.ocks.org/mike/constancy/
circles.exit().remove();
circles.enter().append("circle")
.on("mouseover", tooltipOn) //Add tooltip before transition
.on("mouseout", function(d){return tooltip.style("visibility", "hidden");})
.call(drag)
.attr("class", "circle")
.merge(circles)
.transition()
.duration(1000) //750; 1000
.attr("cx", function(d) {
return xScale(((d.num_dem-d.num_rep)/d.num_group)*100);
})
.attr("cy", function(d) {
return yScale(d.turnout*100);
})
.attr("r", function(d) {
return getRadioScaled(d);
})
.attr("fill",function(d){
if (d.group.search(searchTerms) != -1) {
return colorScale(((d.num_dem-d.num_rep)/d.num_group)*100);
}else {
return "rgba(192,192,192,0.05)";
}
});
}
function initialize(yearData, year) {
//Change D,R scores:
updateScore(yearData);
//Update title
title.text(year);
var circles = svg.selectAll("circle")
.data(yearData, function(d) { return d.id ; }); // Key for object constancy: https://bost.ocks.org/mike/constancy/
circles.exit().remove(); //.transition().duration(100);
//Create any new circles
//var circles = svg.selectAll("circle")
// .data(yearData, function(d) { return d.state; })
circles.enter().append("circle")
.attr("class", "circle")
.attr("cx", function(d) {
return xScale(((d.num_dem-d.num_rep)/d.num_group)*100);
})
.attr("cy", function(d) {
return yScale(d.turnout*100);
})
.attr("r", function(d) {
//return rScale(Math.sqrt(d.num_state/Math.PI));
return getRadioScaled(d);
})
// .attr("fill",function(d){
// return colorScale(((d.num_dem-d.num_rep)/d.num_state)*100);
// })
.attr("fill",function(d){
if (d.group.search(searchTerms) != -1) {
return colorScale(((d.num_dem-d.num_rep)/d.num_group)*100);
}else {
return "rgba(192,192,192,0.05)";
}
})
.call(drag)
.on("mouseover", tooltipOn)
.on("mouseout", function(d){return tooltip.style("visibility", "hidden");});
}
//Initialize scatterplot
initialize(yearData, year);
//update(yearData, year); //Works, but transition is weird.
} //);
}
renderCounties();
//renderStates();
//renderDemographic();
</script>
</body>
</html>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment