Skip to content

Instantly share code, notes, and snippets.

@borgar
Last active January 1, 2020 09:18
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save borgar/55a51313ed77df615d2de5a694bfb04d to your computer and use it in GitHub Desktop.
Save borgar/55a51313ed77df615d2de5a694bfb04d to your computer and use it in GitHub Desktop.
Possible majority coalitions 2017
license: mit
height: 507
border: no
scrolling: yes

This is a remix of the same chart for last year's elections.

7 or more parties will in all likelyhood get parlimentary seats in Iceland's 2017 parlimentary elections, so it is quite challenging to understand what majority posibilities exist post-election.

What this attempts to do is show what majority coalitions are possible using latest poll data, ruling out coalitions which been rejected by the parties themselves or are obviously not going to happen. What remains indicates that forming a viable majority will be challenging.

In order to determine seats per-party a predictive model is used. National percentages, provided by kosningaspa.is, are distributed among constituencies using known historical biases. The resulting outcome is then run through a full implementation of the Icelandic election system.

The chart will continually update until the elections on the 28th of october, 2017.

{
"B": { "NV": 0.143695, "NA": 0.201159, "SU": 0.210940, "SV": 0.209046, "RS": 0.121961, "RN": 0.113199 },
"D": { "NV": 0.086783, "NA": 0.103354, "SU": 0.150575, "SV": 0.311591, "RS": 0.181478, "RN": 0.166219 },
"F": { "NV": 0.131399, "NA": 0.098337, "SU": 0.150509, "SV": 0.236231, "RS": 0.207670, "RN": 0.175854 },
"N": { "NV": 0.068118, "NA": 0.075935, "SU": 0.092686, "SV": 0.222781, "RS": 0.281407, "RN": 0.259073 },
"S": { "NV": 0.080672, "NA": 0.108312, "SU": 0.133604, "SV": 0.270130, "RS": 0.204517, "RN": 0.202765 },
"T": { "NV": 0.068211, "NA": 0.098136, "SU": 0.172276, "SV": 0.303925, "RS": 0.188377, "RN": 0.169075 },
"V": { "NV": 0.095259, "NA": 0.165852, "SU": 0.094389, "SV": 0.205329, "RS": 0.202606, "RN": 0.236565 },
"I": { "NV": 0.032800, "NA": 0.043632, "SU": 0.117437, "SV": 0.305285, "RS": 0.256170, "RN": 0.244676 },
"O": { "NV": 0.043420, "NA": 0.051039, "SU": 0.102153, "SV": 0.327539, "RS": 0.227532, "RN": 0.248317 },
"P": { "NV": 0.064030, "NA": 0.080338, "SU": 0.126963, "SV": 0.263451, "RS": 0.220499, "RN": 0.244719 },
"A": { "NV": 0.046935, "NA": 0.076675, "SU": 0.097240, "SV": 0.354138, "RS": 0.212741, "RN": 0.212271 },
"G": { "NV": 0.063765, "NA": 0.090742, "SU": 0.215205, "SV": 0.283568, "RS": 0.176272, "RN": 0.170448 },
"J": { "NV": 0.382979, "NA": 0.151410, "SU": 0.203859, "SV": 0.093023, "RS": 0.079664, "RN": 0.089065 },
"L": { "NV": 0.053886, "NA": 0.067196, "SU": 0.092529, "SV": 0.266423, "RS": 0.220052, "RN": 0.299914 },
"C": { "NV": 0.052542, "NA": 0.074585, "SU": 0.099799, "SV": 0.345093, "RS": 0.223452, "RN": 0.204529 }
}
/* globals d3 */
function dhont (parties, total_seats) {
const list = [];
const partySeats = {};
const quot = (d) => d.votes / (partySeats[d.party] + 1);
parties.forEach(d => { partySeats[d.party] = 0; });
for (let round = 0; round < total_seats; round++) {
const top = parties.sort((a, b) => quot(b) - quot(a))[0];
partySeats[top.party]++;
list.push({
party: top.party,
constituency: top.constituency || '',
seat: partySeats[top.party],
q: quot(top)
});
}
return list;
}
function getConstituencies (year) {
const constituencies = {
2003: { NV: [ 9, 1 ], NA: [ 9, 1 ], SU: [ 9, 1 ], SV: [ 9, 2 ], RS: [ 9, 2 ], RN: [ 9, 2 ] },
2007: { NV: [ 8, 1 ], NA: [ 9, 1 ], SU: [ 9, 1 ], SV: [ 10, 2 ], RS: [ 9, 2 ], RN: [ 9, 2 ] },
2009: { NV: [ 8, 1 ], NA: [ 9, 1 ], SU: [ 9, 1 ], SV: [ 10, 2 ], RS: [ 9, 2 ], RN: [ 9, 2 ] },
2013: { NV: [ 7, 1 ], NA: [ 9, 1 ], SU: [ 9, 1 ], SV: [ 11, 2 ], RS: [ 9, 2 ], RN: [ 9, 2 ] },
2016: { NV: [ 7, 1 ], NA: [ 9, 1 ], SU: [ 9, 1 ], SV: [ 11, 2 ], RS: [ 9, 2 ], RN: [ 9, 2 ] },
2017: { NV: [ 7, 1 ], NA: [ 9, 1 ], SU: [ 9, 1 ], SV: [ 11, 2 ], RS: [ 9, 2 ], RN: [ 9, 2 ] }
};
return Object.keys(constituencies[year]).map(c => {
return {
id: c,
fixed: constituencies[year][c][0],
level: constituencies[year][c][1]
};
});
}
// eslint-disable-next-line
function elections (data, year = 2017, debug = false) {
/* expects data as a list of objects in the format of:
[ { party: 'D', votes: 1234, constituency: 'NE' },
{ party: 'G', votes: 2341, constituency: 'NE' },
...
*/
const constituencies = getConstituencies(year);
const seats_fixed = {};
const seats_level = {};
const seats_alotted = {};
const seats_runup = {};
const seats_by_party = {};
const min_percent = 0.05;
const total_votes = d3.sum(data, d => d.votes);
const votes_by_area = data.reduce((c, d) => {
c[d.constituency] = (c[d.constituency] || 0) + d.votes;
return c;
}, {});
constituencies.forEach(d => {
seats_fixed[d.id] = d.fixed;
seats_level[d.id] = d.level;
seats_alotted[d.id] = [];
seats_runup[d.id] = [];
});
const d1 = d3.nest().key(d => d.constituency).entries(data);
d1.forEach(d => {
const constituency = d.key;
d.sum_votes = d3.sum(d.values, d => d.votes);
d.uniq_party = d3.set(d.values.map(d => d.party)).values();
// FIXME: need a better system for excluding
const votes = d.values.filter(d => d.party !== '_' && d.party !== '?');
const res = dhont(votes, seats_fixed[constituency] + 200);
seats_alotted[constituency] = res.slice(0, seats_fixed[constituency]);
seats_runup[constituency] = res.slice(seats_fixed[constituency]);
});
const seats_table = [];
d1.forEach(d => {
const constituency = d.key;
d.uniq_party.forEach(p => {
const seats = seats_alotted[constituency].filter(f => f.party === p).length;
const votes = d.values.filter(f => f.party === p)[0].votes;
seats_by_party[p] = (seats_by_party[p] || 0) + seats;
seats_table.push({
constituency: constituency,
party: p,
seats: seats,
votes: votes
});
});
});
// Ef flokkur hefur hlotið minna en 5% atkvæða kemur
// hann ekki til greina við úthlutun jöfnunarsæta.
const parties = d3.set(data.map(d => d.party)).values()
.map(p => ({ party: p, votes: d3.sum(data, d => d.party === p ? d.votes : 0) }))
.filter(p => (p.votes / total_votes) >= min_percent)
// FIXME: need a better system for excluding parties
.filter(d => d.party !== '_' && d.party !== '?');
const valid_parties = d3.set(parties.map(d => d.party));
// Fyrir aðra flokka er tekinn saman listi yfir svokallaðar landstölur flokksins.
// Fyrsta talan á þeim lista er heildaratkvæðafjöldi flokksins á landsvísu deilt með
// fjölda kjördæmissæta hans að viðbættum einum; önnur talan er atkvæðafjöldinn deilt
// með kjördæmissætum að viðbættum tveimur, o.s.frv.
const num_level_seats = d3.sum(constituencies.map(d => seats_level[d.id]));
let level_table = [];
parties.forEach(p => {
for (let i = 1; i < num_level_seats + 1; i++) {
const level = p.votes / (seats_by_party[p.party] + i);
level_table.push({ party: p.party, level: level });
}
});
level_table = level_table
.sort((a, b) => b.level - a.level)
.slice(0, num_level_seats);
// Taka skal saman skrá um þau tvö sæti hvers framboðslista sem næst
// komust því að fá úthlutun í kjördæmi skv. 107. gr. Við hvert þessara
// sæta skal skrá hlutfall útkomutölu sætisins skv. 1. tölul. 107. gr.
// af öllum gildum atkvæðum í kjördæminu.
function ratioTable (baselist, numrows) {
const list = [];
d3.range(numrows).forEach(i => {
baselist.forEach(d => {
const q = d.votes / (d.seats + 1 + i);
list.push({
party: d.party,
ratio: (q / votes_by_area[d.constituency]) * 100,
constituency: d.constituency,
votes: d.votes,
seats: d.seats,
quot: q,
index: i
});
});
});
return list;
}
let totelist = constituencies.reduce((list, c) => {
const r = seats_table.filter(d => d.constituency === c.id);
const tab = ratioTable(r, c.level);
return list.concat(tab);
}, []);
totelist = totelist
.filter(d => valid_parties.has(d.party))
.sort((a, b) => b.ratio - a.ratio);
const have_seats = constituencies.reduce((t, c) => { t[c.id] = c.level; return t; }, {});
const level_seats = level_table.map((lev, i) => {
const p = totelist.filter(d => d.party === lev.party && have_seats[d.constituency] && !d.used);
const seat = p[0];
// mark this leveling seat used
seat.used = true;
have_seats[seat.constituency]--;
seats_by_party[seat.party] = (seats_by_party[seat.party] || 0) + 1;
return {
seat: i + 1,
level: lev.level,
ratio: seat.ratio,
party: seat.party,
constituency: seat.constituency
};
});
// prepare a final list of allotments
let seats_final = [];
const sbp = {};
seats_table.forEach(d => {
for (let i = 0; i < d.seats; i++) {
sbp[d.party] = (sbp[d.party] || 0) + 1;
seats_final.push({
constituency: d.constituency,
party: d.party,
seat: sbp[d.party],
fixed: true
});
}
});
seats_final = seats_final.concat(level_seats.map(d => {
sbp[d.party] = (sbp[d.party] || 0) + 1;
return {
constituency: d.constituency,
party: d.party,
seat: sbp[d.party],
level: true
};
}));
return {
seats: seats_final,
seats_by_party: seats_by_party,
seats_fixed: seats_table.filter(d => d.seats),
seats_level: level_seats
};
}
/* globals d3 elections */
function factTable (depth = 2) {
const table = {};
const fact = function (p, c) {
let t = table;
for (let i = 0; i < depth; i++) {
const k = arguments[i];
if (!t[k]) { t[k] = {}; }
t = t[k];
}
if (t.value == null) {
t.value = 0;
}
return t;
};
fact.values = table;
return fact;
}
const _kjorskra = { NA: 29618, NV: 21516, RN: 46109, RS: 45607, SU: 36154, SV: 69498 };
function allotment (fylgi, kjorskra = _kjorskra, bias = {}) {
// fylgi = { B: 0.1773, D: 0.3368, F: 0.07382, … }
// bias = { B: { NV: 0.04, NA: 0.05 … } … }
const constituencies = Object.keys(kjorskra);
const parties = Object.keys(fylgi);
const num_constituencies = constituencies.length;
const num_parties = parties.length;
const total_voters = d3.sum(constituencies, c => kjorskra[c]);
// ensure that the bias table contains all parties:
parties.forEach(p => {
if (!bias[p]) {
// apply an even distribution
bias[p] = constituencies.reduce((a, c) => {
a[c] = 1 / num_constituencies;
return a;
}, {});
}
});
// a naïve "store" to function as a 2 dim array
const fact = factTable();
const sumC = (c) => d3.sum(parties, p => fact(p, c).value);
const biasPC = () => constituencies.reduce((s, c) => s + Math.abs(kjorskra[c] - sumC(c)), 0);
// perform initial allotment
parties.forEach(p => {
constituencies.forEach(c => {
fact(p, c).value = total_voters * fylgi[p] * bias[p][c];
});
});
const step = () => {
// find the constituency with the most
// difference between voters and votes
let curr_c = null;
let curr_v = 0;
constituencies.forEach(c => {
const v = Math.abs(kjorskra[c] - sumC(c));
if (v > curr_v) {
curr_c = c;
curr_v = v;
}
});
// the difference is divided evenly between
// all parties in all other constituencies
const votes = sumC(curr_c);
constituencies.forEach(c => {
const m = votes - kjorskra[curr_c];
if (c === curr_c) {
// remove votes from all parties in this constituency
const v = m / num_parties;
parties.forEach(p => { fact(p, c).value -= (v * fylgi[p]); });
}
else {
// add votes to all parties in this constituency
const v = (m / (num_constituencies - 1)) / num_parties;
parties.forEach(p => { fact(p, c).value += (v * fylgi[p]); });
}
});
};
if (true) {
console.time('compute');
let pre;
let post;
let nstep = 0;
do {
pre = biasPC();
step();
nstep++;
post = biasPC();
}
while ((pre - post) > 2);
console.timeEnd('compute');
console.log(`${nstep} steps`);
}
const ret = [];
parties.forEach(p => {
constituencies.forEach(c => {
ret.push({
party: p,
constituency: c,
votes: Math.round(fact(p, c).value)
});
});
});
return ret;
}
<!DOCTYPE html>
<meta charset='utf-8'>
<style>
svg {
font: 11px sans-serif;
}
.header {
text-anchor: start;
font: bold 16px sans-serif;
}
.legend {
text-anchor: start;
font: 13px sans-serif;
}
.legend-r {
text-anchor: end;
font: 13px sans-serif;
}
rect.seat {
stroke-width: .5;
}
line {
shape-rendering: crispEdges;
}
</style>
<body>
<script src="//d3js.org/d3.v3.min.js" charset="utf-8"></script>
<script src="election-system.js"></script>
<script src="force-allotment.js"></script>
<script src="util.js"></script>
<script>
function toSeats ( lineup ) {
var _lineup = [];
lineup.forEach(d => {
for (var i=0; i<parties[d].seats; i++) {
_lineup.push({ id: d, index: _lineup.length });
}
});
while (_lineup.length < total_mps) {
_lineup.push({ id: '?', index: _lineup.length });
}
return _lineup;
}
var margin = { top: 20, right: 10, bottom: 25, left: 3 },
width = 960 - margin.left - margin.right,
height = 1500 - margin.top - margin.bottom,
cx = width / 2;
var total_mps = 63;
var half_mps = total_mps / 2;
var parties = {
'A': { id: 'A', label: 'Björt framtíð', color: '#a151e9' },
'B': { id: 'B', label: 'Framsókn', color: '#b8d887' },
'C': { id: 'C', label: 'Viðreisn', color: '#62b8e7' },
'D': { id: 'D', label: 'Sjálfstæðisfl.', color: '#517cb6' },
'P': { id: 'P', label: 'Píratar', color: '#7f7f7f' },
'S': { id: 'S', label: 'Samfylkingin', color: '#cf5163' },
'V': { id: 'V', label: 'Vinstri græn', color: '#7ca772' },
'F': { id: 'F', label: 'Flokkur fólksins', color: '#FF6FCF' },
'T': { id: 'T', label: 'Dögun', color: '#222222' },
'M': { id: 'M', label: 'Miðflokkurinn', color: '#256676' },
'?': { id: '?', label: 'Aðrir', percent: 0 }
};
var common_coalitions = [
['D','A','C'], // núverandi stjórn
['B','D'], // the all time classic stjórn
['D','C','B','M'], // hægri stórn
];
var root = d3.select( 'body' ).append( 'svg' )
.attr( 'width', width + margin.left + margin.right )
.attr( 'height', height + margin.top + margin.bottom );
var svg = root.append( 'g' )
.attr( 'transform', `translate(${margin.left},${margin.top})` );
svg.append('defs')
.append('pattern')
.attr('id', 'stripes')
.attr('patternUnits', 'userSpaceOnUse')
.attr('width', 4)
.attr('height', 4)
.append('path')
.attr('d', "M0,4 l4,-4 M-1,1 l2,-2 M3,5 l2,-2")
.attr('stroke-width', 1)
.attr('shape-rendering', 'auto')
.attr('stroke', '#aaa')
.attr('stroke-linecap', 'square');
let polldata = null;
let biasdata = null;
let seats_by_party = null;
Promise.all([
loadJSON('bias.json'),
loadCSV('https://docs.google.com/spreadsheets/d/e/2PACX-1vRG7QOfs43r-yeDQVF9uEmleXcUvF5m8PAu80WCp7PlmrnhfZp889-Ds1nQFvmVfw_udNhPoZAt6jVt/pub?gid=0&single=true&output=csv&h=ATOcDxt0ET-bhlTDg0_Mbih3rjUkq8JKiCvGPUAz1quQhhQ2_WhR0bd8OKzg3QWJFMKvZpQFvPhRiwthS-Oaj0AQoS2FilCijdqmcoImV1Uqyf4U6c-K9Kae1O72ebsHL_9ItTUNCxkhIKM&s=1&enc=AZMcCoWPz2m-cJfBnRdA178P2Zi5PIwnsL-6NHWnUgW26IaFGWz86OfIXYUKR9xILTvQuVjuI3BqGP2sknSVdX1K')
])
.then((arr) => {
const [ bias, data ] = arr;
const row = data[0];
const pt = {};
polldata = [];
biasdata = bias;
Object.keys(parties)
.forEach(k => {
const p = parties[k];
pt[p.label.slice(0, 5).toLowerCase()] = p;
});
Object.keys(row).forEach(k => {
const m = /^\s*(.{5}).*?(?:\(([A-Z])\))?\s*$/.exec(k.toLowerCase());
if (m && pt[m[1]]) {
pt[m[1]].percent = +row[k]
polldata.push(pt[m[1]])
}
});
})
.then(() => {
// allot "votes" into constituencies
const fylgi = polldata.reduce((o, d) => { o[d.id] = d.percent * 0.01; return o; }, {});
const votes = allotment(fylgi, undefined, biasdata);
// run the election system calculations as normal
const electionData = elections(votes);
// console.log( electionData );
polldata.forEach(d => { d.seats = electionData.seats_by_party[d.id] || 0; });
return votes;
})
.then(() => render(polldata))
function render ( data ) {
var sections = [];
var pool = d3.comb(polldata.filter(d => d.seats).map(d => d.id))
.map(lineup => {
// filter out obvious "won't work"s
var set = d3.set(lineup);
if (// Píratar vinna ekki með D
(set.has('P') && set.has('D')) ||
// Það er afar ósennilegt að C og V vinni saman
(set.has('C') && set.has('V'))
) {
return null;
}
var id = lineup.sort(d3.ascending).join(''),
counts = lineup.map(d => parties[d].seats).sort(d3.descending),
mps = d3.sum(counts),
but_last = d3.sum(counts.slice(0,-1));
if (but_last > half_mps) { return null; }
if (mps && mps > half_mps) {
lineup = lineup.sort((a,b) => d3.descending(parties[a].percent, parties[b].percent));
return {
'id': id,
'mps': mps,
'seats': toSeats(lineup),
'lineup': lineup,
};
}
})
.filter(Boolean)
.sort((a,b) => b.mps - a.mps);
sections.push({
'offset': 0,
'title': "Mögulegar meirihlutastjórnir",
'combinations': pool,
});
var runners_up = common_coalitions
.map(lineup => {
var id = lineup.sort(d3.ascending).join('');
if (pool.filter( d=> d.id === id).length) {
// already exists in pool
return null;
}
lineup = lineup.sort((a,b) => d3.descending(parties[a].percent, parties[b].percent));
return {
'id': id,
'mps': d3.sum(lineup, d => parties[d].seats),
'seats': toSeats(lineup),
'lineup': lineup,
};
});
sections.push({
'offset': d3.sum(sections, d => d.combinations.length + 1),
'title': 'Pólitískur ómöguleiki',
'combinations': runners_up.filter(Boolean),
});
// reconfigure chart height:
height = d3.sum(sections, d => 1 + d.combinations.length) * 45;
root.attr('height', height + margin.top + margin.bottom);
svg.append('rect')
.attr('width', width)
.attr('height', height)
.attr('fill', 'white');
var x = d3.scale.linear()
.domain([ 0, total_mps ])
.range([ 0, Math.floor(width/total_mps) * total_mps ]);
var y = d3.scale.ordinal()
.domain(pool.map(d => d.id))
.rangeBands([ 0, pool.length * 45 ]);
var sects = svg.selectAll('.section')
.data(sections).enter()
.append('g')
.attr('class', 'section')
.attr('transform', d => `translate(0,${(1+d.offset) * 45})`);
sects.append('text')
.attr('class', 'header')
.attr('y', -30)
.text(d => d.title);
var stacks = sects.selectAll('.stack')
.data(d => d.combinations).enter()
.append('g')
.attr('class', 'stack')
.attr('transform', (d, i) => `translate(0,${i * 45})`);
stacks.append('text')
.attr('class', 'legend')
.attr('dy', -5)
.text(d => {
var t = d.lineup.map(d => parties[d].label).join(', ');
return `${d.mps} þingsæti: ${t}.`;
});
stacks.append('text')
.attr('class', 'legend-r')
.attr('x', x(total_mps))
.attr('dy', -5)
.text(d => {
if (d.mps < half_mps) {
return `Vantar ${-Math.floor(d.mps - half_mps)} þingsæti.`;
}
var seats = (d.mps - half_mps) * 2;
if (seats === 1) {
return '1 sætis meirihluti.';
}
return `${seats} sæta meirihluti.`;
});
var bars = stacks.selectAll('.seat')
.data(d => d.seats).enter();
bars.append('rect')
.attr('class', 'seat')
.attr('x', d => x(d.index))
.attr('height', x(1))
.attr('width', x(1))
.attr('fill', d => parties[d.id].color || 'url(#stripes)')
.attr('stroke', 'white');
bars.append('circle')
.attr('class', 'empty-seat')
.attr('cy', x(1) / 2)
.attr('cx', d => x(d.index) + x(1) / 2)
.attr('r', x(1) / 5)
.attr('fill', d => {
if ( d.index < half_mps && d.id === '?' ) {
return 'gray';
}
if ( d.index > half_mps - 1 && d.id !== '?' ) {
return 'white';
}
return 'none';
})
.attr('stroke', d => {
if ( d.index < half_mps && d.id === '?' ) {
return 'white';
}
if ( d.index > half_mps - 1 && d.id !== '?' ) {
return 'gray';
}
return 'none';
});
}
</script>
d3.comb = function ( arr ) {
function _cmb ( active, rest, comb ) {
if ( active.length && !rest.length ) {
comb.push( active );
}
else if ( active.length || rest.length ) {
var _rest = rest.slice( 1 );
_cmb( active.concat([ rest[0] ]), _rest, comb );
_cmb( active, _rest, comb );
}
return comb;
}
return _cmb( [], arr, [] );
};
function loadJSON (url) {
return new Promise((resolve, reject) => {
d3.json(url, (err, data) => {
err ? reject(err) : resolve(data);
});
});
}
function loadCSV (url) {
return new Promise((resolve, reject) => {
d3.csv(url, (err, data) => {
err ? reject(err) : resolve(data);
});
});
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment