Skip to content

Instantly share code, notes, and snippets.

@rpgove
Created November 5, 2012 09:08
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 2 You must be signed in to fork a gist
  • Save rpgove/4016178 to your computer and use it in GitHub Desktop.
Save rpgove/4016178 to your computer and use it in GitHub Desktop.
US Electoral Map Sized by Electoral Votes

This visualization shows the USA electoral map, with states sized by the number of electoral votes for each state.

Click a state to see its details, and drag the slider to update the visualization with different poll data. The data displayed shows the most recent polls conducted for each state before the date selected on the slider, or the Election 2008 Presidential Election outcome if no polls occurred in that state before the selected date. The chart on the right shows the electoral vote tally for the selected polls. A toss up occurs if the Democrat and Republican candidate are within 2 points (e.g. 47% to 49%).

I created this visualization because I thought most electoral map visualizations don't tell the whole story. Most maps try to size states according to their geographic area, but visually this makes Wyoming look like it has much more weight in the election than D.C., even though they both have the same number of electoral votes. The New York Times created a great interactive electoral map that solves that problem, but it also makes all the states square shaped. I thought it would be fun to take an alternate approach and retain the states' shapes in order to support recognition over recall.

Note that this visualization makes no attempt to show how Maine or Nebraska's votes might be separated. The NY Times visualization does show this, however.

Colors by ColorBrew.

Poll data from Electoral-Vote.com.

<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html;charset=utf-8"/>
<title>US Electoral Map</title>
<link rel="stylesheet" href="style.css"/>
<link rel="stylesheet" href="http://code.jquery.com/mobile/1.2.0/jquery.mobile-1.2.0.min.css" />
<script src="http://d3js.org/d3.v2.min.js?2.10.1"></script>
<script src="http://code.jquery.com/jquery-1.8.2.min.js"></script>
<script src="http://code.jquery.com/mobile/1.2.0/jquery.mobile-1.2.0.min.js"></script>
<script src="main.js"></script>
</head>
<body>
<div id="body">
<div style="display: inline-block; width: 260px;">
<fieldset data-role="controlgroup" data-type="horizontal" data-mini="true">
<input type="radio" name="radio-mini" id="election-2008-radio" value="election2008" />
<label for="election-2008-radio">2008 Results</label>
<input type="radio" name="radio-mini" id="election-2012-radio" value="election2012" checked="checked" />
<label for="election-2012-radio">2012 Polls</label>
</fieldset>
</div>
<span id="date-slider-value-label" class="label">Polls on or before: </span><span id="date-slider-value"></span>
<div id="#date-slider-container" data-role="fieldcontain">
<span id="date-slider-min-label"></span>
<input type="range" name="date-slider" id="date-slider" value="25" min="0" max="100" style="display:none;"/>
<span id="date-slider-max-label"></span>
</div>
<div id="main-container">
<div id="cartogram"> </div>
</div>
<div id="side-container">
<div id="vote-chart"> </div>
<div id="state-details">
<h3>Click a state</h3>
<p><span class='label'># of votes: </span></p>
<p><span class='label'># of polls: </span></p>
<p><span class='label'>Displaying poll: </span></p>
<p><span class='label'>Date conducted: </span></p>
<p><span class='label'>% Dem: </span></p>
<p><span class='label'>% GOP: </span></p>
</div>
</div>
</div>
</body>
</html>
$(document).bind("pageinit", init);
var chartMargin = {top: 20, right: 20, bottom: 30, left: 80},
chartWidth = 300 - chartMargin.left - chartMargin.right,
chartHeight = 200 - chartMargin.top - chartMargin.bottom;
var chartColor = d3.scale.ordinal()
.domain(["Dem","Toss up","GOP"])
.range(["#2166AC","#B7B7B7","#B2182B"]);
var x = d3.scale.ordinal()
.domain(["Dem","GOP","Toss up"])
.rangeRoundBands([0, chartWidth], .1)
var y = d3.scale.linear()
.domain([0, 538])
.range([chartHeight, 0]);
var xAxis = d3.svg.axis()
.scale(x)
.orient("bottom");
var yAxis = d3.svg.axis()
.scale(y)
.orient("left")
.tickValues([270, 538]);
var stateColor = d3.scale.linear()
.clamp(true)
.domain([-7,0,7])
.range(["#2166AC","#B7B7B7","#B2182B"]);
var dateFormatter = d3.time.format("%b %d, %Y");
var selectedState = null;
var transDur = 200;
var electionDataDate = 1199163600000, // Jan 01, 2008 (2008 election data)
firstDate = electionDataDate,
lastDate = 0;
var totalVotes = [
{"party": "Dem", "votes": 0},
{"party": "GOP", "votes": 0},
{"party": "Toss up", "votes": 0}
];
function init() {
var w = 650, h = 400;
var path = d3.geo.path();
var cartogramSvg = d3.select("#cartogram").append("svg:svg").attr("width", w).attr("height", h);
d3.json("us-states.json", function(states) {
var nodes = [], links = [];
var temp;
var maxVotes = 55; //CA
var baseArea = 5000; //base pixel area for rescaling states
var i = 0;
// compute new area for each state
states.features.forEach(function(d, j) {
d.properties.origArea = path.area(d.geometry);
d.properties.newArea = baseArea * d.properties.votes / maxVotes;
// put polls in chronological order
d.properties.polls = d.properties.polls.reverse();
// indicate the initial selection is the most recent poll
d.properties.selectedPoll = d.properties.polls.length -1;
for (i = 0; i < d.properties.polls.length; ++i) {
// the first "poll" is the 2008 election with Obama v. McCain
// all other polls were in 2012
var pollDate = new Date(d.properties.polls[i].date + (i == 0 ? ",2008" : ",2012"));
d.properties.polls[i].date = pollDate;
if (i > 0 && (pollDate.getTime() < firstDate || firstDate == electionDataDate))
firstDate = pollDate.getTime();
if (pollDate.getTime() > lastDate)
lastDate = pollDate.getTime();
}
});
// Election 2008/2012 toggle
$("input[type='radio']").bind( "change", function(event, ui) {
if (event.target.value == "election2008") {
$("#date-slider").slider("disable");
updatePollSelection(electionDataDate);
updateStateColors();
stateClick(selectedState);
updateChart();
} else {
$("#date-slider").slider("enable");
updatePollSelection($("#date-slider").attr("value"));
updateStateColors();
stateClick(selectedState);
updateChart();
}
});
// ensure the 2012 radio button is checked and the slider is enabled
$("input#election-2008-radio").attr("checked",false).checkboxradio("refresh");
$("input#election-2012-radio").attr("checked",true).checkboxradio("refresh");
$("#date-slider").slider("enable");
// update the slider with the min and max dates
$("#date-slider")
.attr("min", firstDate)
.attr("value", lastDate)
.attr("max", lastDate)
.slider("refresh");
$("#date-slider").change(function(event) {
updatePollSelection(+event.target.value);
updateStateColors();
stateClick(selectedState);
updateChart();
});
d3.select("#date-slider-min-label").text(dateFormatter(new Date(firstDate)));
d3.select("#date-slider-max-label").text(dateFormatter(new Date(lastDate)));
d3.select("#date-slider-value-label").text(dateFormatter(new Date(lastDate)));
states.features.forEach(function(d, i) {
var centroid = path.centroid(d);
centroid.x = centroid[0];
centroid.y = centroid[1];
centroid.feature = d;
nodes.push(centroid);
});
var node = cartogramSvg.selectAll("g")
.data(nodes)
.enter()
.append("svg:g")
.append("svg:path")
.attr("class", "state")
.attr("transform", function(d) {
return d.feature.transform;
})
.attr("d", function(d) { return path(d.feature); })
.attr("title", function(d) { return d.feature.properties.name + ": " + d.feature.properties.votes + " votes"; })
.style("stroke-width", function(d) { return 1 / Math.sqrt(d.feature.properties.newArea/d.feature.properties.origArea); })
.style("fill", function(d) {
var selectedPoll = d.feature.properties.polls[d.feature.properties.selectedPoll];
var diff = selectedPoll.gop - selectedPoll.dem;
return stateColor(diff);
})
.on("click", function(d) {
d3.selectAll("path.state").classed("selected", false);
d3.select(this).classed("selected", true);
stateClick(d);
});
// find the poll for each state and calculate votes
updatePollSelection($("#date-slider").attr("value"));
var chartSvg = d3.select("#vote-chart").append("svg")
.attr("width", chartWidth + chartMargin.left + chartMargin.right)
.attr("height", chartHeight + chartMargin.top + chartMargin.bottom)
.append("g")
.attr("transform", "translate(" + chartMargin.left + "," + chartMargin.top + ")");
chartSvg.append("g")
.attr("class", "title")
.append("text")
.attr("x", 25)
.text("US Electoral Votes");
chartSvg.append("g")
.attr("class", "x axis")
.attr("transform", "translate(0," + chartHeight + ")")
.call(xAxis);
chartSvg.append("g")
.attr("class", "y axis")
.call(yAxis);
chartSvg.selectAll(".bar").data(totalVotes)
.enter().append("rect")
.attr("class", "bar")
.attr("x", function(d) { return x(d.party); })
.attr("width", x.rangeBand())
.attr("y", function(d) { return y(d.votes); })
.attr("height", function(d) { return chartHeight - y(d.votes); })
.attr("title", function(d) { return d.votes + " votes"; })
.style("fill", function(d) { return chartColor(d.party); });
});
}
function updatePollSelection(date) {
var currDate = new Date(+date);
// update slider label
if (date == electionDataDate) {
d3.select("#date-slider-value-label").text("Election results: ");
d3.select("#date-slider-value").text("2008");
} else {
d3.select("#date-slider-value-label").text("Polls on or before: ");
d3.select("#date-slider-value").text(dateFormatter(currDate));
}
totalVotes[0].votes = 0;
totalVotes[1].votes = 0;
totalVotes[2].votes = 0;
d3.selectAll("path.state").each(function(d) {
var i = 0;
var poll;
// find the first date >= currently selected date
// if no poll after selected date, use the most recent poll
// if no poll available, uses Election 2008
for (i = 0; i < d.feature.properties.polls.length; ++i) {
poll = d.feature.properties.polls[i];
if (poll.date > currDate) {
break;
}
d.feature.properties.selectedPoll = i;
}
// recompute the electoral vote totals
if (poll.dem - poll.gop > 2) { // Dem winning
totalVotes[0].votes += d.feature.properties.votes;
} else if (poll.gop - poll.dem > 2) { //GOP winning
totalVotes[1].votes += d.feature.properties.votes;
} else { //toss up
totalVotes[2].votes += d.feature.properties.votes;
}
});
}
function updateStateColors() {
d3.selectAll("path.state").transition()
.duration(transDur)
.style("fill", function(d) {
var poll = d.feature.properties.polls[d.feature.properties.selectedPoll];
return stateColor(poll.gop - poll.dem);
});
}
function updateChart() {
d3.selectAll(".bar").data(totalVotes)
.transition().duration(transDur)
.attr("y", function(d) { return y(d.votes); })
.attr("height", function(d) { return chartHeight - y(d.votes); })
.attr("title", function(d) { return d.votes + " votes"; });
}
function stateClick(d) {
if (d === null) return;
var selectedPoll = d.feature.properties.polls[d.feature.properties.selectedPoll];
var detailsHtml = "<h3>" + d.feature.properties.name + "</h3>";
detailsHtml += "<p><span class='label'># of votes: </span>" + d.feature.properties.votes + "</p>";
detailsHtml += "<p><span class='label'># of polls: </span>" + (d.feature.properties.polls.length -1) + "</p>";
detailsHtml += "<p><span class='label'>" + "Displaying poll: </span>" + selectedPoll.pollster + "</p>";
detailsHtml += "<p><span class='label'>" + "Date conducted: </span>" + dateFormatter(selectedPoll.date) + "</p>";
detailsHtml += "<p><span class='label'>" + "% Dem: </span>" + selectedPoll.dem + "</p>";
detailsHtml += "<p><span class='label'>" + "% GOP: </span>" + selectedPoll.gop + "</p>";
d3.select("#state-details").html(detailsHtml);
selectedState = d;
}
div#body {
width: 960px;
}
path.state {
fill: #aaa;
fill-opacity: .8;
stroke: #fff;
}
path:hover.state,
path.state.selected {
fill-opacity: 1;
stroke: #222;
}
.axis path,
.axis line {
fill: none;
stroke: #bbb;
shape-rendering: crispEdges;
stroke-width: 2px;
}
.x.axis path {
display: none;
}
.bar {
fill-opacity: .8;
}
span.label {
color: #555555;
}
#main-container {
display: inline-block;
width: 650px;
height: 400px;
}
#side-container {
position: relative;
display: inline-block;
width: 300px;
height: 400px;
}
#state-details h3 {
margin: 18px 0 0 0;
}
#state-details p {
margin: 0 0 0 125px;
text-indent: -125px;
}
#state-details p span.label {
display: inline-block;
margin: 0 -125px 0 125px;
width: 125px;
}
#vote-chart {
position: absolute;
top: 0;
left: 0;
width: 300px;
height: 200px;
}
#state-details {
position: absolute;
bottom: 0;
left: 0;
width: 300px;
height: 200px;
}
Display the source blob
Display the rendered blob
Raw
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment