Skip to content

Instantly share code, notes, and snippets.

@borgar
Created May 13, 2018 16:40
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/21a293abfb94a33835cc9298da142a90 to your computer and use it in GitHub Desktop.
Save borgar/21a293abfb94a33835cc9298da142a90 to your computer and use it in GitHub Desktop.
Meiri samanburður á frambjóðendum 2018
license: mit
height: 980
border: no
scrolling: no

Hversu sammála eru frambjóðendur í sveitastjórnarkosningunum 2018? Ef eitthvað er að marka þetta fylkisrit sem byggir á svörum frambjóðenda í Kosningaprófi RÚV þá virðist svarið vera "heilt yfir, ágætlega". Hægt er þó að sjá hvaða frambjóðendur skera sig úr, og hvernig frambjóðendur eru almennt meira sammála meðframbjóðendum en öðrum.

Hér byggir auðvitað á þeirri forsendu að Kosningapróf RÚV hafi náð að kjarna helstu hjartans mál framboða, sem er nánast öruggt að það gerir ekki í einungis 31 spurningu.

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset=utf-8 />
<title>2018 municipality elections in Iceland matrix</title>
</head>
<body>
<style>
body {
font-family: sans-serif;
font-size: 12px;
background: white;
overflow: hidden;
}
svg {
font-family: sans-serif;
font-size: 12px;
}
text {
cursor: default;
}
.domain {
stroke: none;
}
.tick line {
stroke: #aaa;
stroke-dasharray: 2 2;
}
select {
width: 160px;
}
#tip {
position: absolute;
height: 0;
}
#info {
box-sizing: border-box;
width: 250px;
position: absolute;
border: 1px solid #ccc;
box-shadow: 0px 2px 5px rgba(0,0,0,.2);
background: white;
padding: 0 1em;
}
#info small {
display: block;
}
#info em {
display: block;
font-weight: bold;
font-style: normal;
}
#ctrl {
height: 18px;
}
</style>
<script src="//cdnjs.cloudflare.com/ajax/libs/babel-polyfill/6.16.0/polyfill.js"></script>
<script src="//d3js.org/d3.v5.min.js"></script>
<div id="ctrl">
<label for="muni">Sveitarfélag</label>
<select id="muni"></select>
&mdash;
<label for="order">Raða eftir</label>
<select id="order">
<option value="party">Framboði</option>
<option value="name">Nafni</option>
<option value="agree">Þvermóðsku (hversu sammála öllum öðrum)</option>
</select>
</div>
<svg width="960" height="960"></svg>
<div id="tip"><div id="info"></div></div>
<script src="main.js"></script>
</body>
</html>
/* globals d3 */
const svg = d3.select('svg');
const margin = { top: 164, right: 1, bottom: 1, left: 164 };
const width = +svg.attr('width') - margin.left - margin.right;
const height = +svg.attr('height') - margin.top - margin.bottom;
const plot = svg.append('g').attr('transform', `translate(${margin.left},${margin.top})`);
var x = d3.scaleBand().range([ 0, width ]).padding(0);
var y = d3.scaleBand().range([ 0, height ]).padding(0);
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 questions = [];
let candById = {};
let data;
function tooltipShow (info) {
if (info.row._id === info.col._id) {
if (info.col.Kyn === 'Karl') {
pro = 'Hann';
}
if (info.col.Kyn === 'Kona') {
pro = 'Hún';
}
d3.select('#info').html(
`<p><small>Röð / Dálkur</small> <em>${info.row.Nafn}</em> (${info.row.Framboð})</p>` +
`<p>${pro} er ${Math.round(100 * info.row.agreeableness)}% sammála öllum öðrum.</p>`
);
}
else {
let pro = 'Þau';
if (info.col.Kyn === 'Karl' && info.row.Kyn === info.col.Kyn) {
pro = 'Þeir';
}
if (info.col.Kyn === 'Kona' && info.row.Kyn === info.col.Kyn) {
pro = 'Þær';
}
d3.select('#info').html(
`<p><small>Röð:</small> <em>${info.row.Nafn}</em> (${info.row.Framboð})</p>` +
`<p><small>Dálkur:</small> <em>${info.col.Nafn}</em> (${info.col.Framboð})</p>` +
`<p>${pro} eru ${Math.round(100 * info.value)}% sammála, byggt á ${info.num} spurningum.</p>`
);
}
d3.select('#tip').style('display', '');
tooltipMove();
}
function tooltipMove () {
const mouse = d3.mouse(document.body);
let l = (mouse[0] + 15);
if (l > (+svg.attr('width') - 300)) {
l = mouse[0] - 250;
}
const h = mouse[1] + 15;
d3.select('#info')
.style('bottom', h > (+svg.attr('height') / 2) ? '0px' : '');
d3.select('#tip')
.style('top', h + 'px')
.style('left', l + 'px');
}
function tooltipHide () {
d3.select('#tip').style('display', 'none');
}
const _cache = {};
function compare (a, b) {
const cid = a._id + ':' + b._id;
if (!_cache[cid]) {
let sum = 0;
let num = 0;
let v = [];
questions.forEach(q => {
if (q.label === 'Sveitarfélag') { return; }
const a_val = a[q.label];
const b_val = b[q.label];
if (a_val != null && b_val != null) {
sum += Math.abs(a_val - b_val);
num++;
v.push(a_val - b_val);
}
})
_cache[cid] = {
row: a,
col: b,
sum: sum,
num: num,
value: num ? 1 - (sum / num) * 0.5 : null
};
}
return _cache[cid];
}
function norm (str) {
return str && str.toLowerCase().trim();
}
const orderBy = {
party: (a, b) => d3.ascending(norm(a.Framboð), norm(b.Framboð)) || d3.ascending(a._seat, b._seat),
name: (a, b) => d3.ascending(norm(a.Nafn), norm(b.Nafn)) || d3.ascending(a._seat, b._seat),
agree: (a, b) => d3.descending(a.agreeableness, b.agreeableness)
};
function render () {
const muni = document.getElementById('muni').value;
const order = document.getElementById('order').value || 'party';
const numByParty = {};
const cands = data.filter(d => {
if (d.Sveitarfélag === muni) {
if (d._answers < 5) {
// sorry, this cand is going to ruin the chart for everyone
return false;
}
const nth = numByParty[norm(d.Framboð)] || 1;
if (nth <= 4) {
d._seat = nth;
numByParty[norm(d.Framboð)] = nth + 1;
return true;
}
}
return false;
});
cands.forEach(d => {
d.agreeableness = cands.reduce((a, c) => a + compare(d, c).value, 0) / cands.length;
});
cands.sort(orderBy[order]);
y.domain(cands.map(d => d._id));
x.domain(cands.map(d => d._id));
plot.html(null);
const yAxis = d3.axisLeft(y)
.tickSize(0)
.tickFormat(t => candById[t].Nafn);
plot.append('g')
.attr('class', 'axis axis--y')
.call(yAxis);
const xAxis = d3.axisTop(x)
.tickSize(0)
.tickFormat(t => candById[t].Nafn);
plot.append('g')
.attr('class', 'axis axis--x')
.call(xAxis)
.attr('text-anchor', 'start')
.selectAll('text')
.attr('x', 2)
.attr('y', 0)
.attr('dy', '.3em')
.attr('transform', 'rotate(-90)');
const matrix = d3.cross(cands, cands, compare);
const ext = d3.extent(matrix.map(d => d.value));
const color = d3.scaleLinear()
.domain(ext)
.range(['#eee', '#044'])
.interpolate(d3.interpolateLab); //interpolateHsl interpolateHcl interpolateRgb
const cellEnter = plot.selectAll('.cell')
.data(matrix)
.enter();
cellEnter.append('rect')
.attr('class', d => 'cell' + (d.col._id === d.row._id ? ' self' : ''))
.attr('x', d => x(d.col._id))
.attr('y', d => y(d.row._id))
.attr('width', x.bandwidth() - 0.75)
.attr('height', y.bandwidth() - 0.75)
.attr('fill', d => d.col._id === d.row._id ? 'url(#stripes)': color(d.value))
.on('mouseover', tooltipShow)
.on('mousemove', tooltipMove)
.on('mouseout', tooltipHide);
}
function populateMuni () {
const muni = document.getElementById('muni');
muni.innerHTML = '';
muni.onchange = render;
d3.set(data.map(d => d.Sveitarfélag).filter(Boolean)).values()
.forEach(d => muni.appendChild(new Option(d, d, d === 'Reykjavík', d === 'Reykjavík')));
const order = document.getElementById('order');
order.onchange = render;
render();
}
d3.json('/borgar/raw/94335ee85a951b9a212efacb68e3706f/data.json')
.then(d => {
const unLabel = str => d.labels[str] || str;
data = d.data.map(pk => {
return Object.keys(pk).reduce((fact, key) => {
return (fact[unLabel(key)] = pk[key] ? unLabel(pk[key]) : pk[key]), fact;
}, {});
});
questions = d.questions;
data.forEach(d => {
d._answers = questions.reduce((a, q) => (a + (q.label in d ? 1 : 0)), 0)
})
candById = data.reduce((a, c) => ((a[c._id] = c), a), {});
})
.then(populateMuni);
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment