├── .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 |

parliament-svg

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 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | 190 | 191 | 192 | 193 | 194 | 195 | 196 | 197 | 198 | 199 | 200 | 201 | 202 | 203 | 204 | 205 | 206 | 207 | 208 | 209 | 210 | 211 | 212 | 213 | 214 | 215 | 216 | 217 | 218 | 219 | 220 | 221 | 222 | 223 | 224 | 225 | 226 | 227 | 228 | 229 | 230 | 231 | 232 | 233 | 234 | 235 | 236 | 237 | 238 | 239 | 240 | 241 | 242 | 243 | 244 | 245 | 246 | 247 | 248 | 249 | 250 | 251 | 252 | 253 | 254 | 255 | 256 | 257 | 258 | 259 | 260 | 261 | 262 | 263 | 264 | 265 | 266 | 267 | 268 | 269 | 270 | 271 | 272 | 273 | 274 | 275 | 276 | 277 | 278 | 279 | 280 | 281 | 282 | 283 | 284 | 285 | 286 | 287 | 288 | 289 | 290 | 291 | 292 | 293 | 294 | 295 | 296 | 297 | 298 | 299 | 300 | 301 | 302 | 303 | 304 | 305 | 306 | 307 | 308 | 309 | 310 | 311 | 312 | 313 | 314 | 315 | 316 | 317 | 318 | 319 | 320 | 321 | 322 | 323 | 324 | 325 | 326 | 327 | 328 | 329 | 330 | 331 | 332 | 333 | 334 | 335 | 336 | 337 | 338 | 339 | 340 | 341 | 342 | 343 | 344 | 345 | 346 | 347 | 348 | 349 | 350 | 351 | 352 | 353 | 354 | 355 | 356 | 357 | 358 | 359 | 360 | 361 | 362 | 363 | 364 | 365 | 366 | 367 | 368 | 369 | 370 | 371 | 372 | 373 | 374 | 375 | 376 | 377 | 378 | 379 | 380 | 381 | 382 | 383 | 384 | 385 | 386 | 387 | 388 | 389 | 390 | 391 | 392 | 393 | 394 | 395 | 396 | 397 | 398 | 399 | 400 | 401 | 402 | 403 | 404 | 405 | 406 | 407 | 408 | 409 | 410 | 411 | 412 | 413 | 414 | 415 | 416 | 417 | 418 | 419 | 420 | 421 | 422 | 423 | 424 | 425 | 426 | 427 | 428 | 429 | 430 | 431 | 432 | 433 | 434 | 435 | 436 | 437 | 438 | 439 | 440 | 441 | 442 | 443 | 444 | 445 | 446 | 447 | 448 | 449 | 450 | 451 | 452 | 453 | 454 | 455 | 456 | 457 | 458 | 459 | 460 | 461 | 462 | 463 | 464 | 465 | 466 | 467 | 468 | 469 | 470 | 471 | 472 | 473 | 474 | 475 | 476 | 477 | 478 | 479 | 480 | 481 | 482 | 483 | 484 | 485 | 486 | 487 | 488 | 489 | 490 | 491 | 492 | 493 | 494 | 495 | 496 | 497 | 498 | 499 | 500 | 501 | 502 | 503 | 504 | 505 | 506 | 507 | 508 | 509 | 510 | 511 | 512 | 513 | 514 | 515 | 516 | 517 | 518 | 519 | 520 | 521 | 522 | 523 | 524 | 525 | 526 | 527 | 528 | 529 | 530 | 531 | 532 | 533 | 534 | 535 | 536 | 537 | 538 | 539 | 540 | 541 | 542 | 543 | 544 | 545 | 546 | 547 | 548 | 549 | 550 | 551 | 552 | 553 | 554 | 555 | 556 | 557 | 558 | 559 | 560 | 561 | 562 | 563 | 564 | 565 | 566 | 567 | 568 | 569 | 570 | 571 | 572 | 573 | 574 | 575 | 576 | 577 | 578 | 579 | 580 | 581 | 582 | 583 | 584 | 585 | 586 | 587 | 588 | 589 | 590 | 591 | 592 | 593 | 594 | 595 | 596 | 597 | 598 | 599 | 600 | 601 | 602 | 603 | 604 | 605 | 606 | 607 | 608 | 609 | 610 | 611 | 612 | 613 | 614 | 615 | 616 | 617 | 618 | 619 | 620 | 621 | 622 | 623 | 624 | 625 | 626 | 627 | 628 | 629 | 630 | 631 | 632 | 633 | 634 | -------------------------------------------------------------------------------- /example/seatCount.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | 190 | 191 | 192 | 193 | 194 | 195 | 196 | 197 | 198 | 199 | 200 | 201 | 202 | 203 | 204 | 205 | 206 | 207 | 208 | 209 | 210 | 211 | 212 | 213 | 214 | 215 | 216 | 217 | 218 | 219 | 220 | 221 | 222 | 223 | 224 | 225 | 226 | 227 | 228 | 229 | 230 | 231 | 232 | 233 | 234 | 235 | 236 | 237 | 238 | 239 | 240 | 241 | 242 | 243 | 244 | 245 | 246 | 247 | 248 | 249 | 250 | 251 | 252 | 253 | 254 | 255 | 256 | 257 | 258 | 259 | 260 | 261 | 262 | 263 | 264 | 265 | 266 | 267 | 268 | 269 | 270 | 271 | 272 | 273 | 274 | 275 | 276 | 277 | 278 | 279 | 280 | 281 | 282 | 283 | 284 | 285 | 286 | 287 | 288 | 289 | 290 | 291 | 292 | 293 | 294 | 295 | 296 | 297 | 298 | 299 | 300 | 301 | 302 | 303 | 304 | 305 | 306 | 307 | 308 | 309 | 310 | 311 | 312 | 313 | 314 | 315 | 316 | 317 | 318 | 319 | 320 | 321 | 322 | 323 | 324 | 325 | 326 | 327 | 328 | 329 | 330 | 331 | 332 | 333 | 334 | 335 | 336 | 337 | 338 | 339 | 340 | 341 | 342 | 343 | 344 | 345 | 346 | 347 | 348 | 349 | 350 | 351 | 352 | 353 | 354 | 355 | 356 | 357 | 358 | 359 | 360 | 361 | 362 | 363 | 364 | 365 | 366 | 367 | 368 | 369 | 370 | 371 | 372 | 373 | 374 | 375 | 376 | 377 | 378 | 379 | 380 | 381 | 382 | 383 | 384 | 385 | 386 | 387 | 388 | 389 | 390 | 391 | 392 | 393 | 394 | 395 | 396 | 397 | 398 | 399 | 400 | 401 | 402 | 403 | 404 | 405 | 406 | 407 | 408 | 409 | 410 | 411 | 412 | 413 | 414 | 415 | 416 | 417 | 418 | 419 | 420 | 421 | 422 | 423 | 424 | 425 | 426 | 427 | 428 | 429 | 430 | 431 | 432 | 433 | 434 | 435 | 436 | 437 | 438 | 439 | 440 | 441 | 442 | 443 | 444 | 445 | 446 | 447 | 448 | 449 | 450 | 451 | 452 | 453 | 454 | 455 | 456 | 457 | 458 | 459 | 460 | 461 | 462 | 463 | 464 | 465 | 466 | 467 | 468 | 469 | 470 | 471 | 472 | 473 | 474 | 475 | 476 | 477 | 478 | 479 | 480 | 481 | 482 | 483 | 484 | 485 | 486 | 487 | 488 | 489 | 490 | 491 | 492 | 493 | 494 | 495 | 496 | 497 | 498 | 499 | 500 | 501 | 502 | 503 | 504 | 505 | 506 | 507 | 508 | 509 | 510 | 511 | 512 | 513 | 514 | 515 | 516 | 517 | 518 | 519 | 520 | 521 | 522 | 523 | 524 | 525 | 526 | 527 | 528 | 529 | 530 | 531 | 532 | 533 | 534 | 535 | 536 | 537 | 538 | 539 | 540 | 541 | 542 | 543 | 544 | 545 | 546 | 547 | 548 | 549 | 550 | 551 | 552 | 553 | 554 | 555 | 556 | 557 | 558 | 559 | 560 | 561 | 562 | 563 | 564 | 565 | 566 | 567 | 568 | 569 | 570 | 571 | 572 | 573 | 574 | 575 | 576 | 577 | 578 | 579 | 580 | 581 | 582 | 583 | 584 | 585 | 586 | 587 | 588 | 589 | 590 | 591 | 592 | 593 | 594 | 595 | 596 | 597 | 598 | 599 | 600 | 601 | 602 | 603 | 604 | 605 | 606 | 607 | 608 | 609 | 610 | 611 | 612 | 613 | 614 | 615 | 616 | 617 | 618 | 619 | 620 | 621 | 622 | 623 | 624 | 625 | 626 | 627 | 628 | 629 | 630 | 631 | 632 | 633 | 631 634 | 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 | [![npm version](https://img.shields.io/npm/v/parliament-svg.svg)](https://www.npmjs.com/package/parliament-svg) 8 | [![License](https://img.shields.io/github/license/juliuste/parliament-svg.svg?style=flat)](license) 9 | [![Contact me](https://img.shields.io/badge/contact-email-turquoise)](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 | ![Example: German Bundestag with seat count enabled](https://rawgit.com/juliuste/parliament-svg/main/example/seatCount.svg) 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 | --------------------------------------------------------------------------------