Public
Edited
Jan 7
2 stars
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Plot.plot({
y: {
tickFormat: "%"
},
marks: [
Plot.barY(data, {x: "election", y: "voteShare", fill: "party", stroke: "grey"}),
Plot.ruleY([0])
],
width: width,
x: {
paddingInner: 0,
},
color: {
type: "categorical",
domain: partyColor.domain(),
range: partyColor.range().map(c => d3.color(c).copy({opacity: 0.8}))
}
})
Insert cell
{
const formatPc = d3.format(".1%")
return Plot.plot({
marks: [
Plot.dot(data, {x: "voteShare", y: "seatShare", fill: "party"}),
Plot.tip(data, Plot.pointer({
x: "voteShare",
y: "seatShare",
title: d => `${d.election}: ${d.party}\nVote Share: ${formatPc(d.voteShare)}\nSeat Share: ${formatPc(d.seatShare)}`}
)),
],
color: {domain: partyColor.domain(), range: partyColor.range()},
x: {tickFormat: ".0%"},
y: {tickFormat: ".0%"}
})
}
Insert cell
voteWinner = currentElection.sort((a, b) => b.votes - a.votes)[0]
Insert cell
seatWinner = currentElection.sort((a, b) => b.seats - a.seats)[0]
Insert cell
Insert cell
Insert cell
nodes = {
const nodes = [];
for (let i = 0; i < 100; ++i) {
// Add nodes representing % of popular vote
nodes.push({cx: width/2, cy: height/3, r: width/200, party: undefined, group: 0})
// Add node representing % of seats in parliament
nodes.push({cx: width/2, cy: 2 * height/3, r: width/200, party: undefined, group: 1})
}
return nodes;
}
Insert cell
sim = {
const sim = d3.forceSimulation()
.force('collide', d3.forceCollide().radius(d => d.r).strength(1).iterations(10))
.force('x', d3.forceX().x(d => d.cx).strength(0.25))
.force('y', d3.forceY().y(d => d.cy).strength(0.25))
.alpha(.2)
.alphaDecay(0)
.stop();

sim.nodes(nodes);
return sim;
}
Insert cell
function reassignParties(election) {
// Update the simulation to reflect a given election year.
const nodes = sim.nodes();


function reassignNodes(nodes, shares) {
const pool = [];
var sumShares = shares.reduce((acc, cur) => acc + cur[1], 0);
shares = shares
.map(([party, share]) => [party, Math.round(share * nodes.length / sumShares)]) // Express in terms of node size.
.sort((a, b) => b[1] - a[1]); // Largest first.
sumShares = shares.reduce((acc, cur) => acc + cur[1], 0);
// Fix issues caused by rounding to the nearest whole number by adjusting the size of the largest group.
// Since this affects it's proportional size the least.
if (sumShares < nodes.length) shares[0][1] += 1;
if (sumShares > nodes.length) shares[0][1] -= 1;
var fixed = shares.reduce((acc, cur) => acc + cur[1], 0)
// Loop 1: Pool Remainder
shares.forEach(([party, share]) => {
let count = 0;
nodes.forEach(n => {
if (n.party === party) count += 1;
// Shift to the next party if share is reached.
if (count > share) n.party = undefined;
})
})
// Loop 2: Distribute pool
shares.forEach(([party, share]) => {
let count = nodes.filter(n => n.party === party).length;
//const pool = nodes.filter(n => n.party === undefined).length;
//console.log(`Remainder ${share - count} pool size ${pool}`)
for (let i = 0; i < nodes.length && count < share; ++i) {
if (nodes[i].party === undefined) {
nodes[i].party = party;
nodes[i].cx = partyX(party)
count += 1;
}
}
})
return nodes;
}

// Resassign each group separately.
const vs = currentElection.map(e => [e.party, e.voteShare]);
const ss = currentElection.map(e => [e.party, e.seats]);
const totalSeats = currentElection[0].totalSeats;
const newNodes = [
... reassignNodes(nodes.filter(n => n.group === 0), vs),
... reassignNodes(nodes.filter(n => n.group === 1), ss)
]
sim.nodes(newNodes);
}
Insert cell
Insert cell
general-elections-and-governments - 3. GE results UK & GB (1).csv
SELECT
*,
CAST(seats as FLOAT)/CAST(totalSeats as FLOAT) as seatShare
FROM (
SELECT
Election as election,
Party as party,
IFNULL( CAST(REPLACE("Votes",',','') as INT), 0) as votes,
IFNULL( CAST(REPLACE("Total Votes",',','') as INT), 0) as totalVotes,
IFNULL( CAST(REPLACE("Vote share",'%','') as FLOAT) / 100, 0) as voteShare,
IFNULL( CAST(REPLACE("Seats",',','') as INT), 0) as seats,
IFNULL( CAST(REPLACE("Total seats",',','') as INT), 0) as totalSeats,
FROM "general-elections-and-governments - 3. GE results UK & GB (1)"
WHERE Country = 'UK'
)
Insert cell
data
select distinct Party from data
Insert cell
parties = sqlParties.map(p => p.party)
Insert cell
data
select distinct Election from data
Insert cell
elections = sqlElections.map(y => y.election)
Insert cell
currentElection = data.filter(d => d.election === year)
Insert cell
Insert cell
Insert cell
height = width / 2
Insert cell
margin = ({top: 10, right: 100, bottom: 10, left: 100})
Insert cell
Insert cell
partyX = d3.scalePoint().domain(["CON", "LAB", "LD", "PC/SNP", "Other"]).range([margin.left, width - margin.right]);
Insert cell
partyColor = d3.scaleOrdinal().domain(["CON", "LAB", "LD", "PC/SNP", "Other"]).range(["#0087DC","#E4003B","#FAA61A","#005B54","darkgrey"]).unknown("pink")
Insert cell
circleSize = d3.scaleSqrt().domain([0, 1]).range([height / 60, height / 8])
Insert cell
percent = d3.format('.2%')
Insert cell

Purpose-built for displays of data

Observable is your go-to platform for exploring data and creating expressive data visualizations. Use reactive JavaScript notebooks for prototyping and a collaborative canvas for visual data exploration and dashboard creation.
Learn more