├── .editorconfig
├── .eslintrc.json
├── .gitattributes
├── .github
└── workflows
│ └── ci.yaml
├── .gitignore
├── .npmrc
├── docs
├── index.html
└── index.js
├── example
├── example.js
├── noseatCount.svg
└── seatCount.svg
├── license
├── package.json
├── readme.md
├── src
└── index.js
├── test
├── bundestag-2013-no-seatcount.test.js
├── bundestag-2013-seatcount-virtual-dom.test.js
├── bundestag-2013-seatcount.test.js
├── data
│ ├── bundestag-2013-no-seatcount-virtual-dom.js
│ ├── bundestag-2013-no-seatcount.js
│ ├── bundestag-2013-seatcount.js
│ ├── four-parties-no-seatcount.js
│ ├── four-parties-seatcount.js
│ ├── two-parties-no-seatcount.js
│ └── two-parties-seatcount.js
├── four-parties-no-seatcount.test.js
├── four-parties-seatcount.test.js
├── two-parties-no-seatcount.test.js
├── two-parties-seatcount.test.js
└── util.js
└── webpack.config.js
/.editorconfig:
--------------------------------------------------------------------------------
1 | # editorconfig.org
2 | root = true
3 |
4 | [*]
5 | indent_style = tab
6 | end_of_line = lf
7 | charset = utf-8
8 | trim_trailing_whitespace = true
9 | insert_final_newline = true
10 |
11 | [*.{yml,yaml}]
12 | indent_style = space
13 | indent_size = 2
14 |
--------------------------------------------------------------------------------
/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "standard",
3 | "rules": {
4 | "indent": [
5 | "error",
6 | "tab"
7 | ],
8 | "no-tabs": "off",
9 | "comma-dangle": [
10 | "error",
11 | "always-multiline"
12 | ]
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/.gitattributes:
--------------------------------------------------------------------------------
1 | * text=auto eol=lf
2 |
--------------------------------------------------------------------------------
/.github/workflows/ci.yaml:
--------------------------------------------------------------------------------
1 | name: CI
2 | on:
3 | [push, pull_request]
4 | jobs:
5 | test:
6 | runs-on: ubuntu-latest
7 | strategy:
8 | matrix:
9 | node-version: [14.x, 16.x]
10 | steps:
11 | - uses: actions/checkout@v2
12 | - name: Use Node.js ${{ matrix.node-version }}
13 | uses: actions/setup-node@v1
14 | with:
15 | node-version: ${{ matrix.node-version }}
16 | - run: npm i
17 | - run: npm test
18 | env:
19 | CI: true
20 | build-and-deploy:
21 | runs-on: ubuntu-latest
22 | needs: test
23 | if: github.ref == 'refs/heads/main'
24 | steps:
25 | - name: Checkout main
26 | uses: actions/checkout@v2
27 | - name: Use Node.js 14
28 | uses: actions/setup-node@v1
29 | with:
30 | node-version: 14
31 | - run: npm i
32 | - run: npm run build
33 | - run: touch docs/.nojekyll
34 | - name: Deploy
35 | uses: JamesIves/github-pages-deploy-action@4.1.3
36 | with:
37 | branch: gh-pages
38 | folder: docs
39 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # general
2 | .DS_Store
3 | *.log
4 |
5 | # node-specific
6 | node_modules
7 | package-lock.json
8 | yarn.lock
9 | shrinkwrap.yaml
10 | pnpm-lock.yaml
11 | dist
12 |
13 | /docs/bundle
14 |
--------------------------------------------------------------------------------
/.npmrc:
--------------------------------------------------------------------------------
1 | package-lock=false
2 |
--------------------------------------------------------------------------------
/docs/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | parliament-svg
6 |
7 |
8 |
9 |
10 |
116 |
117 |
118 | Generate parliament charts as virtual-dom SVG.
119 |
120 |
121 |
122 |
123 |
141 |
145 |
146 |
147 |
148 |
149 |
150 |
--------------------------------------------------------------------------------
/docs/index.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 |
3 | import h from 'virtual-hyperscript-svg'
4 | import createElement from 'virtual-dom/create-element.js'
5 | import diff from 'virtual-dom/diff.js'
6 | import patch from 'virtual-dom/patch.js'
7 |
8 | import patterns from '../src/index.js'
9 |
10 | const data = document.querySelector('#demo-data')
11 | const seats = document.querySelector('#demo-seats')
12 |
13 | const render = () => patterns(JSON.parse(data.value), { seatCount: seats.checked, hFunction: h })
14 |
15 | let tree = render()
16 | let root = createElement(tree)
17 | document.querySelector('#demo-target').appendChild(root)
18 |
19 | const rerender = () => {
20 | const tree2 = render()
21 | root = patch(root, diff(tree, tree2))
22 | tree = tree2
23 | }
24 | const callRerender = function () {
25 | return setTimeout(rerender, 5)
26 | }
27 |
28 | data.addEventListener('keydown', function (e) {
29 | // 8 is the keycode for backspace
30 | if (e.keyCode === 8) callRerender()
31 | })
32 | data.addEventListener('keypress', function () {
33 | callRerender()
34 | })
35 | seats.addEventListener('change', rerender)
36 |
--------------------------------------------------------------------------------
/example/example.js:
--------------------------------------------------------------------------------
1 | import { toHtml } from 'hast-util-to-html'
2 | import svgify from '../src/index.js'
3 |
4 | const germanBundestag = {
5 | linke: {
6 | seats: 64,
7 | colour: '#a08',
8 | },
9 | spd: {
10 | seats: 193,
11 | colour: '#e02',
12 | },
13 | gruene: {
14 | seats: 63,
15 | colour: '#0b2',
16 | },
17 | union: {
18 | seats: 311,
19 | colour: '#333',
20 | },
21 | }
22 |
23 | process.stdout.write(toHtml(svgify(germanBundestag, true)))
24 |
--------------------------------------------------------------------------------
/example/noseatCount.svg:
--------------------------------------------------------------------------------
1 |
634 |
--------------------------------------------------------------------------------
/example/seatCount.svg:
--------------------------------------------------------------------------------
1 |
635 |
--------------------------------------------------------------------------------
/license:
--------------------------------------------------------------------------------
1 | Copyright (c) 2021, Julius Tens
2 |
3 | Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted, provided that the above copyright notice and this permission notice appear in all copies.
4 |
5 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
6 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "parliament-svg",
3 | "version": "3.0.0",
4 | "description": "Generate parliament charts as virtual-dom SVG.",
5 | "keywords": [
6 | "chart",
7 | "parlament",
8 | "parliament",
9 | "svg",
10 | "wiki",
11 | "wikipedia"
12 | ],
13 | "homepage": "https://github.com/juliuste/parliament-svg",
14 | "bugs": "https://github.com/juliuste/parliament-svg/issues",
15 | "repository": "juliuste/parliament-svg",
16 | "license": "ISC",
17 | "author": "Julius Tens ",
18 | "files": [
19 | "src/*"
20 | ],
21 | "main": "src/index.js",
22 | "type": "module",
23 | "scripts": {
24 | "build": "webpack",
25 | "check-deps": "depcheck --ignores='webpack-cli' --ignore-dirs='bundle'",
26 | "fix": "npm run lint -- --fix",
27 | "lint": "eslint src test example docs/index.js",
28 | "prepublishOnly": "npm test",
29 | "test": "npm run lint && npm run check-deps && tape test/*.js"
30 | },
31 | "dependencies": {
32 | "hastscript": "^7.0.2",
33 | "lodash": "^4.17.21",
34 | "sainte-lague": "^2.1.1"
35 | },
36 | "devDependencies": {
37 | "depcheck": "^1.4.2",
38 | "eslint": "^7.32.0",
39 | "eslint-config-standard": "^16.0.3",
40 | "eslint-plugin-import": "^2.24.2",
41 | "eslint-plugin-node": "^11.1.0",
42 | "eslint-plugin-promise": "^5.1.0",
43 | "hast-util-to-html": "^8.0.2",
44 | "tape": "^5.3.1",
45 | "virtual-dom": "^2.1.1",
46 | "virtual-hyperscript-svg": "^2.0.0",
47 | "webpack": "^5.56.1",
48 | "webpack-cli": "^4.8.0"
49 | },
50 | "engines": {
51 | "node": ">=14"
52 | }
53 | }
54 |
--------------------------------------------------------------------------------
/readme.md:
--------------------------------------------------------------------------------
1 | # parliament-svg
2 |
3 | Generate parliament charts as **[_hast_](https://github.com/syntax-tree/hast) virtual DOM SVG\***. Design inspired by the [Wikipedia parliament charts](https://github.com/slashme/parliamentdiagram). *Play around with the [__live demo__](https://juliuste.github.io/parliament-svg/)!* For westminster-style parliament charts, see [westminster-svg](https://github.com/juliuste/westminster-svg). If you are using [D3](https://github.com/d3/d3/), you might prefer working with the [d3-parliament](https://github.com/geoffreybr/d3-parliament) module.
4 |
5 | \*Also compatible with other virtual DOM implementations, see the [docs below](#Usage).
6 |
7 | [](https://www.npmjs.com/package/parliament-svg)
8 | [](license)
9 | [](mailto:mail@juliustens.eu)
10 |
11 | ## Installation
12 |
13 | **This package is [ESM only](https://gist.github.com/sindresorhus/a39789f98801d908bbc7ff3ecc99d99c): Node 12+ is needed to use it and it must be `import`ed instead of `require`d.**
14 |
15 | ```shell
16 | npm install --save parliament-svg
17 | ```
18 |
19 | ## Usage
20 |
21 | ```js
22 | import parliamentSVG from 'parliament-svg'
23 |
24 | const virtualSvg = parliamentSVG(parties, [opt])
25 | ```
26 |
27 | - **`opt`** can contain the following options:
28 | - **`seatCount`** is a boolean, if `true` the total seat count will be displayed in the chart. Defaults to `false`.
29 | - **`hFunction`** is a function that will be used to generate the element tree. Defaults to [`hastscript`](https://github.com/syntax-tree/hastscript/)'s `s()` function, custom values need to match that function's signature. You could use [`virtual-hyperscript-svg`](https://github.com/substack/virtual-hyperscript-svg)'s `h()` function here if you prefer working with [`virtual-dom`](https://github.com/Matt-Esch/virtual-dom), for example.
30 | - **`parties`** is an object containing seat count and colour for each party, e.g.:
31 |
32 | ```json
33 | {
34 | "linke": {
35 | "seats": 64,
36 | "colour": "#a08"
37 | },
38 | "spd": {
39 | "seats": 193,
40 | "colour": "#e02"
41 | },
42 | "gruene": {
43 | "seats": 63,
44 | "colour": "#0b2"
45 | },
46 | "union": {
47 | "seats": 311,
48 | "colour": "#333"
49 | }
50 | }
51 | ```
52 | Each seat contains the party name in its `class` attribute.
53 |
54 | For the given `parties` object and `seatCount` enabled, the rendered result should look as follows:
55 |
56 | 
57 |
58 | If you want to convert the [_hast_](https://github.com/syntax-tree/hast) tree to an SVG string, use `hast-util-to-html` (don't get confused by the name, the library can also stringify SVG):
59 |
60 | ```js
61 | import parliamentSVG from 'parliament-svg'
62 | import { toHtml as toSvg } from 'hast-util-to-html'
63 |
64 | const virtualSvg = parliamentSVG(parties, seatCount)
65 | const svg = toSvg(virtualSvg)
66 | ```
67 |
68 | Check the [`code example`](example/example.js) as well.
69 |
70 | ### What if I prefer virtual-dom (or anything else)?
71 |
72 | If you prefer [`virtual-dom`](https://github.com/Matt-Esch/virtual-dom) over `hast`, e.g. for diffing or patching, you can either:
73 | - use [`hast-to-hyperscript`](https://github.com/syntax-tree/hast-to-hyperscript) to transform the tree after it was generated _or_
74 | - use the [`hFunction`](#Usage) parameter documented above with a virtual-dom `h()` function of your choice
75 |
76 | ## See also
77 |
78 | - [westminster-svg](https://github.com/juliuste/westminster-svg) - "westminster-style parliament charts"
79 | - [d3-parliament](https://github.com/geoffreybr/d3-parliament) - "parliament charts for [D3](https://github.com/d3/d3/)"
80 | - [wikidata-parliament-svg](https://github.com/k-nut/wikidata-parliament-svg) - "draws parliament graphs based on data from wikidata"
81 |
82 | ## Contributing
83 |
84 | If you found a bug or want to propose a feature, feel free to visit [the issues page](https://github.com/juliuste/parliament-svg/issues).
85 |
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 |
3 | import { s as hastH } from 'hastscript'
4 | import roundTo from 'lodash/round.js'
5 | import sl from 'sainte-lague'
6 |
7 | const pi = Math.PI
8 |
9 | const round = (x) => roundTo(x, 10)
10 | const seatSum = (o) => {
11 | let result = 0
12 | for (const key in o) result += o[key].seats
13 | return result
14 | }
15 | const merge = (arrays) => {
16 | let result = []
17 | for (const list of arrays) result = result.concat(list)
18 | return result
19 | }
20 |
21 | const coords = (r, b) => ({
22 | x: round(r * Math.cos(b / r - pi)),
23 | y: round(r * Math.sin(b / r - pi)),
24 | })
25 |
26 | const calculateSeatDistance = (seatCount, numberOfRings, r) => {
27 | const x = (pi * numberOfRings * r) / (seatCount - numberOfRings)
28 | const y = 1 + (pi * (numberOfRings - 1) * numberOfRings / 2) / (seatCount - numberOfRings)
29 |
30 | const a = x / y
31 | return a
32 | }
33 |
34 | const score = (m, n, r) => Math.abs(calculateSeatDistance(m, n, r) * n / r - (5 / 7))
35 |
36 | const calculateNumberOfRings = (seatCount, r) => {
37 | let n = Math.floor(Math.log(seatCount) / Math.log(2)) || 1
38 | let distance = score(seatCount, n, r)
39 |
40 | let direction = 0
41 | if (score(seatCount, n + 1, r) < distance) direction = 1
42 | if (score(seatCount, n - 1, r) < distance && n > 1) direction = -1
43 |
44 | while (score(seatCount, n + direction, r) < distance && n > 0) {
45 | distance = score(seatCount, n + direction, r)
46 | n += direction
47 | }
48 | return n
49 | }
50 |
51 | const nextRing = (rings, ringProgress) => {
52 | let progressQuota, tQuota
53 | for (const index in rings) {
54 | tQuota = round((ringProgress[index] || 0) / rings[index].length)
55 | if (!progressQuota || tQuota < progressQuota) progressQuota = tQuota
56 | }
57 | for (const index in rings) {
58 | tQuota = round((ringProgress[index] || 0) / rings[index].length)
59 | if (tQuota === progressQuota) return index
60 | }
61 | }
62 |
63 | const generatePoints = (parliament, r0) => {
64 | const seatCount = seatSum(parliament)
65 | const numberOfRings = calculateNumberOfRings(seatCount, r0)
66 | const seatDistance = calculateSeatDistance(seatCount, numberOfRings, r0)
67 |
68 | // calculate ring radii
69 | let rings = []
70 | for (let i = 1; i <= numberOfRings; i++) {
71 | rings[i] = r0 - (i - 1) * seatDistance
72 | }
73 |
74 | // calculate seats per ring
75 | // todo: float to int
76 | rings = sl(rings, seatCount)
77 |
78 | const points = []
79 | let r, a, point
80 |
81 | // build seats
82 | // loop rings
83 | let ring
84 | for (let i = 1; i <= numberOfRings; i++) {
85 | ring = []
86 | // calculate ring-specific radius
87 | r = r0 - (i - 1) * seatDistance
88 | // calculate ring-specific distance
89 | a = (pi * r) / ((rings[i] - 1) || 1)
90 |
91 | // loop points
92 | for (let j = 0; j <= rings[i] - 1; j++) {
93 | point = coords(r, j * a)
94 | point.r = 0.4 * seatDistance
95 | ring.push(point)
96 | }
97 | points.push(ring)
98 | }
99 |
100 | // fill seats
101 | const ringProgress = Array(points.length).fill(0)
102 | for (const party in parliament) {
103 | for (let i = 0; i < parliament[party].seats; i++) {
104 | ring = nextRing(points, ringProgress)
105 | points[ring][ringProgress[ring]].fill = parliament[party].colour
106 | points[ring][ringProgress[ring]].party = party
107 | ringProgress[ring]++
108 | }
109 | }
110 |
111 | return merge(points)
112 | }
113 |
114 | const pointToSVG = hFn => point => hFn('circle', {
115 | cx: point.x,
116 | cy: point.y,
117 | r: point.r,
118 | fill: point.fill,
119 | class: point.party,
120 | })
121 |
122 | const defaults = {
123 | seatCount: false,
124 | hFunction: hastH,
125 | }
126 |
127 | const generate = (parliament, options = {}) => {
128 | const { seatCount, hFunction } = Object.assign({}, defaults, options)
129 | if (typeof seatCount !== 'boolean') throw new Error('`seatCount` option must be a boolean')
130 | if (typeof hFunction !== 'function') throw new Error('`hFunction` option must be a function')
131 |
132 | const radius = 20
133 | const points = generatePoints(parliament, radius)
134 | const a = points[0].r / 0.4
135 | const elements = points.map(pointToSVG(hFunction))
136 | if (seatCount) {
137 | elements.push(hFunction('text', {
138 | x: 0,
139 | y: 0,
140 | 'text-anchor': 'middle',
141 | style: {
142 | 'font-family': 'Helvetica',
143 | 'font-size': 0.25 * radius + 'px',
144 | },
145 | class: 'seatNumber',
146 | }, elements.length))
147 | }
148 | const document = hFunction('svg', {
149 | xmlns: 'http://www.w3.org/2000/svg',
150 | viewBox: [-radius - a / 2, -radius - a / 2, 2 * radius + a, radius + a].join(','),
151 | }, elements)
152 | return document
153 | }
154 |
155 | export default generate
156 |
--------------------------------------------------------------------------------
/test/bundestag-2013-no-seatcount.test.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 |
3 | import chart from '../src/index.js'
4 | import tape from 'tape'
5 | import { normalize as n } from './util.js'
6 |
7 | import expected from './data/bundestag-2013-no-seatcount.js'
8 |
9 | const parliament = {
10 | linke: {
11 | seats: 64,
12 | colour: '#a08',
13 | },
14 | spd: {
15 | seats: 193,
16 | colour: '#e02',
17 | },
18 | gruene: {
19 | seats: 63,
20 | colour: '#0b2',
21 | },
22 | union: {
23 | seats: 311,
24 | colour: '#333',
25 | },
26 | }
27 |
28 | tape('Bundestag federal election results from 2013, seatCount not set (defaults to false)', t => {
29 | const svg = chart(parliament)
30 | t.deepEqual(n(svg), n(expected), 'Generated virtual dom SVG and expected output are the same')
31 | t.end()
32 | })
33 |
--------------------------------------------------------------------------------
/test/bundestag-2013-seatcount-virtual-dom.test.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 |
3 | import hFunction from 'virtual-hyperscript-svg'
4 | import chart from '../src/index.js'
5 | import tape from 'tape'
6 | import { normalize as n } from './util.js'
7 |
8 | import expected from './data/bundestag-2013-no-seatcount-virtual-dom.js'
9 |
10 | const parliament = {
11 | linke: {
12 | seats: 64,
13 | colour: '#a08',
14 | },
15 | spd: {
16 | seats: 193,
17 | colour: '#e02',
18 | },
19 | gruene: {
20 | seats: 63,
21 | colour: '#0b2',
22 | },
23 | union: {
24 | seats: 311,
25 | colour: '#333',
26 | },
27 | }
28 |
29 | tape('Bundestag federal election results from 2013, seatCount not set (defaults to false), custom h function', t => {
30 | const svg = chart(parliament, { hFunction })
31 | t.deepEqual(n(svg), n(expected), 'Generated virtual dom SVG and expected output are the same')
32 | t.end()
33 | })
34 |
--------------------------------------------------------------------------------
/test/bundestag-2013-seatcount.test.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 |
3 | import chart from '../src/index.js'
4 | import tape from 'tape'
5 | import { normalize as n } from './util.js'
6 |
7 | import expected from './data/bundestag-2013-seatcount.js'
8 |
9 | const parliament = {
10 | linke: {
11 | seats: 64,
12 | colour: '#a08',
13 | },
14 | spd: {
15 | seats: 193,
16 | colour: '#e02',
17 | },
18 | gruene: {
19 | seats: 63,
20 | colour: '#0b2',
21 | },
22 | union: {
23 | seats: 311,
24 | colour: '#333',
25 | },
26 | }
27 | const seatCount = true
28 |
29 | tape('Bundestag federal election results from 2013, seatCount true', t => {
30 | const svg = chart(parliament, { seatCount })
31 | t.deepEqual(n(svg), n(expected), 'Generated virtual dom SVG and expected output are the same')
32 | t.end()
33 | })
34 |
--------------------------------------------------------------------------------
/test/data/four-parties-no-seatcount.js:
--------------------------------------------------------------------------------
1 | export default {
2 | type: 'element',
3 | tagName: 'svg',
4 | properties: {
5 | xmlns: 'http://www.w3.org/2000/svg',
6 | viewBox: '-30.47197551196598,-30.47197551196598,60.94395102393196,40.94395102393196',
7 | },
8 | children: [
9 | {
10 | type: 'element',
11 | tagName: 'circle',
12 | properties: {
13 | cx: -20,
14 | cy: 0,
15 | r: 8.377580409572783,
16 | fill: '#000',
17 | className: [
18 | 'party1',
19 | ],
20 | },
21 | children: [],
22 | },
23 | {
24 | type: 'element',
25 | tagName: 'circle',
26 | properties: {
27 | cx: -10,
28 | cy: -17.3205080757,
29 | r: 8.377580409572783,
30 | fill: '#fff',
31 | className: [
32 | 'party2',
33 | ],
34 | },
35 | children: [],
36 | },
37 | {
38 | type: 'element',
39 | tagName: 'circle',
40 | properties: {
41 | cx: 10,
42 | cy: -17.3205080757,
43 | r: 8.377580409572783,
44 | fill: '#abc',
45 | className: [
46 | 'party3',
47 | ],
48 | },
49 | children: [],
50 | },
51 | {
52 | type: 'element',
53 | tagName: 'circle',
54 | properties: {
55 | cx: 20,
56 | cy: 0,
57 | r: 8.377580409572783,
58 | fill: '#def',
59 | className: [
60 | 'party4',
61 | ],
62 | },
63 | children: [],
64 | },
65 | ],
66 | }
67 |
--------------------------------------------------------------------------------
/test/data/four-parties-seatcount.js:
--------------------------------------------------------------------------------
1 | export default {
2 | type: 'element',
3 | tagName: 'svg',
4 | properties: {
5 | xmlns: 'http://www.w3.org/2000/svg',
6 | viewBox: '-30.47197551196598,-30.47197551196598,60.94395102393196,40.94395102393196',
7 | },
8 | children: [
9 | {
10 | type: 'element',
11 | tagName: 'circle',
12 | properties: {
13 | cx: -20,
14 | cy: 0,
15 | r: 8.377580409572783,
16 | fill: '#000',
17 | className: [
18 | 'party1',
19 | ],
20 | },
21 | children: [],
22 | },
23 | {
24 | type: 'element',
25 | tagName: 'circle',
26 | properties: {
27 | cx: -10,
28 | cy: -17.3205080757,
29 | r: 8.377580409572783,
30 | fill: '#fff',
31 | className: [
32 | 'party2',
33 | ],
34 | },
35 | children: [],
36 | },
37 | {
38 | type: 'element',
39 | tagName: 'circle',
40 | properties: {
41 | cx: 10,
42 | cy: -17.3205080757,
43 | r: 8.377580409572783,
44 | fill: '#abc',
45 | className: [
46 | 'party3',
47 | ],
48 | },
49 | children: [],
50 | },
51 | {
52 | type: 'element',
53 | tagName: 'circle',
54 | properties: {
55 | cx: 20,
56 | cy: 0,
57 | r: 8.377580409572783,
58 | fill: '#def',
59 | className: [
60 | 'party4',
61 | ],
62 | },
63 | children: [],
64 | },
65 | {
66 | type: 'element',
67 | tagName: 'text',
68 | properties: {
69 | x: 0,
70 | y: 0,
71 | textAnchor: 'middle',
72 | style: 'font-family: Helvetica; font-size: 5px',
73 | className: [
74 | 'seatNumber',
75 | ],
76 | },
77 | children: [
78 | {
79 | type: 'text',
80 | value: '4',
81 | },
82 | ],
83 | },
84 | ],
85 | }
86 |
--------------------------------------------------------------------------------
/test/data/two-parties-no-seatcount.js:
--------------------------------------------------------------------------------
1 | export default {
2 | type: 'element',
3 | tagName: 'svg',
4 | properties: {
5 | xmlns: 'http://www.w3.org/2000/svg',
6 | viewBox: '-51.41592653589793,-51.41592653589793,102.83185307179586,82.83185307179586',
7 | },
8 | children: [
9 | {
10 | type: 'element',
11 | tagName: 'circle',
12 | properties: {
13 | cx: -20,
14 | cy: 0,
15 | r: 25.132741228718345,
16 | fill: '#000',
17 | className: [
18 | 'party1',
19 | ],
20 | },
21 | children: [],
22 | },
23 | {
24 | type: 'element',
25 | tagName: 'circle',
26 | properties: {
27 | cx: 20,
28 | cy: 0,
29 | r: 25.132741228718345,
30 | fill: '#fff',
31 | className: [
32 | 'party2',
33 | ],
34 | },
35 | children: [],
36 | },
37 | ],
38 | }
39 |
--------------------------------------------------------------------------------
/test/data/two-parties-seatcount.js:
--------------------------------------------------------------------------------
1 | export default {
2 | type: 'element',
3 | tagName: 'svg',
4 | properties: {
5 | xmlns: 'http://www.w3.org/2000/svg',
6 | viewBox: '-51.41592653589793,-51.41592653589793,102.83185307179586,82.83185307179586',
7 | },
8 | children: [
9 | {
10 | type: 'element',
11 | tagName: 'circle',
12 | properties: {
13 | cx: -20,
14 | cy: 0,
15 | r: 25.132741228718345,
16 | fill: '#000',
17 | className: [
18 | 'party1',
19 | ],
20 | },
21 | children: [],
22 | },
23 | {
24 | type: 'element',
25 | tagName: 'circle',
26 | properties: {
27 | cx: 20,
28 | cy: 0,
29 | r: 25.132741228718345,
30 | fill: '#fff',
31 | className: [
32 | 'party2',
33 | ],
34 | },
35 | children: [],
36 | },
37 | {
38 | type: 'element',
39 | tagName: 'text',
40 | properties: {
41 | x: 0,
42 | y: 0,
43 | textAnchor: 'middle',
44 | style: 'font-family: Helvetica; font-size: 5px',
45 | className: [
46 | 'seatNumber',
47 | ],
48 | },
49 | children: [
50 | {
51 | type: 'text',
52 | value: '2',
53 | },
54 | ],
55 | },
56 | ],
57 | }
58 |
--------------------------------------------------------------------------------
/test/four-parties-no-seatcount.test.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 |
3 | import chart from '../src/index.js'
4 | import tape from 'tape'
5 | import { normalize as n } from './util.js'
6 |
7 | import expected from './data/four-parties-no-seatcount.js'
8 |
9 | const parliament = {
10 | party1: {
11 | seats: 1,
12 | colour: '#000',
13 | },
14 | party2: {
15 | seats: 1,
16 | colour: '#fff',
17 | },
18 | party3: {
19 | seats: 1,
20 | colour: '#abc',
21 | },
22 | party4: {
23 | seats: 1,
24 | colour: '#def',
25 | },
26 | }
27 | const seatCount = false
28 |
29 | tape('Four parties with 1 seat each, seatCount false', t => {
30 | const svg = chart(parliament, { seatCount })
31 | t.deepEqual(n(svg), n(expected), 'Generated virtual dom SVG and expected output are the same')
32 | t.end()
33 | })
34 |
--------------------------------------------------------------------------------
/test/four-parties-seatcount.test.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 |
3 | import chart from '../src/index.js'
4 | import tape from 'tape'
5 | import { normalize as n } from './util.js'
6 |
7 | import expected from './data/four-parties-seatcount.js'
8 |
9 | const parliament = {
10 | party1: {
11 | seats: 1,
12 | colour: '#000',
13 | },
14 | party2: {
15 | seats: 1,
16 | colour: '#fff',
17 | },
18 | party3: {
19 | seats: 1,
20 | colour: '#abc',
21 | },
22 | party4: {
23 | seats: 1,
24 | colour: '#def',
25 | },
26 | }
27 | const seatCount = true
28 |
29 | tape('Four parties with 1 seat each, seatCount true', t => {
30 | const svg = chart(parliament, { seatCount })
31 | t.deepEqual(n(svg), n(expected), 'Generated virtual dom SVG and expected output are the same')
32 | t.end()
33 | })
34 |
--------------------------------------------------------------------------------
/test/two-parties-no-seatcount.test.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 |
3 | import chart from '../src/index.js'
4 | import tape from 'tape'
5 | import { normalize as n } from './util.js'
6 |
7 | import expected from './data/two-parties-no-seatcount.js'
8 |
9 | const parliament = {
10 | party1: {
11 | seats: 1,
12 | colour: '#000',
13 | },
14 | party2: {
15 | seats: 1,
16 | colour: '#fff',
17 | },
18 | }
19 | const seatCount = false
20 |
21 | tape('Two parties with 1 seat each, seatCount false', t => {
22 | const svg = chart(parliament, { seatCount })
23 | t.deepEqual(n(svg), n(expected), 'Generated virtual dom SVG and expected output are the same')
24 | t.end()
25 | })
26 |
--------------------------------------------------------------------------------
/test/two-parties-seatcount.test.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 |
3 | import chart from '../src/index.js'
4 | import tape from 'tape'
5 | import { normalize as n } from './util.js'
6 |
7 | import expected from './data/two-parties-seatcount.js'
8 |
9 | const parliament = {
10 | party1: {
11 | seats: 1,
12 | colour: '#000',
13 | },
14 | party2: {
15 | seats: 1,
16 | colour: '#fff',
17 | },
18 | }
19 | const seatCount = true
20 |
21 | tape('Two parties with 1 seat each, seatCount true', t => {
22 | const svg = chart(parliament, { seatCount })
23 | t.deepEqual(n(svg), n(expected), 'Generated virtual dom SVG and expected output are the same')
24 | t.end()
25 | })
26 |
--------------------------------------------------------------------------------
/test/util.js:
--------------------------------------------------------------------------------
1 | export const normalize = x => JSON.parse(JSON.stringify(x))
2 |
--------------------------------------------------------------------------------
/webpack.config.js:
--------------------------------------------------------------------------------
1 | import { dirname, resolve } from 'path'
2 | import { fileURLToPath } from 'url'
3 |
4 | export default {
5 | mode: 'production',
6 | entry: './docs/index.js',
7 | output: {
8 | path: resolve(dirname(fileURLToPath(import.meta.url)), 'docs/bundle'),
9 | filename: 'index.js',
10 | },
11 | }
12 |
--------------------------------------------------------------------------------