Using the layout and scale functions of my d3 icon array plugin to render the senate election battleground.
Based on a chart I made for the FT wedsite
Using the layout and scale functions of my d3 icon array plugin to render the senate election battleground.
Based on a chart I made for the FT wedsite
(function (global, factory) { | |
typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports, require('d3-scale')) : | |
typeof define === 'function' && define.amd ? define(['exports', 'd3-scale'], factory) : | |
(factory((global.d3_iconarray = global.d3_iconarray || {}),global.d3)); | |
}(this, function (exports,d3) { 'use strict'; | |
function iconArrayLayout() { | |
var width = undefined; | |
var height = undefined; | |
var widthFirst = true; | |
var maxDimension = undefined; | |
function layout(data){ | |
//work our missing height, width stuff | |
setDimensions(data.length); | |
return data.map(function(d,i){ | |
return { | |
data:d, | |
position:position(i) | |
}; | |
}); | |
} | |
function position(i){ | |
if(isNaN(width) || isNaN(height)){ | |
console.log('Warning: width/height undefined') | |
return 0; | |
} | |
if(widthFirst){ | |
return { | |
x: i % width, | |
y: Math.floor( i/width ) | |
}; | |
}else{ | |
return { | |
x: Math.floor( i/height ), | |
y: i % height | |
}; | |
} | |
} | |
function setDimensions(l){ | |
//neither width or height is defined | |
if(isNaN(width) && isNaN(height)){ | |
console.log('no width or height'); | |
if(widthFirst){ | |
width = Math.ceil( Math.sqrt(l) ); | |
height = Math.ceil( l / width ); | |
}else{ | |
height = Math.ceil( Math.sqrt(l) ); | |
width = Math.ceil( l / height ); | |
} | |
}else if(isNaN(width)){ //width undefined | |
width = Math.ceil( l / height ); | |
}else if(isNaN(height)){ //height undefined | |
height = Math.ceil( l / width ); | |
} | |
} | |
layout.maxDimension = function(x){ | |
var itemPosition = position(x); | |
if(widthFirst){ | |
var x = Math.max(itemPosition.x, width); | |
return Math.max(x, itemPosition.y); | |
} | |
var y = Math.max(itemPosition.y, height); | |
return Math.max(y, itemPosition.x); | |
} | |
layout.position = function(x){ | |
return position(x); | |
} | |
layout.width = function(x){ | |
if(x === undefined) return width; | |
width = x; | |
return layout; | |
}; | |
layout.height = function(x){ | |
if(x === undefined) return height; | |
height = x; | |
return layout; | |
}; | |
layout.widthFirst = function(b){ | |
if(b === undefined) return widthFirst; | |
widthFirst = b; | |
return layout; | |
}; | |
return layout; | |
}; | |
function iconArrayScale(){ | |
var domain = [0,100]; | |
var range = [0,100]; | |
var gapInterval = 10; | |
var gapSize = 0; //default no change | |
var notionalScale = d3.scaleLinear() | |
.domain(domain) | |
.range(range); | |
function scale(domainValue){ | |
var rangeValue = 20; | |
var adjustedDomainValue = domainValue + Math.floor(domainValue/gapInterval)*gapSize; | |
//console.log(notionalScale.domain()); | |
return rangeValue = notionalScale(adjustedDomainValue); | |
} | |
function rescale(){ | |
//calculate an adjusted domain | |
var domainLength = (domain[1] - domain[0]) * gapSize; | |
var gaps = Math.ceil( domainLength/ gapInterval ); | |
var adjustedDomain = [ domain[0], domain[1] + gaps ]; | |
//calculate an adjusted range | |
notionalScale.domain(adjustedDomain) | |
.range(range); | |
} | |
scale.gapInterval = function(x){ | |
if(!x) return gapInterval; | |
gapInterval = x; | |
rescale(); | |
return scale; | |
}; | |
scale.gapSize = function(x){ | |
if(isNaN(x)) return gapSize; | |
gapSize = x; | |
rescale(); | |
return scale; | |
} | |
scale.domain = function(array){ | |
if(!array) return domain; | |
domain = array; | |
rescale(); | |
return scale; | |
}; | |
scale.range = function(array){ | |
if(!array) return range; | |
range = array; | |
rescale(); | |
return scale; | |
}; | |
rescale(); | |
return scale; | |
} | |
var version = "0.0.1"; | |
exports.version = version; | |
exports.layout = iconArrayLayout; | |
exports.scale = iconArrayScale; | |
})); |
state | result.winner | result.caucus | result.r | result.d | result.i | result.reporting | result.time | seatid | encumbentparty | Cook Political Rating | election | |
---|---|---|---|---|---|---|---|---|---|---|---|---|
AK | AK1 | D | Tossup | yes | ||||||||
AL | R | R | 100 | 0 | 8:00 EST | AL1 | R | Solid Republican | yes | |||
AR | R | R | 56.5 | 40 | 73 | 1:18 EST | AR1 | D | Tossup | yes | ||
CO | R | R | 50.3 | 44.3 | 87 | 1:41 EST | CO1 | D | Tossup | yes | ||
DE | D | D | 42.2 | 55.8 | 100 | 00:48 EST | DE1 | D | Solid Democratic | yes | ||
GA | R | R | 53 | 45.1 | 99.6 | 1:42 EST | GA1 | R | Tossup | yes | ||
HI | D | D | 25.2 | 72.5 | 7.7 | 1:42 EST | HI1 | D | Solid Democratic | yes | ||
IA | R | R | 52.1 | 43.8 | 98.6 | 1:46 EST | IA1 | D | Tossup | yes | ||
ID | R | R | 66.39 | 33.61 | 55 | 1:15 EST | ID1 | R | Solid Republican | yes | ||
IL | D | D | 43.3 | 52.92 | 97.92 | 00:51 EST | IL1 | D | Solid Democratic | yes | ||
KS | R | R | 53.07 | 42.72 | 69.4 | 00:41 EST | KS1 | R | Tossup | yes | ||
KY | R | R | 56.2 | 40.7 | 100 | 2:07 EST | KY1 | R | Lean Republican | yes | ||
LA | 42.1 | 41 | 100 | 1:50 EST | LA1 | D | Tossup | yes | ||||
MA | D | D | 37.8 | 62.2 | 97.24 | 00:51 EST | MA1 | D | Solid Democratic | yes | ||
ME | R | R | 68.1 | 32 | 63.9 | 1:52 EST | ME1 | R | Solid Republican | yes | ||
MI | D | D | 41.3 | 54.7 | 93.9 | 1:55 EST | MI1 | D | Lean Democratic | yes | ||
MN | D | D | 42.8 | 53.3 | 92.2 | 1:55 EST | MN1 | D | Likely Democratic | yes | ||
MS | R | R | 59.2 | 38.6 | 96.5 | 00:49 EST | MS1 | R | Likely Republican | yes | ||
MT | R | R | 57.48 | 40.46 | 59 | 1:15 EST | MT1 | D | Solid Republican | yes | ||
NC | R | R | 49.1 | 47.2 | 3.7 | 96.6 | 11:25 EST | NC1 | D | Tossup | yes | |
NE | R | R | 64.9 | 30.91 | 66 | 1:15 EST | NE1 | R | Solid Republican | yes | ||
NH | D | D | 48.4 | 51.6 | 87.4 | 00:53 EST | NH1 | D | Tossup | yes | ||
NJ | D | D | 42.06 | 56.11 | 95.53 | 00:43 EST | NJ1 | D | Solid Democratic | yes | ||
NM | D | D | 44.97 | 55.03 | 65 | 00:44 EST | NM1 | D | Solid Democratic | yes | ||
OK | R | R | 68 | 28.5 | 100 | 1:56 EST | OK1 | R | Solid Republican | yes | ||
OK | R | R | 67.9 | 29 | 100 | 1:56 EST | OK2 | R | Solid Republican | yes | ||
OR | D | D | 38.2 | 55.14 | 65.1 | 00:46 EST | OR1 | D | Likely Democratic | yes | ||
RI | D | D | 29.5 | 70.5 | 99.8 | 1:58 EST | RI1 | D | Solid Democratic | yes | ||
SC | R | R | 54.5 | 38.9 | 99 | 1:59 EST | SC1 | R | Solid Republican | yes | ||
SC | R | R | 61.2 | 37.1 | 99 | 2:00 EST | SC2 | D | Solid Republican | yes | ||
SD | R | R | 51.6 | 28.4 | 16.8 | 87.5 | 2:00 EST | SD1 | D | Lean Republican | yes | |
TN | R | R | 61.9 | 31.9 | 99 | 2:03 EST | TN1 | R | Solid Republican | yes | ||
TX | R | R | 61.6 | 34.4 | 97 | 2:03 EST | TX1 | R | Solid Republican | yes | ||
VA | VA1 | D | Likely Democratic | yes | ||||||||
WV | R | R | 62 | 34.6 | 98.6 | 2:06 EST | WV1 | D | Likely Republican | yes | ||
WY | R | R | 72.3 | 17.6 | 100 | 2:06 EST | WY1 | R | Solid Republican | yes | ||
AK | AK2 | R | ||||||||||
AL | AL2 | R | ||||||||||
AR | AR2 | R | ||||||||||
AZ | AZ1 | R | ||||||||||
AZ | AZ2 | R | ||||||||||
CA | CA1 | D | ||||||||||
CA | CA2 | D | ||||||||||
CO | CO2 | D | ||||||||||
CT | CT1 | D | ||||||||||
CT | CT2 | D | ||||||||||
DE | DE2 | D | ||||||||||
FL | FL1 | R | ||||||||||
FL | FL2 | D | ||||||||||
GA | GA2 | R | ||||||||||
HI | HI2 | D | ||||||||||
IA | IA2 | R | ||||||||||
ID | ID2 | R | ||||||||||
IL | IL2 | R | ||||||||||
IN | IN1 | R | ||||||||||
IN | IN2 | D | ||||||||||
KS | KS2 | R | ||||||||||
KY | KY2 | R | ||||||||||
LA | LA2 | R | ||||||||||
MA | MA2 | D | ||||||||||
MD | MD1 | D | ||||||||||
MD | MD2 | D | ||||||||||
ME | ME2 | I | ||||||||||
MI | MI2 | D | ||||||||||
MN | MN2 | D | ||||||||||
MO | MO1 | D | ||||||||||
MO | MO2 | R | ||||||||||
MS | MS2 | R | ||||||||||
MT | MT2 | D | ||||||||||
NC | NC2 | R | ||||||||||
ND | ND1 | R | ||||||||||
ND | ND2 | D | ||||||||||
NE | NE2 | R | ||||||||||
NH | NH2 | R | ||||||||||
NJ | NJ2 | D | ||||||||||
NM | NM2 | D | ||||||||||
NV | NV1 | D | ||||||||||
NV | NV2 | R | ||||||||||
NY | NY1 | D | ||||||||||
NY | NY2 | D | ||||||||||
OH | OH1 | R | ||||||||||
OH | OH2 | D | ||||||||||
OR | OR2 | D | ||||||||||
PA | PA1 | D | ||||||||||
PA | PA2 | R | ||||||||||
RI | RI2 | D | ||||||||||
SD | SD2 | R | ||||||||||
TN | TN2 | R | ||||||||||
TX | TX2 | R | ||||||||||
UT | UT1 | R | ||||||||||
UT | UT2 | R | ||||||||||
VA | VA2 | D | ||||||||||
VT | VT1 | D | ||||||||||
VT | VT2 | I | ||||||||||
WA | WA1 | D | ||||||||||
WA | WA2 | D | ||||||||||
WI | WI1 | D | ||||||||||
WI | WI2 | R | ||||||||||
WV | WV2 | D | ||||||||||
WY | WY2 | R |
<!DOCTYPE html> | |
<html> | |
<head> | |
<title>Simple icon array example</title> | |
<script src="//d3js.org/d3.v4.0.0-alpha.18.min.js" charset="utf-8"></script> | |
<script type="text/javascript" src="d3-iconarray.js"></script> | |
<style type="text/css"> | |
*{ | |
font-family: sans-serif; | |
letter-spacing: 1px; | |
} | |
.R, .I{ | |
fill:#F00; | |
} | |
.Solid-Republican{ | |
stroke:#F00; | |
stroke-width:2; | |
fill: none; | |
} | |
.Likely-Republican{ | |
stroke:#F00; | |
stroke-width:2; | |
stroke-dasharray:4 2; | |
fill: none; | |
} | |
.Lean-Republican{ | |
stroke:#F00; | |
stroke-width:2; | |
stroke-dasharray:2 4; | |
fill: none; | |
} | |
.Tossup{ | |
stroke:#CCC; | |
stroke-width:2; | |
fill: none; | |
} | |
.Lean-Democratic{ | |
stroke:#00F; | |
stroke-width:2; | |
stroke-dasharray:2 4; | |
fill: none; | |
} | |
.Likely-Democratic{ | |
stroke:#00F; | |
stroke-width:2; | |
stroke-dasharray:4 2; | |
fill: none; | |
} | |
.Solid-Democratic{ | |
stroke:#00F; | |
stroke-width:2; | |
fill: none; | |
} | |
.D{ | |
fill:#00F; | |
} | |
.key-element text{ | |
font-size: 12px | |
} | |
</style> | |
</head> | |
<body> | |
<h1>2014 US senate election</h1> | |
<p>This graphic shows the 100 seats of the US senate in the run up to the 2014 election. It indicates which were up for election and what their result was expected to be according to <a href="http://cookpolitical.com/senate/charts/race-ratings/8108">The Cook Political Report</a></p> | |
<div id="senate-example"> | |
</div> | |
</body> | |
<script type="text/javascript"> | |
var order = [ | |
'R', | |
'Solid Republican', | |
'Likely Republican', | |
'Lean Republican', | |
'Tossup', | |
'Lean Democratic', | |
'Likely Democratic', | |
'Solid Democratic', | |
'D' ] | |
var width = 600, height = 400, | |
margin = {top:20,left:20,bottom:20,right:20}; | |
var plotWidth = width - (margin.left + margin.right); | |
var svg = d3.select('#senate-example') | |
.append('svg') | |
.attr('width',width) | |
.attr('height', height) | |
.append('g') | |
.attr('transform','translate('+margin.left+','+margin.top+')') | |
d3.csv('data-senate-2014.csv', function(data){ | |
var sorted = data.map(function(d){ | |
if (d.election == "" || d.election == undefined){ | |
d['Cook Political Rating'] = d.encumbentparty; | |
} | |
return d; | |
}) | |
.sort(function(a,b){ | |
return order.indexOf(a['Cook Political Rating']) - order.indexOf(b['Cook Political Rating']); | |
}); | |
var layout = d3_iconarray.layout() | |
.height(5) | |
.widthFirst(false); | |
var grid = layout(sorted); | |
var scale = d3_iconarray.scale() | |
.domain([0, layout.maxDimension(sorted.length)]) | |
.range([0, plotWidth]) | |
.gapInterval(10) | |
.gapSize(1.5); | |
svg.selectAll('circle') | |
.data(grid, function(d){ return d.data.seatid; }) | |
.enter() | |
.append('circle') | |
.attr('r',function(d){ | |
if(d.data.election){ | |
return 5 | |
} | |
return 2 | |
}) | |
.attr('cx',function(d){ | |
return scale(d.position.x); | |
}) | |
.attr('cy',function(d){ | |
return scale(d.position.y); | |
}) | |
.attr('class',function(d){ | |
return d.data["Cook Political Rating"].replace(/\s/g,'-') | |
}); | |
//key | |
var columns = [ | |
['R','Solid Republican','Likely Republican','Lean Republican'], | |
['Tossup'], | |
['D','Solid Democratic','Likely Democratic','Lean Democratic'] | |
]; | |
d3.select('svg').selectAll('g.key-column') | |
.data(columns).enter() | |
.append('g') | |
.attr('class','key-column') | |
.attr('transform',function(d,i){ | |
return 'translate('+(margin.left + (i*plotWidth/columns.length))+','+ (margin.top*4 + scale(layout.height())) +')'; | |
}) | |
.each(function(columnData){ | |
d3.select(this) | |
.selectAll('g.key-element') | |
.data(columnData) | |
.enter() | |
.append('g') | |
.attr('class','key-element') | |
.attr('transform',function(d,i){ | |
return 'translate(0,'+scale(i)+')'; | |
}) | |
.call(function(parent){ | |
parent.append('circle') | |
.attr('cy',-5) | |
.attr('r',function(d){ | |
if(d==='D'||d==='R'){ | |
return 2 | |
} | |
return 5 | |
}) | |
.attr('class',function(d){ | |
return d.replace(/\s/g,'-') | |
}); | |
parent.append('text') | |
.attr('dx', margin.left) | |
.text(function(d){ | |
if(d==='R') return 'Rep (no election)' | |
if(d==='D') return 'Dem (no election)' | |
if(d==='Tossup') return 'Tossup ¯\\_(ツ)_/¯' //added the shrugging guy to make the key look abalanced ¯\_(ツ)_/¯ | |
return d; | |
}) | |
}); | |
}); | |
}); | |
</script> | |
</html> |