Skip to content

Instantly share code, notes, and snippets.

@borgar
Last active January 1, 2020 09:16
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/a80d077ed37b1c7fac9c8319b6810fcf to your computer and use it in GitHub Desktop.
Save borgar/a80d077ed37b1c7fac9c8319b6810fcf to your computer and use it in GitHub Desktop.
Election turnout pyramid
license: mit
height: 500
border: no

The city of Reykjavík performed a study on voting by age for the first time in 2014 for Reykjavík's two constituencies. So breakdown of the election turnout is a relatively new addition to elections in Iceland.

The pending numbers for the most recent election have not been published yet, but the city has published an excellent analysis on the 2014 municipality elections, which had the lowest turnout since 1928.

The Reykjavík turnout data is plotted here as a population pyramid of the registered voters, with the turnout highlighted. It's very obvious, as it was in the analysis, that the "younger generation" doesn't show up to vote. Why? We don't know. Has it maybe always been this way? We also don't know. This was the first time the breakdown was available.

Another curiosity is the lower turnout amongst females over 70 years old, compared to males over 70. Women have had full voting rights in Iceland since 1915, and even longer in municipality elections, so there are very few women alive in Iceland today that haven't always had the right to vote. We can guess that this might still be a sign of times past but since we don't have data, again, we just don't know why.

age male female total turnout_m turnout_f turnout percent_registered percent_turnout_overall
18-19 1531 1477 3008 43.8% 46.3% 45.0% 3% 2%
20-24 4592 4560 9152 41.1% 42.1% 41.6% 10% 7%
25-29 4542 4540 9082 45.0% 50.0% 47.5% 10% 8%
30-34 4715 4396 9111 53.3% 59.3% 56.2% 10% 9%
35-39 4111 3918 8029 58.2% 63.3% 60.7% 9% 9%
40-44 3786 3625 7411 62.5% 67.7% 65.1% 8% 8%
45-49 3653 3796 7449 65.4% 70.5% 68.0% 8% 9%
50-54 3799 3839 7638 70.3% 71.8% 71.0% 8% 10%
55-59 3546 3724 7270 72.6% 75.6% 74.2% 8% 9%
60-64 3146 3252 6398 77.4% 77.5% 77.4% 7% 9%
65-69 2535 2549 5084 78.8% 79.4% 79.1% 6% 7%
70-74 1606 1881 3487 82.6% 77.3% 79.7% 4% 5%
75-79 1146 1411 2557 81.1% 74.1% 77.2% 3% 3%
80+ 1787 3026 4813 70.8% 57.0% 62.1% 5% 5%
<!DOCTYPE html>
<meta charset="utf-8">
<style>
body {
margin: 0;
font: 12px sans-serif;
}
.female .registered {
fill: #F8CC98;
}
.female .voted {
fill: #FAA43A;
}
.male .registered {
fill: #A1BFE3;
}
.male .voted {
fill: #5DA5DA;
}
.female .label {
text-anchor: start;
}
.male .label {
text-anchor: end;
}
.label {
font: 11px sans-serif;
fill: #fff;
fill-opacity: .9;
}
.axislabel {
fill: #000;
font: 13px sans-serif;
}
.avg line {
stroke: black;
stroke-dasharray: 2 2;
}
.avg text, .y_label {
fill: black;
text-anchor: middle;
}
.axis, .avg {
shape-rendering: crispEdges;
}
</style>
<body>
<svg width="960" height="500"></svg>
<script src="//cdnjs.cloudflare.com/ajax/libs/babel-polyfill/6.16.0/polyfill.js"></script>
<script src="//d3js.org/d3.v4.min.js"></script>
<script>
// no margins
var svg = d3.select('svg'),
width = +svg.attr('width'),
height = +svg.attr('height');
// with margins
var pad = { top: 25, right: 65, bottom: 30, left: 65 },
width = +svg.attr('width') - pad.left - pad.right,
height = +svg.attr('height')- pad.top - pad.bottom;
svg = svg.append('g')
.attr('transform', `translate(${pad.left},${pad.top})`);
var percent = (d, s) => Math.round(d) + '% ' + (s||'');
var parse = r => {
r.male = +r.male;
r.female = +r.female;
r.turnout = parseFloat(r.turnout);
r.turnout_f = parseFloat(r.turnout_f);
r.turnout_m = parseFloat(r.turnout_m);
return r;
};
d3.csv('data.csv', parse, (err, data) => {
var size = 420;
var age = d3.scaleBand()
.domain(data.map(d => d.age))
.range([size, 0])
.padding(.15);
var _males = data.map(d => d.male);
var _females = data.map(d => d.female);
var total = d3.sum(_males.concat(_females));
var maxCount = d3.max(_males.concat(_females));
var voted_f = 0;
var voted_m = 0;
data.forEach(d => {
d.voted_f = (d.turnout_f / 100) * d.female;
d.voted_m = (d.turnout_m / 100) * d.male;
voted_f += d.voted_f;
voted_m += d.voted_m;
});
var turnout_m = voted_m / d3.sum(_males) * 100;
var turnout_f = voted_f / d3.sum(_females) * 100;
var count = d3.scaleLinear()
.domain([0, maxCount])
.range([0, size]);
var axy = svg.append('g')
.attr('class', 'y_label')
.attr('transform', d => `translate(${width/2},0)`);
axy.selectAll('.y_label')
.data(data).enter()
.append('text')
.attr('dy', '.34em')
.attr('transform', d => `translate(0,${age(d.age)+age.bandwidth()/2})`)
.text(d => d.age);
axy.append('text')
.attr('dy', '-.34em')
.text('Age');
// === === === === ===
var females = svg.append('g')
.attr('class', 'females')
.attr('transform', d => `translate(${width/2+25},0)`);
var count_f = d3.scaleLinear()
.domain([0, maxCount])
.range([0, size]);
var g = females.selectAll('.female')
.data(data).enter()
.append('g')
.attr('class', 'female')
.attr('transform', d => `translate(0, ${age(d.age)})`);
g.append('rect')
.attr('class', 'registered')
.attr('height', age.bandwidth())
.attr('width', d => count_f(d.female));
g.append('rect')
.attr('class', 'voted')
.attr('height', age.bandwidth())
.attr('width', d => count_f(d.voted_f));
g.append('text')
.attr('class', 'label')
.attr('dy', '.34em')
.attr('transform', `translate(5,${age.bandwidth() / 2})`)
.text(d => percent(d.turnout_f));
females.append('g')
.attr('class', 'axis')
.attr('transform', `translate(0,${size+5})`)
.call(d3.axisBottom().scale(count_f).tickFormat(String).ticks(5))
.append('text')
.attr('class', 'axislabel')
.attr('x', count_f(maxCount/2))
.attr('y', 35)
.text('Number of female voters');
var avg = females.append('g')
.attr('class', 'avg')
.attr('transform', `translate(${count_f(voted_f/data.length)},0)`);
avg.append('text')
.attr('dy', '-.34em')
.text(percent(turnout_f, 'avg.'));
avg.append('line')
.attr('y2', size+5);
// === === === === ===
var males = svg.append('g')
.attr('class', 'males')
.attr('transform', d => `translate(${width/2-25},0)`);
var count_m = d3.scaleLinear()
.domain([maxCount, 0])
.range([-size, 0]);
var g = males.selectAll('.male')
.data(data).enter()
.append('g')
.attr('class', 'male')
.attr('transform', d => `translate(0, ${age(d.age)})`);
g.append('rect')
.attr('class', 'registered')
.attr('x', d => -count_f(d.male))
.attr('height', age.bandwidth())
.attr('width', d => count_f(d.male));
g.append('rect')
.attr('class', 'voted')
.attr('x', d => -count_f(d.voted_m))
.attr('height', age.bandwidth())
.attr('width', d => count_f(d.voted_m));
g.append('text')
.attr('class', 'label')
.attr('dy', '.34em')
.attr('transform', `translate(-5,${age.bandwidth() / 2})`)
.text(d => percent(d.turnout_m));
males.append('g')
.attr('class', 'axis')
.attr('transform', `translate(0,${size+5})`)
.call(d3.axisBottom().scale(count_m).tickFormat(String).ticks(5))
.append('text')
.attr('class', 'axislabel')
.attr('x', count_m(maxCount/2))
.attr('y', 35)
.text('Number of male voters');
var avg = males.append('g')
.attr('class', 'avg')
.attr('transform', `translate(${-count_f(voted_m/data.length)},0)`);
avg.append('text')
.attr('dy', '-.34em')
.text(percent(turnout_m, 'avg.'));
avg.append('line')
.attr('y2', size+5);
});
</script>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment