parliament-svg
Version:
Generate parliament charts as virtual-dom SVG.
156 lines (131 loc) • 4.05 kB
JavaScript
import { s as hastH } from 'hastscript'
import roundTo from 'lodash/round.js'
import sl from 'sainte-lague'
const pi = Math.PI
const round = (x) => roundTo(x, 10)
const seatSum = (o) => {
let result = 0
for (const key in o) result += o[key].seats
return result
}
const merge = (arrays) => {
let result = []
for (const list of arrays) result = result.concat(list)
return result
}
const coords = (r, b) => ({
x: round(r * Math.cos(b / r - pi)),
y: round(r * Math.sin(b / r - pi)),
})
const calculateSeatDistance = (seatCount, numberOfRings, r) => {
const x = (pi * numberOfRings * r) / (seatCount - numberOfRings)
const y = 1 + (pi * (numberOfRings - 1) * numberOfRings / 2) / (seatCount - numberOfRings)
const a = x / y
return a
}
const score = (m, n, r) => Math.abs(calculateSeatDistance(m, n, r) * n / r - (5 / 7))
const calculateNumberOfRings = (seatCount, r) => {
let n = Math.floor(Math.log(seatCount) / Math.log(2)) || 1
let distance = score(seatCount, n, r)
let direction = 0
if (score(seatCount, n + 1, r) < distance) direction = 1
if (score(seatCount, n - 1, r) < distance && n > 1) direction = -1
while (score(seatCount, n + direction, r) < distance && n > 0) {
distance = score(seatCount, n + direction, r)
n += direction
}
return n
}
const nextRing = (rings, ringProgress) => {
let progressQuota, tQuota
for (const index in rings) {
tQuota = round((ringProgress[index] || 0) / rings[index].length)
if (!progressQuota || tQuota < progressQuota) progressQuota = tQuota
}
for (const index in rings) {
tQuota = round((ringProgress[index] || 0) / rings[index].length)
if (tQuota === progressQuota) return index
}
}
const generatePoints = (parliament, r0) => {
const seatCount = seatSum(parliament)
const numberOfRings = calculateNumberOfRings(seatCount, r0)
const seatDistance = calculateSeatDistance(seatCount, numberOfRings, r0)
// calculate ring radii
let rings = []
for (let i = 1; i <= numberOfRings; i++) {
rings[i] = r0 - (i - 1) * seatDistance
}
// calculate seats per ring
// todo: float to int
rings = sl(rings, seatCount)
const points = []
let r, a, point
// build seats
// loop rings
let ring
for (let i = 1; i <= numberOfRings; i++) {
ring = []
// calculate ring-specific radius
r = r0 - (i - 1) * seatDistance
// calculate ring-specific distance
a = (pi * r) / ((rings[i] - 1) || 1)
// loop points
for (let j = 0; j <= rings[i] - 1; j++) {
point = coords(r, j * a)
point.r = 0.4 * seatDistance
ring.push(point)
}
points.push(ring)
}
// fill seats
const ringProgress = Array(points.length).fill(0)
for (const party in parliament) {
for (let i = 0; i < parliament[party].seats; i++) {
ring = nextRing(points, ringProgress)
points[ring][ringProgress[ring]].fill = parliament[party].colour
points[ring][ringProgress[ring]].party = party
ringProgress[ring]++
}
}
return merge(points)
}
const pointToSVG = hFn => point => hFn('circle', {
cx: point.x,
cy: point.y,
r: point.r,
fill: point.fill,
class: point.party,
})
const defaults = {
seatCount: false,
hFunction: hastH,
}
const generate = (parliament, options = {}) => {
const { seatCount, hFunction } = Object.assign({}, defaults, options)
if (typeof seatCount !== 'boolean') throw new Error('`seatCount` option must be a boolean')
if (typeof hFunction !== 'function') throw new Error('`hFunction` option must be a function')
const radius = 20
const points = generatePoints(parliament, radius)
const a = points[0].r / 0.4
const elements = points.map(pointToSVG(hFunction))
if (seatCount) {
elements.push(hFunction('text', {
x: 0,
y: 0,
'text-anchor': 'middle',
style: {
'font-family': 'Helvetica',
'font-size': 0.25 * radius + 'px',
},
class: 'seatNumber',
}, elements.length))
}
const document = hFunction('svg', {
xmlns: 'http://www.w3.org/2000/svg',
viewBox: [-radius - a / 2, -radius - a / 2, 2 * radius + a, radius + a].join(','),
}, elements)
return document
}
export default generate