From b63f8a2868e1943d9152a068c527584e6e058a20 Mon Sep 17 00:00:00 2001 From: Stuart Lowe Date: Wed, 11 Dec 2019 17:45:41 +0000 Subject: [PATCH] Update NS to latest --- ge2019/ns.html | 399 ++++++++++++++++++++++++++++++++----------------- 1 file changed, 265 insertions(+), 134 deletions(-) diff --git a/ge2019/ns.html b/ge2019/ns.html index a54035e7..b7fe0d4a 100644 --- a/ge2019/ns.html +++ b/ge2019/ns.html @@ -9,11 +9,8 @@ - - - - - + + + @@ -228,12 +219,12 @@
- +
- Visualisation: ODI Leeds / Data: New Statesman, Electoral Commission, Houses of Parliament, Houses of Parliament, They Work For You (CC-BY-SA), Democracy Club (CC-BY 4.0) + Visualisation: ODI Leeds / Data: New Statesman, Britain Elects, Electoral Commission, Parliament (2015 & 2017), They Work For You (CC-BY-SA), Democracy Club (CC-BY 4.0)
@@ -241,22 +232,24 @@ var constituencies; var parties = { - 'Con':{'color':'#0485A8','title':'Conservative'}, - 'Lab':{'color':'#DC4343','title':'Labour'}, - 'LD':{'color':'#EAA544','title':'Lib Dem','short':'LibDem'}, - 'Green':{'color':'#5EBD4C'}, - 'Brexit':{'color':'#0FA697'}, - 'SNP':{'color':'#EBDB1C'}, - 'UKIP':{'color':'#73537A'}, - 'PC':{'color':'#4FBA7C','title':'Plaid Cymru'}, - 'Ind':{'color':'#CCA7C6','title':'Independent'}, - 'DUP':{'color':'#BF3759'}, - 'SF':{'color':'#2C604D','title':'Sinn Féin'}, - 'SDLP':{'color':'#53BC5B'}, - 'Alliance':{'color':'#EAA544','short':'Aln'}, - 'Spk':{'color':'#909090','title':'Speaker'}, - 'XSpk':{'color':'#909090','title':'Ex-Speaker'}, - 'none':{'color':'#dfdfdf'} + 'Con':{'color':'#0485A8','title':'Conservative','short':'Con'}, + 'Lab':{'color':'#DC4343','title':'Labour','short':'Lab'}, + 'LD':{'color':'#EAA544','title':'Lib Dem','short':'LDem'}, + 'Green':{'color':'#5EBD4C','short':'Grn'}, + 'Brexit':{'color':'#0FA697','short':'Brex'}, + 'SNP':{'color':'#EBDB1C','short':'SNP'}, + 'UKIP':{'color':'#73537A','short':'UKIP'}, + 'PC':{'color':'#4FBA7C','title':'Plaid Cymru','short':'PC'}, + 'Ind':{'color':'#CCA7C6','title':'Independent','short':'Ind'}, + 'DUP':{'color':'#BF3759','short':'DUP'}, + 'UUP':{'color':'#3b75a8','short':'UUP'}, + 'SF':{'color':'#2C604D','title':'Sinn Féin','short':'SF'}, + 'SDLP':{'color':'#53BC5B','short':'SDLP'}, + 'Alliance':{'color':'#EAA544','short':'Alln'}, + 'Spk':{'color':'#909090','title':'Speaker','short':'Spk'}, + 'XSpk':{'color':'#909090','title':'Ex-Speaker','short':'Ex-Spk'}, + 'Monster':{'color':'#cccccc','short':'Mon'}, + 'none':{'color':'#dfdfdf','short':'Oth'} }; function getPartyDetails(party){ @@ -275,10 +268,11 @@ } - var render = function(title,region,data){ + var render = function(title,region,data,attr){ var r; var p = ""; var lbl = ""; + var img = ""; var wincolour = ""; if(!data) data = {}; @@ -293,13 +287,21 @@ elections.sort().reverse(); incumbent = ""; + lastge = ""; changedIncumbent = false; if(data.elections['2019-12-12'] && data.elections['2019-12-12'].incumbent){ incumbent = data.elections['2019-12-12'].incumbent; - changedIncumbent = true; - }else if(data.elections[elections[1]] && data.elections[elections[1]].party){ - incumbent = data.elections[elections[1]]; + //}else if(data.elections[elections[1]] && data.elections[elections[1]].party){ + // incumbent = data.elections[elections[1]]; + lastge = data.elections['2019-12-12'].incumbent; + } + for(var i = 1; i < elections.length; i++){ + if(data.elections[elections[i]].type=="general"){ + lastge = data.elections[elections[1]]; + break; + } } + if(data.elections['2019-12-12'].party && lastge.party.code != data.elections['2019-12-12'].party.code) changedIncumbent = true; // Current election if(data.elections['2019-12-12']){ @@ -307,52 +309,92 @@ if(!data.elections[yy].party){ lbl += '

2019 Candidates:

'; } - total = 0; - // Work out total votes cast and set any missing ones to zero + + // Set votes to zero for(var c = 0; c < data.elections[yy].candidates.length; c++){ - if(data.elections[yy].candidates[c].votes) total += data.elections[yy].candidates[c].votes; - else data.elections[yy].candidates[c].votes = 0; + if(!data.elections[yy].candidates[c].votes) data.elections[yy].candidates[c].votes = 0; } + // Sort the candidates by their results data.elections[yy].candidates.sort(function(a, b) { if(a.votes===b.votes) return 0; else return (a.votes < b.votes) ? -1 : 1; }).reverse(); - + + total = getTotalVotes(data.elections[yy]); + total2 = getTotalVotes(data.elections['2017-06-08']); + if(total > 0){ - p1 = getPartyDetails(data.elections[yy].candidates[0].party); - p2 = getPartyDetails(incumbent.party); + var ccode = -1; + for(var c = 0; c < data.elections[yy].candidates.length; c++){ + if(data.elections[yy].candidates[c].name == data.elections[yy].mp) img = data.elections[yy].candidates[c].img; + } + p1 = getPartyDetails(data.elections[yy].party); + p2 = getPartyDetails(lastge.party); lbl += '
'; - lbl += '
'; - lbl += '
'; - lbl += ''+p1.short+' '+(p1.code==p2.code ? 'HOLD':'GAIN from '+p2.short) - lbl += '
'+data.elections[yy].candidates[0].name; - if(data.elections[yy].majority) lbl += '
Majority: '+data.elections[yy].majority.toLocaleString(); + lbl += '
'; + lbl += '
'; + if(p1.code == "Spk") lbl += data.elections[yy].mp+' re-elected as Speaker'; + else if(p2.code == "Spk" && p1.code != "Spk") lbl += p1.short+' GAIN'; + else lbl += ''+p1.short+' '+(p1.code==p2.code ? 'HOLD':'GAIN from '+p2.short); + lbl += '
Elected: '+data.elections[yy].mp+''; + if(data.elections[yy].majority) lbl += '
Majority: '+data.elections[yy].majority.toLocaleString()+(data.elections[yy].majority.pc ? '('+data.elections[yy].majority.pc.toFixed(1)+'%)':''); + lbl += '
'; } - lbl += ''; + + if(total > 0 && total2 > 0){ + for(var c = 0; c < data.elections[yy].candidates.length; c++){ + if(data.elections[yy].candidates[c].party.code!="Ind" && typeof data.elections[yy].candidates[c].change!=="number"){ + // Default to the current percent (if they didn't exist last election) + data.elections[yy].candidates[c].change = parseFloat(data.elections[yy].candidates[c].pc); + for(var c2 = 0; c2 < data.elections['2017-06-08'].candidates.length; c2++){ + // If the party seems to have existed last time work out the change + if(fuzzyMatchParty(data.elections[yy].candidates[c].party,data.elections['2017-06-08'].candidates[c2].party)) data.elections[yy].candidates[c].change = (parseFloat(data.elections[yy].candidates[c].pc) - parseFloat(data.elections['2017-06-08'].candidates[c2].pc)); + } + } + } + } + + lbl += '
'; for(var c = 0; c < data.elections[yy].candidates.length; c++){ party = data.elections[yy].candidates[c].party; p1 = getPartyDetails(party); name = data.elections[yy].candidates[c].name; url = "https://candidates.democracyclub.org.uk/person/"+data.elections[yy].candidates[c].id; - if(data.elections[yy].party) lbl += ''; + pc = data.elections[yy].candidates[c].votes/total; + if(data.elections[yy].party) lbl += ''; else lbl += ''; } lbl += '
'+p1.short+': '+data.elections[yy].candidates[c].votes.toLocaleString()+'
'+acronymise(p1.short||p1.title)+': '+(100*pc).toFixed(1)+'% '+(typeof data.elections[yy].candidates[c].change==="number" ? '('+(data.elections[yy].candidates[c].change >= 0 ? '+':'')+data.elections[yy].candidates[c].change.toFixed(1)+'%)':'')+'
'+name+' - '+p1.title+'
'; - tbl = ''; - if(data.elections[yy].turnout) tbl += 'Turnout:'+data.elections[yy].turnout.pc+'% ('+(data.elections[yy].valid+(data.elections[yy].invalid||0)).toLocaleString()+')'; - if(data.elections[yy].invalid) tbl += 'Invalid votes:'+data.elections[yy].invalid.toLocaleString()+''; + if(attr.timestamp) lbl += 'Last updated: '+attr.timestamp+''; + lbl += '

'; + var tbl = ''; + if(data.elections[yy].turnout && typeof data.elections[yy].turnout.pc!=="number") data.elections[yy].turnout.pc = null; + if(data.elections[yy].turnout){ + tbl += 'Turnout:'; + if(data.elections[yy].turnout.pc) tbl += data.elections[yy].turnout.pc+'% ('+data.elections[yy].turnout.value.toLocaleString()+')'; + else tbl += data.elections[yy].turnout.value.toLocaleString(); + tbl += ''; + } + if(data.elections[yy].valid) tbl += 'Valid votes:'+data.elections[yy].valid.toLocaleString()+''; + if(data.elections[yy].invalid) tbl += 'Invalid votes:'+data.elections[yy].invalid.toLocaleString()+''; + if(data.elections[yy].electorate) tbl += 'Electorate:'+data.elections[yy].electorate.toLocaleString()+''; + if(data.demographics){ + if(data.demographics['age18-29']) tbl += 'Age 18-29:'+data.demographics['age18-29']+'%'; + if(data.demographics['withdegree']) tbl += 'With a degree:'+data.demographics['withdegree']+'%'; + if(data.demographics['2016Leave']) tbl += '2016 Leave vote (estimate):'+data.demographics['2016Leave']+'%'; + if(data.demographics['2015UKIP']) tbl += '2015 UKIP vote:'+data.demographics['2015UKIP']+'%'; + } if(tbl) lbl += ''+tbl+'
'; - if(changedIncumbent){ + if(incumbent){ lbl += '

'; party = incumbent.party; p1 = getPartyDetails(party); - lbl += ''+incumbent.mp+' - '+p1.title; + lbl += ''+incumbent.mp+' - '+p1.title+''; } - } for(var y = 0; y < elections.length; y++){ @@ -366,33 +408,117 @@ } party = data.elections[yy].party; p1 = getPartyDetails(party); - lbl += ''+(data.elections[yy].mysoc ? '':'')+data.elections[yy].mp+' - '+p1.title+''+(data.elections[yy].mysoc ? '':'')+'
'; - if(data.elections[yy].majority) lbl += 'Majority: '+data.elections[yy].majority.toLocaleString()+'
'; - if(data.elections[yy].turnout) lbl += 'Turnout: '+data.elections[yy].turnout.pc+'% ('+data.elections[yy].turnout.value.toLocaleString()+')
'; - if(data.elections[yy].invalid) lbl += 'Invalid votes: '+data.elections[yy].invalid.toLocaleString()+'
'; + lbl += ''+(data.elections[yy].mysoc ? '':'')+data.elections[yy].mp+' - '+p1.title+''+(data.elections[yy].mysoc ? '':'')+'
'; + if(data.elections[yy].majority) lbl += 'Majority: '+data.elections[yy].majority.toLocaleString()+'
'; + if(data.elections[yy].turnout) lbl += 'Turnout: '+data.elections[yy].turnout.pc+'% ('+data.elections[yy].turnout.value.toLocaleString()+')
'; + if(data.elections[yy].invalid) lbl += 'Invalid votes: '+data.elections[yy].invalid.toLocaleString()+'
'; lbl += '

'; } } } - // Add bio - if(this.data['constituency-card']){ - r = this.data['constituency-card'][region]; - lbl += '
MP: '+r['dispname']+', '+r['partynow']+'
Area: '+r['sq_km']+'km² ('+r['sq_mi']+' miles²)
Distance from power: '+r['km_fr_pow']+' km ('+r['mi_fr_pow']+' miles)'+(r['result17'] ? '
2017 turnout: '+r['turnout17']+'% ('+r['valid17']+'/'+r['elect17']+')
' : '')+'
'; + lbl += '

Data: New Statesman / Britain Elects / Democracy Club / They Work For You / Parliament (2015 & 2017).

' + + function postRender(title,region,data){ + S('#tooltip').remove(); + if(!data.elections['2019-12-12']) return; + var prev = -1; + S('.infobubble .results tr').on('click',{me:this,'region':region,'election':data.elections['2019-12-12']},function(e){ + var id = S(e.currentTarget).attr('candidate'); + if(id==prev){ + S('#tooltip').remove(); + }else{ + S('#tooltip').remove(); + var election = e.data.election; + var candidate = ""; + var total = 0; + for(var c = 0; c < election.candidates.length; c++){ + if(election.candidates[c].id == id) candidate = election.candidates[c]; + total += election.candidates[c].votes; + } + var pc = candidate.votes/total; + var html = ""; + if(candidate.img) html += '
'; + html += '

'+candidate.name+'

'+candidate.party.title+'

Votes: '+(100*pc).toFixed(1)+'% ('+candidate.votes.toLocaleString()+')

'; + var td = S(e.currentTarget).find('td'); + td.css({'position':'relative'}).append('
'+html+'
'); + S('#tooltip').css({'position':'absolute','left':'50%','top':'100%'}); + } + prev = id; + }); + S('.infobubble .close')[0].focus(); + + // Update the image for the winner + var my_image = new Image(); + my_image.onload = function(){ + // replace image in popup + var img = S('.infobubble .image img'); + var title = img.attr('title'); + img.parent().html('')[0].appendChild(my_image); + + } + my_image.src = img; + } - - lbl += '

Data: New Statesman / Democracy Club / They Work For You / Houses of Parliament / Houses of Parliament.

' - return {'label':lbl,'class':'generalelection','color':wincolour||''}; + return {'label':lbl,'class':'generalelection','color':wincolour||'','callback': postRender }; + } + function getTotalVotes(el){ + + var total = 0; + // Work out total votes cast and set any missing ones to zero + for(var c = 0; c < el.candidates.length; c++){ + if(el.candidates[c].votes) total += el.candidates[c].votes; + else el.candidates[c].votes = 0; + } + // Work out percent + for(var c = 0; c < el.candidates.length; c++){ + if(typeof el.candidates[c].pc!=="number") el.candidates[c].pc = (total > 0 ? 100*el.candidates[c].votes/total : 0).toFixed(1); + } + return total; + } + function fuzzyMatchParty(a,b){ + if(!a.code && a.title) a.code = acronymise(a.title); + if(!b.code && b.title) b.code = acronymise(b.title); + + if(a.code && b.code){ + // Do the codes match? + if(a.code == b.code) return true; + // Remove leading "The" + c1 = a.code.replace(/^The /,""); + c2 = b.code.replace(/^The /,""); + // If the name has spaces in it we will acronymise it + if(c1.indexOf(" ") > 0){ + console.warn('Need to acronymise '+c1); + c1 = acronymise(c1); + } + if(c2.indexOf(" ") > 0){ + console.warn('Need to acronymise '+c2); + c2 = acronymise(c2); + } + if(c1==c2) return true; + }else{ + console.warn('Missing codes for ',a,b); + } + return false; + } + function acronymise(string){ + if(string.length > 5){ + string = string.replace(/^The /,""); + // Split by uppercase characters + var words = string.split(/(?=[A-Z])/); + var str = ""; + for(var w = 0; w < words.length; w++) str += words[w][0].toUpperCase(); + return str; + }else return string; } - function uppercaseFirst(string){ return string.charAt(0).toUpperCase() + string.slice(1); } function validParties(_obj,key){ var got = {}; for(var r in _obj.data[key]){ - party = _obj.data[key][r].party; + party = (_obj.data[key][r].party||_obj.data[key][r].first); if(!got[party]) got[party] = 0; got[party]++; } @@ -401,17 +527,21 @@ var views = { 'GE2019-results':{ - 'file': '2019-results.csv', + 'file': 'https://odileeds-uk-election-2019.s3.eu-west-2.amazonaws.com/processed/live/2019-results.csv', 'live': true, 'process': function(type,data){ if(!this.data[type]) this.data[type] = {}; - for(var i = 0; i < data.length; i++){ - code = data[i]['ccode']; - if(code) this.data[type][code] = {'first': data[i].first19}; + if(data.length > 0){ + for(var i = 0; i < data.length; i++){ + code = data[i]['ccode']; + if(code) this.data[type][code] = {'first': data[i].first19}; + } + }else{ + for(var r in this.hex.hexes) this.data[type][r] = {}; } }, 'popup': { - 'file': 'constituencies/%region%.json', + 'file': 'https://odileeds-uk-election-2019.s3.eu-west-2.amazonaws.com/processed/live/%region%.json', 'live':true, 'render': render }, @@ -419,12 +549,13 @@ var _obj = this; var key = ""; this.hex.setColours = function(region){ + if(!_obj.data["GE2019-results"][region]) return parties.none.color; r = _obj.data["GE2019-results"][region].first; if(r) r = r.replace(/[\n\r]/g,""); //else console.warn('No region',r,region,parties[r]); return (parties[r] ? parties[r].color||parties.none.color : parties.none.color); } - got = validParties(_obj,'GE2019-results'); + var got = validParties(_obj,'GE2019-results'); for(var party in parties){ if(got[party]) key += '
'+(parties[party].title || party)+'
'; } @@ -529,7 +660,7 @@ 'width':915, 'height':1120, 'padding':0, - 'file':'../maps/constituencies.hexjson', + 'file':'constituencies.hexjson', 'views': views, 'search':{'id':'search'} });