Skip to content

Instantly share code, notes, and snippets.

@gilmoreorless
Last active June 29, 2016 02:45
Show Gist options
  • Save gilmoreorless/79af21560f8b0115a48eb64558994e48 to your computer and use it in GitHub Desktop.
Save gilmoreorless/79af21560f8b0115a48eb64558994e48 to your computer and use it in GitHub Desktop.
Senate "How to Vote" connections
height: 700
license: cc-by-4.0

An attempt to visualise the Senate preference connections between candidate parties for the 2016 Australian election.

New voting rules are in place for the 2016 election, which means that Senate parties no longer submit a full list of party preferences to the Australian Electoral Commission. The only way to determine a party’s preferences is to look at their “How to Vote” suggestions. The data for this visualisation come from the “How to Vote” preferences for parties in NSW, as collated by Antony Green.

Not all parties running for the Senate are shown here — only those where data is available (either giving or receiving preferences). Parties that deliberately don’t give preferences and haven’t received any other preferences are shown as unattached circles (e.g. Nick Xenophon Team (NXT)).

The layout is a basic D3 force-directed graph, where the forces are based on the strength of preference. Higher preferences enact a stronger force on the links, indicated by the thickness of the arrow between parties. The nodes are also “sticky” on dragging (inspired by another D3 demo), so you can move the nodes around to create a more pleasant layout and better see the connections. The node colours don’t mean anything, they’re just evenly spaced samples from a colour hue wheel to make it look prettier.

{
"parties": {
"SEC": "Secular Party of Australia",
"NXT": "Nick Xenophon Team",
"PPA": "Pirate Party Australia",
"SA": "Socialist Alliance",
"ASX": "Australian Sex Party",
"HEMP": "Marijuana (HEMP) Party",
"SCI": "Science Party",
"CYC": "Cyclists Party",
"ART": "Arts Party",
"GRN": "Greens",
"AJP": "Animal Justice Party",
"AP": "Australian Progressives",
"VEP": "Voluntary Euthanasia Party",
"DLR": "Drug Law Reform",
"REP": "Renewable Energy Party",
"ALP": "Australian Labor Party",
"JLN": "Jacqui Lambie Network",
"LNP": "Liberals and Nationals",
"AMEP": "Australian Motoring Enthusiast Party",
"ONP": "Pauline Hanson's One Nation",
"RUA": "Rise Up Australia Party",
"FFP": "Family First",
"DLP": "Democratic Labour Party",
"LD": "Liberal Democrats",
"KAP": "Katter's Australian Party",
"ALA": "Australian Liberty Alliance",
"CDP": "Christian Democratic Party",
"SFF": "Shooters, Fishers and Farmers",
"NCPP": "Non-Custodial Parents Party",
"VP": "Veterans Party",
"SUS": "Sustainable Australia",
"DHJP": "Derryn Hinch Justice Party",
"SUP": "Seniors United Party",
"CM": "CountryMinded",
"VFO": "VoteFlux.org",
"HAP": "Health Australia Party",
"SEQ": "Socialist Equality Party"
},
"knownLeft": "ASX",
"knownRight": "CDP",
"states": {
"nsw": {
"FFP": ["DLP", "LD", "KAP", "ALA", "CDP"],
"LD": ["SFF", "ASX", "FFP", "ALA", "KAP"],
"LNP": ["CDP", "SFF", "FFP", "LD", "AMEP"],
"DLP": ["FFP", "CDP", "KAP", "SFF", "HAP"],
"SCI/CYC": ["ART", "GRN", "ASX", "AJP", "ALP"],
"SFF": ["CDP", "ALA", "RUA", "LD", "LNP"],
"SA": ["GRN", "AP", "VEP", "DLR", "HEMP", "AJP", "ALP"],
"RUA": ["ONP", "CDP", "DLP", "SFF", "FFP"],
"ALP": ["GRN", "REP", "AJP", "ASX", "LD"],
"DHJP": [],
"JLN": [],
"PPA": ["GRN", "ASX", "SCI/CYC", "ALP", "REP"],
"ONP": ["RUA", "AJP", "DLP", "ALA", "NCPP"],
"VP": ["DHJP", "SUP", "CYC", "SCI", "CM"],
"SEQ": [],
"AJP": ["SUS", "REP", "HAP", "GRN", "ALP"],
"NCPP": [],
"ASX": ["HEMP", "VEP", "REP", "SEC", "AJP"],
"AP": [],
"NXT": [],
"SUS": ["AJP", "REP"],
"GRN": ["PPA", "SCI/CYC", "SA", "AJP", "ALP"],
"ALA": ["FFP", "SFF", "KAP", "ONP", "LD"],
"REP": ["AJP", "SUS", "ASX", "GRN", "ALP", "HEMP", "VFO", "SCI"]
}
}
}
<!DOCTYPE html>
<meta charset="utf-8">
<style>
body {
font-family: sans-serif;
margin: auto;
position: relative;
width: 960px;
}
.node {
cursor: pointer;
}
.node circle {
fill: #987;
stroke: #fff;
stroke-width: 1.5px;
}
.party-text {
fill: #fff;
font-size: 0.75em;
text-anchor: middle;
}
.link {
fill: #999;
fill-opacity: .9;
stroke: #999;
stroke-opacity: .6;
}
</style>
<body>
<script src="//d3js.org/d3.v3.js"></script>
<script src="//cdnjs.cloudflare.com/ajax/libs/underscore.js/1.8.3/underscore-min.js"></script>
<script>
var width = 960,
height = 700,
radius = 20;
var svg = d3.select('body').append('svg')
.attr('width', width)
.attr('height', height)
svg.append('defs')
.append('marker')
.attr('id', 'arrow-head')
.attr('orient', 'auto')
// .attr('markerUnits', 'userSpaceOnUse')
.attr('viewBox', '0 0 10 10')
.attr('refX', 30)
.attr('refY', 5)
.append('path')
.attr('class', 'link')
.attr('d', 'M 0,0 L 10,5 0,10 Z');
var dist = d3.scale.linear()
.domain([0, 1])
.range([200, 100]);
var force = d3.layout.force()
.size([width, height])
.charge(-200)
.linkStrength(function (d) { return d.weight; })
.linkDistance(function (d) { return dist(d.weight); });
function partitionByAssocation(nodes, targetNode) {
var key = targetNode.short;
var hasAssociation = function (pref) {
return pref.party === key;
};
return _.partition(nodes, function (node) {
return node.short === key || _.some(node.outPrefs, hasAssociation) || _.some(node.inPrefs, hasAssociation);
});
}
function partyComparitor(targetParty) {
var findWeight = function (prefs) {
return (_.findWhere(prefs, {party: targetParty}) || {}).weight || 0;
};
var getNodeWeight = function (node) {
return findWeight(node.inPrefs) + findWeight(node.outPrefs);
}
return function (a, b) {
if (a.short === targetParty) return -1;
if (b.short === targetParty) return 1;
return getNodeWeight(b) - getNodeWeight(a);
}
}
function sortByProximity(nodes, targetParty) {
var splitNodes = partitionByAssocation(nodes, nodeMap.get(targetParty));
var ret = splitNodes[0].sort(partyComparitor(targetParty));
return ret.concat(splitNodes[1]);
}
var nodeMap = d3.map();
d3.json('data.json', function (err, rawData) {
if (err) throw err;
var links = [];
var len = Object.keys(rawData.parties).length;
// Map party names to node objects
var nodes = Object.keys(rawData.parties).map(function (key, i) {
var node = {
short: key,
long: rawData.parties[key],
x: i / len * width,
y: Math.random() * height,
outPrefs: [],
inPrefs: []
};
nodeMap.set(key, node);
return node;
});
// Create link objects for each defined preference
Object.keys(rawData.states.nsw).forEach(function (partyGroup) {
partyGroup.split('/').forEach(function (party) {
links.push(rawData.states.nsw[partyGroup].map(function (prefGroup, i) {
return prefGroup.split('/').map(function (pref) {
var source = nodeMap.get(party);
var target = nodeMap.get(pref);
var weight = (10 - i) / 10;
source.outPrefs.push({party: pref, order: i, weight: weight});
target.inPrefs.push({party: party, order: i, weight: weight});
return {
source: source,
target: target,
weight: weight
};
});
}));
});
});
// Flatten links to single array
var flatten = function (arr) {
return arr.reduce(function (list, item) {
return list.concat(item.reduce ? flatten(item) : item);
}, []);
};
links = flatten(links);
// Sort nodes by connections to known left-wing party, for initial placement
nodes = sortByProximity(nodes, rawData.knownLeft);
nodes.forEach(function (node, i) {
node.x = i / len * width;
});
// Do force layout
force.nodes(nodes)
.links(links)
.start();
// Render stuff
var link = svg.selectAll('.link')
.data(links)
.enter().append('path')
.attr('class', 'link');
var node = svg.selectAll('.node')
.data(nodes)
.enter().append('g')
.attr('class', 'node')
.call(force.drag().on('dragstart', function (d) {
d.fixed = true;
}));
node.append('circle')
.attr('r', 20)
.style('fill', function (d, i) { return 'hsl(' + (i * 300 / len) + ', 50%, 50%)'; });
node.append('text')
.attr('class', 'party-text')
.attr('dy', '0.35em')
.text(function (d) { return d.short; });
node.append('title')
.text(function (d) { return d.long; });
force.on('tick', function() {
link.each(positionLink);
node.attr('transform', function(d) { return 'translate(' + [d.x, d.y] + ')'; });
});
});
function positionLink(d) {
var width = d.weight * 5;
var x = d.target.x - d.source.x;
var y = d.target.y - d.source.y;
var dist = Math.sqrt(x * x + y * y);
var angle = Math.atan2(y, x) / Math.PI * 180;
var w2 = width / 2;
var w3 = width * 0.75;
var coordsBase = [
0, w2,
dist, 0,
0, -w2
];
var coordsHead = [
dist - radius, 0,
dist - radius - 10, -w3,
dist - radius - 10, w3
];
d3.select(this)
.attr('d', 'M0,0 L' + coordsBase + 'z M' + coordsHead.slice(0, 2) + 'L' + coordsHead.slice(2) + 'z')
.attr('transform', 'translate(' + [d.source.x, d.source.y] + ') rotate(' + angle + ')');
}
d3.select(self.frameElement).style("height", height + "px");
</script>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment