d3.csv("voters.csv", function(err, res) { voters = res .filter(ƒ(0)) .map(function(d){ var rv = [] var i = 0 while (d[i] && d[i] != 'No preference'){ rv.push(d[i++]) } // rv = rv.filter(function(d){ return d == "Donald Trump" || d == "Ben Carson" }) return rv }) candidateNames = ["Donald Trump", "Ben Carson", "Marco Rubio", "Carly Fiorina", "Jeb Bush", "Ted Cruz", "Mike Huckabee", "Rand Paul", "John Kasich","Bobby Jindal", "Chris Christie", "Lindsey Graham"] pairs = {} candidateNames.forEach(function(a){ candidateNames.forEach(function(b){ if (a != b) pairs[a + '-' + b] = 0 }) }) voters.forEach(function(d){ var lowerPref = d.slice() var lowerPref = candidateNames.slice() d.forEach(function(candidateName){ lowerPref = lowerPref.filter(function(lowerName){ return lowerName != candidateName }) lowerPref.forEach(function(lowerName){ pairs[candidateName + '-' + lowerName]++ }) }) }) //vote circle !(function(){ d3.select('body').append('h2').text('Click candidate to show head to head matchups') r = 200, m = 50 candidates = candidateNames.map(function(str, i){ return {name: str, i: i, firstName: str.split(' ')[1], links: []} }) nameToCandidate = d3.nest().key(ƒ('name')).rollup(ƒ(0)).map(candidates) allPairs = d3.entries(pairs) .map(function(d){ return {a: d.key.split('-')[0], b: d.key.split('-')[1], aVal: d.value} }) allPairs.forEach(function(d){ d.bVal = pairs[d.b + '-' + d.a] d.total = d.bVal + d.aVal d.percentA = d.aVal/d.total d.aC = nameToCandidate[d.a] d.bC = nameToCandidate[d.b] d.aC.links.push(d) d.bC.links.push(d) }) links = allPairs.filter(function(d){ return d.aVal >= d.bVal }) var svg = d3.select('body') .append('svg') .attr({width: 2*m + 2*r, height: 2*m + 2*r}) .append('g') .translate([m + r, m + r]) var angleScale = d3.scale.linear() .domain([0, candidates.length]) .range([0, Math.PI*2]) var strokeScale = d3.scale.linear() .domain([0, d3.max(links, ƒ('total'))]) .range([0, 10]) candidates.forEach(setCandidatePos) links.forEach(setTickPos) var lineSel = svg.dataAppend(links, 'path.link') .attr('d', pathStr) var ticks = svg.dataAppend(links, 'circle.tick') .translate(ƒ('tickPos')) .attr('r', 1) overlayG = svg.append('g') var candidateSel = svg.dataAppend(candidates, 'g.candidate') .translate(ƒ('pos')) .call(d3.attachTooltip) .on('click', centerCandidate) .each(function(d){ d3.sel = d3.select(this) }) candidateSel.append('circle') .attr('r', 10) .style('fill', 'lightgrey') candidateSel.append('text') .text(ƒ('firstName')) .attr({'text-anchor': 'middle', dy: '.33em'}) function centerCandidate(d){ candidates.forEach(setCandidatePos) d.pos = [0, 0] links.forEach(setTickPos) lineSel.transition().duration(1000) .attr('d', pathStr) ticks.transition().duration(1000) .translate(ƒ('tickPos')) candidateSel.transition().duration(1000) .translate(ƒ('pos')) overlayG.selectAll('.overlay') .classed('overlay', false) .transition() .style('opacity', 0) .remove() overlayG.dataAppend(d.links, 'path.overlay.r' + Math.round(Math.random()*10000)).filter(ƒ('tickPos')) .attr('d', 'M0,0L0,0') .transition().delay(1000).duration(1000) .attr('d', function(e){ return 'M0,0L' + e.tickPos }) .style('stroke', function(e){ return e.aC == d ? 'red' : 'steelblue' }) .style('stroke-width', ƒ('total', strokeScale)) } centerCandidate(candidates[3]) function pathStr(d){ return 'M' + [d.aC.pos, d.bC.pos].join('L') } function angleToPos(angle){ return [r*Math.cos(angle), r*Math.sin(angle)] } function setCandidatePos(d){ d.pos = angleToPos(angleScale(d.i)) } function setTickPos(d){ d.tickPos = percentMid(d.aC.pos, d.bC.pos, d.percentA) } })() //sm place support !(function(){ candidates.forEach(function(d){ d.votes = d3.range(6).map(function(){ return 0 }) }) voters.forEach(function(d){ d.forEach(function(str, i){ nameToCandidate[str].votes[i]++ }) }) d3.select('body').append('h2').text('Number of people who have the candidate as a first, second, third... choice. Click to remove a candidate from the race and see where their votes go') var candidateDivs = d3.select('body').append('div').dataAppend(candidates, 'div.candidate') var x, y candidateDivs.each(function(d){ var c = d3.conventions({parentSel: d3.select(this), width: 60, height: 100, margin: {top: 0, left: 8, right: 8, bottom:0 }}) x = c.x.domain([0, 5]) y = c.y.domain([0, 1500]) // c.drawAxis() c.svg.dataAppend(d.votes, 'path.bg') .translate([3, 0]) .attr('d', function(d, i){ return ['M', x(i), y(0), 'L', x(i), y(0)].join(' ') }) .style('stroke', 'steelblue') .style('stroke-width', 5) c.svg.dataAppend(d.votes, 'path.fg') .attr('d', function(d, i){ return ['M', x(i), y(0), 'L', x(i), y(d)].join(' ') }) .style('stroke', 'black') .style('stroke-width', 5) c.svg.append('path').attr('d', ['M0,', c.height - 1, 'h', c.width].join(' ')).style('stroke', 'black').style('stroke-width', 2) }) candidateDivs.append('div').text(ƒ('firstName')) candidateDivs.on('click', function(d){ d.isOff = !d.isOff candidates.forEach(function(d){ d.rVotes = d3.range(6).map(function(){ return 0 }) }) var offCandidates = candidates.filter(ƒ('isOff')).map(ƒ('name')) candidateDivs.style('opacity', function(d){ return d.isOff ? .4 : 1 }) voters.forEach(function(d){ var skips = 0 d.forEach(function(str, i){ if (_.contains(offCandidates, str)) return skips++ nameToCandidate[str].rVotes[i - skips]++ }) }) candidateDivs.selectAll('.bg').data(ƒ('rVotes')) .transition() .attr('d', function(d, i){ return ['M', x(i), y(0), 'L', x(i), y(d)].join(' ') }) }) })() d3.select('body').append('h2').text('Head to head grid. Circles sized by number of people who listed candidate A over candidate B and colored by the percent of people that prefered A to B') var c = d3.conventions({height: 500, width: 500, margin: {top: 40, left: 100, right: 20, bottom: 20}}) c.x.domain([0, candidates.length]) var rScale = d3.scale.sqrt().domain([0, d3.max(allPairs, ƒ('aVal'))]).range([0, 30]) var colorScale = d3.scale.linear() .domain([.1, .5 -.000001, .5 + .000001, .9]) .range(['#007FFF', '#D4E2F0', '#E2B0B0', '#FA0000']) c.svg.dataAppend(_.sortBy(allPairs, ƒ('aVal')), 'circle.pair') .attr('r', ƒ('aVal', rScale)) .attr('cx', ƒ('aC', 'i', c.x)) .attr('cy', ƒ('bC', 'i', c.x)) .style('fill', ƒ('percentA', colorScale)) .call(d3.attachTooltip) c.svg.dataAppend(candidates, 'text.x') .attr('x', ƒ('i', c.x)) .text(ƒ('firstName')) .attr({'text-anchor': 'middle', dy: '-2.55em'}) c.svg.dataAppend(candidates, 'text.y') .attr('y', ƒ('i', c.x)) .text(ƒ('firstName')) .attr({'text-anchor': 'middle', dx: '-5.3em', dy: '.33em'}) d3.select('body').append('h2').text('Other ideas:') d3.select('body').append('h3').text('Which candidates are second place picks for other candidates?') d3.select('body').append('h3').text('Does voters breakdown into establishment v. anti?') d3.select('body').append('h3').text('Directed force graph of preference') d3.select(self.frameElement).style("height", d3.select('body').node().getBoundingClientRect().height + 50 + "px"); }) function dist(a, b){ return Math.sqrt( Math.pow(a[0] - b[0], 2) + Math.pow(a[1] - b[1], 2)) } function percentMid(a, b, p){ return [a[0] + (b[0] - a[0])*p, a[1] + (b[1] - a[1])*p] }