├── src ├── styles │ ├── index.css │ └── buttons.css ├── MenuSejm.tsx ├── MenuInformacje.tsx ├── MenuSenat.tsx ├── index.tsx ├── App.tsx ├── Informacje.tsx ├── Senat.tsx ├── Sejm.tsx ├── Visualization.tsx ├── Table.tsx ├── ProgressBar.tsx ├── Info.tsx ├── App.css ├── utils.ts ├── Chart.tsx ├── assets │ ├── SejmMap copy.jsx │ └── SejmMap.svg └── SejmMap.jsx ├── .gitignore ├── public ├── robots.txt ├── manifest.json └── index.html ├── tsconfig.json ├── package.json └── README.md /src/styles/index.css: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | build -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | 6 | ], 7 | "start_url": ".", 8 | "display": "standalone", 9 | "theme_color": "#000000", 10 | "background_color": "#ffffff" 11 | } 12 | -------------------------------------------------------------------------------- /src/MenuSejm.tsx: -------------------------------------------------------------------------------- 1 | function MenuSejm() { 2 | return ( 3 | 10 | ) 11 | } 12 | 13 | export default MenuSejm; -------------------------------------------------------------------------------- /src/MenuInformacje.tsx: -------------------------------------------------------------------------------- 1 | function MenuInformacje() { 2 | return ( 3 | 10 | ) 11 | } 12 | 13 | export default MenuInformacje; -------------------------------------------------------------------------------- /src/MenuSenat.tsx: -------------------------------------------------------------------------------- 1 | // TODO - duplicate menu is bad 2 | 3 | function MenuSenat() { 4 | return ( 5 | 12 | ) 13 | } 14 | 15 | export default MenuSenat; -------------------------------------------------------------------------------- /src/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom/client'; 3 | import './styles/index.css'; 4 | import App from './App'; 5 | import { HashRouter } from 'react-router-dom'; 6 | 7 | const root = ReactDOM.createRoot( 8 | document.getElementById('root') as HTMLElement 9 | ); 10 | 11 | root.render( 12 | 13 | 14 | 15 | 16 | 17 | ); -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": [ 5 | "dom", 6 | "dom.iterable", 7 | "esnext" 8 | ], 9 | "allowJs": true, 10 | "skipLibCheck": true, 11 | "esModuleInterop": true, 12 | "allowSyntheticDefaultImports": true, 13 | "strict": true, 14 | "forceConsistentCasingInFileNames": true, 15 | "noFallthroughCasesInSwitch": true, 16 | "module": "esnext", 17 | "moduleResolution": "node", 18 | "resolveJsonModule": true, 19 | "isolatedModules": true, 20 | "noEmit": true, 21 | "jsx": "react-jsx" 22 | }, 23 | "include": [ 24 | "src" 25 | ] 26 | } 27 | -------------------------------------------------------------------------------- /src/App.tsx: -------------------------------------------------------------------------------- 1 | import './App.css'; 2 | import Menu from './MenuSejm'; 3 | import { Routes, Route } from 'react-router-dom'; 4 | import Sejm from './Sejm'; 5 | import Senat from './Senat'; 6 | import Informacje from './Informacje'; 7 | import './styles/buttons.css'; 8 | 9 | function App() { 10 | return ( 11 |
12 | 13 | } /> 14 | } /> 15 | } /> 16 | } /> 17 | 18 |
19 | ); 20 | } 21 | 22 | export default App; 23 | -------------------------------------------------------------------------------- /src/Informacje.tsx: -------------------------------------------------------------------------------- 1 | import MenuInformacje from "./MenuInformacje"; 2 | 3 | function Informacje() { 4 | return ( 5 |
6 | 7 |
8 | Wyniki są na bieżąco (co ok. 15 minut) pobierane z PKW, następnie przeliczane na mandaty, które są przydzielane do 1 z 4 kategorii: 9 |
    10 |
  • Tossup: Niby ten mandat obecnie do kogoś należy, ale nie należy się do tego przywiązywać, trwa o niego zacięta walka
  • 11 |
  • Leaning: Partia obecnie posiadająca ten mandat może mieć o 10% niższe poparcie w pozostałych komisjach i dalej go utrzyma
  • 12 |
  • Likely: Partia obecnie posiadająca ten mandat może mieć o 25% niższe poparcie w pozostałych komisjach i dalej go utrzyma
  • 13 |
  • Safe: Partia obecnie posiadająca ten mandat może mieć o 40% niższe poparcie w pozostałych komisjach i dalej go utrzyma
  • 14 |
15 | 16 | 19 |
20 |
21 | ); 22 | } 23 | 24 | export default Informacje; 25 | -------------------------------------------------------------------------------- /src/Senat.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from 'react'; 2 | import { Chart } from './Chart'; 3 | import MenuSenat from './MenuSenat'; 4 | import SenatMap from './SenatMap'; 5 | import { createDefaultSenatInfo } from './Info'; 6 | import { BASE_URL } from './utils'; 7 | 8 | function Senat() { 9 | const [resultsData, setResultsData] = useState([]); 10 | 11 | useEffect(() => { 12 | fetch(`${BASE_URL}/results/senat/detailed`) 13 | .then(response => { 14 | return response.json() 15 | }) 16 | .then(data => { 17 | setResultsData(data) 18 | createDefaultSenatInfo() 19 | }) 20 | }, []); 21 | 22 | return ( 23 |
24 | 25 |
26 | 27 |
28 |
29 | 30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 | 39 | ) 40 | } 41 | 42 | export default Senat; -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "client", 3 | "version": "0.1.0", 4 | "homepage": "https://mkostyk.github.io/wybory2023-client/#/", 5 | "private": true, 6 | "dependencies": { 7 | "@testing-library/jest-dom": "^5.17.0", 8 | "@testing-library/react": "^13.4.0", 9 | "@testing-library/user-event": "^13.5.0", 10 | "@types/jest": "^27.5.2", 11 | "@types/node": "^16.18.55", 12 | "@types/react": "^18.2.23", 13 | "@types/react-dom": "^18.2.8", 14 | "chart.js": "^4.4.0", 15 | "faker": "^6.6.6", 16 | "react": "^18.2.0", 17 | "react-chartjs-2": "^5.2.0", 18 | "react-dom": "^18.2.0", 19 | "react-router-dom": "^6.16.0", 20 | "react-scripts": "^5.0.1", 21 | "typescript": "^4.9.5", 22 | "web-vitals": "^2.1.4" 23 | }, 24 | "scripts": { 25 | "predeploy": "npm run build", 26 | "deploy": "gh-pages -d build", 27 | "start": "react-scripts start", 28 | "build": "react-scripts build", 29 | "test": "react-scripts test", 30 | "eject": "react-scripts eject" 31 | }, 32 | "eslintConfig": { 33 | "extends": [ 34 | "react-app", 35 | "react-app/jest" 36 | ] 37 | }, 38 | "browserslist": { 39 | "production": [ 40 | ">0.2%", 41 | "not dead", 42 | "not op_mini all" 43 | ], 44 | "development": [ 45 | "last 1 chrome version", 46 | "last 1 firefox version", 47 | "last 1 safari version" 48 | ] 49 | }, 50 | "devDependencies": { 51 | "@faker-js/faker": "^8.1.0", 52 | "gh-pages": "^6.0.0" 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/Sejm.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from 'react'; 2 | import { Chart } from './Chart'; 3 | import SejmMap from './SejmMap'; 4 | import { FullData } from './utils'; 5 | import { createDefaultSejmInfo } from './Info'; 6 | import MenuSejm from './MenuSejm'; 7 | import { BASE_URL } from './utils'; 8 | 9 | function Sejm() { 10 | const [resultsData, setResultsData] = useState([]); 11 | 12 | useEffect(() => { 13 | fetch(`${BASE_URL}/results/sejm/detailed`) 14 | .then(response => { 15 | return response.json() 16 | }) 17 | .then(data => { 18 | setResultsData(data) 19 | createDefaultSejmInfo() 20 | }) 21 | }, []); 22 | 23 | return ( 24 |
25 | 26 |
27 | 28 |
29 |
30 | 31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 | ) 40 | } 41 | 42 | export default Sejm; -------------------------------------------------------------------------------- /src/Visualization.tsx: -------------------------------------------------------------------------------- 1 | import { COLORS, SHORT_NAME, FullData } from "./utils"; 2 | 3 | // Creates a 100% pie chart with party's color 4 | function createSquare(party: string, type: string, size: string, count: number = 1) { 5 | const div = document.createElement('div') 6 | if (size === 'standard') { 7 | div.style.width = '3.5rem' 8 | div.style.height = '3.5rem' 9 | } else { 10 | div.style.width = size 11 | div.style.height = size 12 | } 13 | 14 | div.style.display = 'flex' 15 | div.style.margin = '0.1rem' 16 | 17 | const color = COLORS[party] ? COLORS[party][type] : COLORS['Inni'][type] 18 | div.style.backgroundColor = color 19 | 20 | if (party === 'tossup') { 21 | div.dataset.type = 'Toss-up' 22 | } else { 23 | div.dataset.type = `${SHORT_NAME[party]} - ${type.charAt(0).toUpperCase() + type.slice(1)}` 24 | } 25 | 26 | if (count !== 1) { 27 | div.dataset.type = div.dataset.type + `: ${count}` 28 | } 29 | 30 | return div; 31 | } 32 | 33 | 34 | function createManyPies(party: string, type: string, count: number, size: string) { 35 | const div = document.getElementById('visualization') 36 | if (!div) { 37 | return 38 | } 39 | 40 | for (let i = 0; i < count; i++) { 41 | div.appendChild(createSquare(party, type, size, size === 'standard' ? 1 : count)) 42 | } 43 | } 44 | 45 | 46 | function createVisualization(data: FullData, seats: number, size = 'standard') { 47 | const div = document.getElementById('visualization') 48 | if (!div) { 49 | return 50 | } 51 | 52 | let count = 0 53 | for (const [key, value] of Object.entries(data)) { 54 | createManyPies(key, 'safe', value.safe, size) 55 | createManyPies(key, 'likely', value.likely, size) 56 | createManyPies(key, 'leaning', value.leaning, size) 57 | createManyPies(key, 'tossup', value.tossup, size) // Party tossups 58 | count += value.safe + value.likely + value.leaning + value.tossup 59 | } 60 | } 61 | 62 | export function createDefaultVisualization(data: FullData, type: string) { 63 | const seats = type === 'sejm' ? 460 : 100 64 | const size = type === 'sejm' ? '1.25rem' : '2.4rem' 65 | createVisualization(data, seats, size) 66 | } 67 | 68 | export default createVisualization; 69 | -------------------------------------------------------------------------------- /src/Table.tsx: -------------------------------------------------------------------------------- 1 | import { COLORS, FullData } from "./utils"; 2 | 3 | // Square is a small colored square inside a cell 4 | function createCell(row:any, content:any, color?:any, square?:any){ 5 | let text = document.createTextNode(content); 6 | let cell = row.insertCell(row.cells.length); 7 | cell.style.position = 'relative' 8 | if (color) { 9 | cell.style.backgroundColor = color 10 | } 11 | 12 | cell.appendChild(text); 13 | 14 | // Create a small colored square inside a cell with color 'square' 15 | if (square) { 16 | const div = document.createElement('div') 17 | div.style.width = '1rem' 18 | div.style.height = '1rem' 19 | div.style.backgroundColor = square 20 | div.style.display = 'inline-block' 21 | div.style.position = 'absolute' 22 | div.style.left = '50%' 23 | div.style.marginTop = '0.25rem' 24 | cell.appendChild(div) 25 | } 26 | } 27 | 28 | function createTable(data: FullData, type: string) { 29 | const tableDiv = document.getElementById('table') 30 | if (!tableDiv) { 31 | return 32 | } 33 | 34 | const table = document.createElement('table') 35 | 36 | const header = table.insertRow(0) 37 | createCell(header, '') 38 | createCell(header, 'Partia') 39 | createCell(header, 'Głosy') 40 | createCell(header, 'Procent') 41 | if (type === 'sejm') { 42 | createCell(header, 'Mandaty') 43 | createCell(header, 'Głosy do następnego mandatu') 44 | } 45 | 46 | // Create table rows 47 | for (const [key, value] of Object.entries(data)) { 48 | const row = table.insertRow(table.rows.length) 49 | if (!value.totalVotes) { 50 | continue; 51 | } 52 | 53 | const ourColor = COLORS[key] ? COLORS[key].safe : COLORS["Inni"].safe 54 | 55 | createCell(row, '', ourColor) 56 | createCell(row, key) 57 | createCell(row, value.votes) 58 | createCell(row, (value.votes * 100 / value.totalVotes).toFixed(2) + '%') 59 | if (type === 'sejm') { 60 | createCell(row, value.current) 61 | createCell(row, value.next.value, null, COLORS[value.next.who] ? COLORS[value.next.who].safe : null) // TODO 62 | } 63 | } 64 | 65 | tableDiv.appendChild(table) 66 | } 67 | 68 | export default createTable; -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Wyniki wyborów parlamentarnych 2023 2 | 3 | Strona pozwala wygodnie i w przejrzysty sposób podejrzeć wyniki pobierane z PKW 4 | 5 | Link: https://mkostyk.github.io/wybory2023-client/ 6 | 7 | This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app). 8 | 9 | ## Available Scripts 10 | 11 | In the project directory, you can run: 12 | 13 | ### `npm start` 14 | 15 | Runs the app in the development mode.\ 16 | Open [http://localhost:3000](http://localhost:3000) to view it in the browser. 17 | 18 | The page will reload if you make edits.\ 19 | You will also see any lint errors in the console. 20 | 21 | ### `npm test` 22 | 23 | Launches the test runner in the interactive watch mode.\ 24 | See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information. 25 | 26 | ### `npm run build` 27 | 28 | Builds the app for production to the `build` folder.\ 29 | It correctly bundles React in production mode and optimizes the build for the best performance. 30 | 31 | The build is minified and the filenames include the hashes.\ 32 | Your app is ready to be deployed! 33 | 34 | See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information. 35 | 36 | ### `npm run eject` 37 | 38 | **Note: this is a one-way operation. Once you `eject`, you can’t go back!** 39 | 40 | If you aren’t satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project. 41 | 42 | Instead, it will copy all the configuration files and the transitive dependencies (webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point you’re on your own. 43 | 44 | You don’t have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn’t feel obligated to use this feature. However we understand that this tool wouldn’t be useful if you couldn’t customize it when you are ready for it. 45 | 46 | ## Learn More 47 | 48 | You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started). 49 | 50 | To learn React, check out the [React documentation](https://reactjs.org/). 51 | -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 24 | 25 | 34 | Wybory 2023 35 | 36 | 37 | 38 |
39 | 49 | 50 | 51 | -------------------------------------------------------------------------------- /src/ProgressBar.tsx: -------------------------------------------------------------------------------- 1 | import {useState, useEffect} from 'react'; 2 | import { 3 | Chart as ChartJS, 4 | CategoryScale, 5 | LinearScale, 6 | BarElement, 7 | Title, 8 | Tooltip, 9 | Legend, 10 | } from 'chart.js'; 11 | import { Bar } from 'react-chartjs-2'; 12 | import { OPPOSITION } from './utils'; 13 | import { FullData, isObjectEmpty } from './utils'; 14 | import Chart from 'chart.js/auto' 15 | 16 | ChartJS.register( 17 | CategoryScale, 18 | LinearScale, 19 | BarElement, 20 | Title, 21 | Tooltip, 22 | Legend 23 | ); 24 | 25 | export function createProgressBar(resultsData: FullData) { 26 | const barDiv = document.getElementById('progressBar') 27 | console.log(resultsData) 28 | if (!barDiv || resultsData === undefined || isObjectEmpty(resultsData)) { 29 | return 30 | } 31 | 32 | const canvas = document.createElement('canvas') 33 | barDiv.style.height = '50px'; 34 | barDiv.appendChild(canvas) 35 | 36 | const options = { 37 | plugins: { 38 | title: { 39 | display: false, 40 | }, 41 | legend: { 42 | display: false 43 | }, 44 | tooltip: { 45 | callbacks: { 46 | label: function(context: any) { 47 | let label = context.dataset.label || ''; 48 | return label; 49 | } 50 | } 51 | } 52 | }, 53 | maintainAspectRatio: false, 54 | responsive: true, 55 | indexAxis: 'y' as "y", // This is bad but what can you do 56 | scales: { 57 | x: { 58 | stacked: true, 59 | max: 100, 60 | ticks: { 61 | display: false, 62 | }, 63 | grid: { 64 | display: false, 65 | } 66 | }, 67 | y: { 68 | stacked: true 69 | } 70 | }, 71 | }; 72 | 73 | const anyKey = Object.keys(resultsData)[0] 74 | 75 | const data = { 76 | labels: [''], 77 | datasets: [ 78 | { 79 | label: `Policzone komisje: ${resultsData[anyKey]['counted']}%`, 80 | data: [resultsData[anyKey]['counted']], 81 | backgroundColor: '#9d032a', 82 | } 83 | ], 84 | }; 85 | 86 | new Chart( 87 | canvas, 88 | { 89 | type: 'bar', 90 | data: data, 91 | options: options, 92 | } 93 | ); 94 | } 95 | -------------------------------------------------------------------------------- /src/Info.tsx: -------------------------------------------------------------------------------- 1 | import createTable from "./Table"; 2 | import createVisualization from "./Visualization"; 3 | import { createDefaultVisualization } from "./Visualization"; 4 | import { SEATS } from "./utils"; 5 | import { useState } from "react"; 6 | import { createProgressBar } from "./ProgressBar"; 7 | import { BASE_URL } from "./utils"; 8 | 9 | function goBack(type: string) { 10 | if (type === 'sejm') { 11 | createDefaultSejmInfo() 12 | } else { 13 | createDefaultSenatInfo() 14 | } 15 | } 16 | 17 | function clearInfo(id: number, type: string) { 18 | const info = document.getElementById('info') 19 | const elections_to = type === 'sejm' ? 'Sejmu' : 'Senatu' 20 | if (info) { 21 | if (id === -1) { 22 | info.innerHTML = 23 | ` 24 |
25 |
26 |

Wyniki wyborów do ${elections_to} 2023 w Polsce

27 |
28 |
29 |
30 |
31 |
32 | ` 33 | } else { 34 | info.innerHTML = 35 | ` 36 |
37 | 38 |
39 |

Wyniki w okręgu nr ${id}

40 |
41 |
42 |
43 |
44 |
45 | ` 46 | } 47 | 48 | const backButton = document.querySelector('.backButton') 49 | if (backButton) { 50 | backButton.addEventListener('click', () => goBack(type)) 51 | } 52 | 53 | return 54 | } 55 | } 56 | 57 | export function generateSejmInfo(event: any) { 58 | const id = event.target.parentElement.dataset.id 59 | fetch(`${BASE_URL}/results/sejm/${id}`) 60 | .then(response => { 61 | return response.json() 62 | }) 63 | .then(data => { 64 | clearInfo(id, 'sejm') 65 | createTable(data, 'sejm') 66 | createProgressBar(data) 67 | createVisualization(data, SEATS[id - 1]) 68 | }) 69 | } 70 | 71 | export function createDefaultSejmInfo() { 72 | fetch(`${BASE_URL}/results/sejm`) 73 | .then(response => { 74 | return response.json() 75 | }) 76 | .then(data => { 77 | clearInfo(-1, 'sejm') 78 | createTable(data, 'sejm') 79 | createDefaultVisualization(data, 'sejm') 80 | }) 81 | } 82 | 83 | export function generateSenatInfo(event: any) { 84 | const id = event.target.parentElement.dataset.id 85 | fetch(`${BASE_URL}/results/senat/${id}`) 86 | .then(response => { 87 | return response.json() 88 | }) 89 | .then(data => { 90 | clearInfo(id, 'senat') 91 | createProgressBar(data) 92 | createTable(data, 'senat') 93 | }) 94 | } 95 | 96 | export function createDefaultSenatInfo() { 97 | fetch(`${BASE_URL}/results/senat`) 98 | .then(response => { 99 | return response.json() 100 | }) 101 | .then(data => { 102 | clearInfo(-1, 'senat') 103 | createTable(data, 'senat') 104 | createDefaultVisualization(data, 'senat') 105 | }) 106 | } -------------------------------------------------------------------------------- /src/App.css: -------------------------------------------------------------------------------- 1 | .App { 2 | text-align: center; 3 | } 4 | 5 | .App-logo { 6 | height: 40vmin; 7 | pointer-events: none; 8 | } 9 | 10 | @media (prefers-reduced-motion: no-preference) { 11 | .App-logo { 12 | animation: App-logo-spin infinite 20s linear; 13 | } 14 | } 15 | 16 | .App-header { 17 | background-color: #282c34; 18 | min-height: 100vh; 19 | display: flex; 20 | flex-direction: column; 21 | align-items: center; 22 | justify-content: center; 23 | font-size: calc(10px + 2vmin); 24 | color: white; 25 | } 26 | 27 | .App-link { 28 | color: #61dafb; 29 | } 30 | 31 | @keyframes App-logo-spin { 32 | from { 33 | transform: rotate(0deg); 34 | } 35 | to { 36 | transform: rotate(360deg); 37 | } 38 | } 39 | 40 | svg.map path { 41 | cursor: pointer; 42 | fill: #e3e3e3; 43 | stroke: #b0b0b0; 44 | stroke-width: .15%; 45 | stroke-linejoin: round; 46 | stroke-linecap: round 47 | } 48 | 49 | svg.map a[data-id] path.active,svg.map a[data-id] path:hover { 50 | filter: brightness(0.7); 51 | } 52 | 53 | .seat { 54 | width: 3.5rem; 55 | height: 3.5rem; 56 | display: flex; 57 | margin: 0.1rem; 58 | } 59 | 60 | #visualization { 61 | display: inline-flex; 62 | flex-wrap: wrap; 63 | width: 100%; 64 | height: auto; 65 | margin-bottom: 1rem; 66 | } 67 | 68 | #visualization:hover { 69 | margin-bottom: 2.5rem; 70 | } 71 | 72 | /* TODO */ 73 | div[data-type]:hover::after { 74 | content: attr(data-type); 75 | display: block; 76 | padding: 0.3rem 0.35rem; 77 | white-space: nowrap; 78 | background: #000; 79 | border-radius: 0.3rem; 80 | font-size: 0.65rem; 81 | color: #fff; 82 | z-index: 1; 83 | height: 1rem; 84 | position: relative; 85 | bottom: -4rem; 86 | left: 0; 87 | font-weight: 400; 88 | } 89 | 90 | /* Milligram.css */ 91 | body { 92 | color: #606c76; 93 | font-family: 'Roboto', 'Helvetica Neue', 'Helvetica', 'Arial', sans-serif; 94 | font-size: 1.2em; 95 | font-weight: 500; 96 | letter-spacing: .01em; 97 | line-height: 1.6; 98 | } 99 | 100 | table { 101 | border-spacing: 0; 102 | display: block; 103 | overflow-x: auto; 104 | text-align: left; 105 | width: 100%; 106 | margin-bottom: 2.5rem; 107 | } 108 | 109 | td, 110 | th { 111 | border-bottom: 0.1rem solid #e1e1e1; 112 | padding: 0.3rem 1rem; 113 | max-width: 30%; 114 | } 115 | 116 | td:first-child, 117 | th:first-child { 118 | padding-left: 0; 119 | } 120 | 121 | td:last-child, 122 | th:last-child { 123 | padding-right: 0; 124 | } 125 | 126 | /* End of Milligram.css */ 127 | 128 | h3 { 129 | margin-block-start: 1rem; 130 | margin-block-end: 1rem; 131 | } 132 | 133 | #table { 134 | font-size: 1rem; 135 | font-weight: bold; 136 | } 137 | 138 | #menu 139 | { 140 | margin: 0; 141 | background: #9b4dca; 142 | font-size: 0.9rem; 143 | } 144 | #menu ul 145 | { 146 | text-align: center; 147 | margin: 0; 148 | } 149 | 150 | #menu li 151 | { 152 | display: inline-block; 153 | margin-bottom: 0; 154 | } 155 | 156 | #menu li a, #menu li span 157 | { 158 | display: inline-block; 159 | margin-left: 0; 160 | padding: 1.3em 2em 1em 2em; 161 | letter-spacing: 0.20em; 162 | text-decoration: none; 163 | font-size: 1em; 164 | font-weight: 600; 165 | text-transform: uppercase; 166 | outline: 0; 167 | color: #FFF; 168 | } 169 | 170 | #menu a:hover 171 | { 172 | color: #FFF !important; 173 | background: #606c76; 174 | } 175 | 176 | #menuClicked 177 | { 178 | color: #9b4dca !important; 179 | background: #FFF; 180 | } 181 | 182 | #info-title{ 183 | width: 92%; 184 | } 185 | 186 | .backButton { 187 | float: left; 188 | } -------------------------------------------------------------------------------- /src/styles/buttons.css: -------------------------------------------------------------------------------- 1 | .button, 2 | button, 3 | input[type='button'], 4 | input[type='reset'], 5 | input[type='submit'] { 6 | background-color: #9b4dca; 7 | border: 0.1rem solid #9b4dca; 8 | border-radius: .4rem; 9 | color: #fff; 10 | cursor: pointer; 11 | display: inline-block; 12 | font-size: 1.5rem; 13 | font-weight: 700; 14 | height: 2.5rem; 15 | line-height: 2.5rem; 16 | padding: 0rem 0.5rem 0.2rem; 17 | text-align: center; 18 | text-decoration: none; 19 | text-transform: uppercase; 20 | white-space: nowrap; 21 | } 22 | 23 | .button:focus, .button:hover, 24 | button:focus, 25 | button:hover, 26 | input[type='button']:focus, 27 | input[type='button']:hover, 28 | input[type='reset']:focus, 29 | input[type='reset']:hover, 30 | input[type='submit']:focus, 31 | input[type='submit']:hover { 32 | background-color: #606c76; 33 | border-color: #606c76; 34 | color: #fff; 35 | outline: 0; 36 | } 37 | 38 | .button[disabled], 39 | button[disabled], 40 | input[type='button'][disabled], 41 | input[type='reset'][disabled], 42 | input[type='submit'][disabled] { 43 | cursor: default; 44 | opacity: .5; 45 | } 46 | 47 | .button[disabled]:focus, .button[disabled]:hover, 48 | button[disabled]:focus, 49 | button[disabled]:hover, 50 | input[type='button'][disabled]:focus, 51 | input[type='button'][disabled]:hover, 52 | input[type='reset'][disabled]:focus, 53 | input[type='reset'][disabled]:hover, 54 | input[type='submit'][disabled]:focus, 55 | input[type='submit'][disabled]:hover { 56 | background-color: #9b4dca; 57 | border-color: #9b4dca; 58 | } 59 | 60 | .button.button-outline, 61 | button.button-outline, 62 | input[type='button'].button-outline, 63 | input[type='reset'].button-outline, 64 | input[type='submit'].button-outline { 65 | background-color: transparent; 66 | color: #9b4dca; 67 | } 68 | 69 | .button.button-outline:focus, .button.button-outline:hover, 70 | button.button-outline:focus, 71 | button.button-outline:hover, 72 | input[type='button'].button-outline:focus, 73 | input[type='button'].button-outline:hover, 74 | input[type='reset'].button-outline:focus, 75 | input[type='reset'].button-outline:hover, 76 | input[type='submit'].button-outline:focus, 77 | input[type='submit'].button-outline:hover { 78 | background-color: transparent; 79 | border-color: #606c76; 80 | color: #606c76; 81 | } 82 | 83 | .button.button-outline[disabled]:focus, .button.button-outline[disabled]:hover, 84 | button.button-outline[disabled]:focus, 85 | button.button-outline[disabled]:hover, 86 | input[type='button'].button-outline[disabled]:focus, 87 | input[type='button'].button-outline[disabled]:hover, 88 | input[type='reset'].button-outline[disabled]:focus, 89 | input[type='reset'].button-outline[disabled]:hover, 90 | input[type='submit'].button-outline[disabled]:focus, 91 | input[type='submit'].button-outline[disabled]:hover { 92 | border-color: inherit; 93 | color: #9b4dca; 94 | } 95 | 96 | .button.button-clear, 97 | button.button-clear, 98 | input[type='button'].button-clear, 99 | input[type='reset'].button-clear, 100 | input[type='submit'].button-clear { 101 | background-color: transparent; 102 | border-color: transparent; 103 | color: #9b4dca; 104 | } 105 | 106 | .button.button-clear:focus, .button.button-clear:hover, 107 | button.button-clear:focus, 108 | button.button-clear:hover, 109 | input[type='button'].button-clear:focus, 110 | input[type='button'].button-clear:hover, 111 | input[type='reset'].button-clear:focus, 112 | input[type='reset'].button-clear:hover, 113 | input[type='submit'].button-clear:focus, 114 | input[type='submit'].button-clear:hover { 115 | background-color: transparent; 116 | border-color: transparent; 117 | color: #606c76; 118 | } 119 | 120 | .button.button-clear[disabled]:focus, .button.button-clear[disabled]:hover, 121 | button.button-clear[disabled]:focus, 122 | button.button-clear[disabled]:hover, 123 | input[type='button'].button-clear[disabled]:focus, 124 | input[type='button'].button-clear[disabled]:hover, 125 | input[type='reset'].button-clear[disabled]:focus, 126 | input[type='reset'].button-clear[disabled]:hover, 127 | input[type='submit'].button-clear[disabled]:focus, 128 | input[type='submit'].button-clear[disabled]:hover { 129 | color: #9b4dca; 130 | } 131 | -------------------------------------------------------------------------------- /src/utils.ts: -------------------------------------------------------------------------------- 1 | // export const BASE_URL = "https://wybory-2023-749033c3329e.herokuapp.com" 2 | export const BASE_URL = "https://wybory-2023-prod-92863b81ba12.herokuapp.com/" 3 | 4 | export const isObjectEmpty = (objectName: any) => { 5 | return ( 6 | objectName && 7 | Object.keys(objectName).length === 0 && 8 | objectName.constructor === Object 9 | ); 10 | }; 11 | 12 | export function getColorFromResult(results: any, id: number, type: string) { 13 | const result = results.results[id - 1] 14 | if (result === undefined || isObjectEmpty(result)) { 15 | return '#898989' 16 | } 17 | 18 | const parties = Object.keys(result) // TODO: this is bad, I should not use dicts everywhere 19 | const winner = parties[0] 20 | let colorType = '' 21 | 22 | if (type === 'senat') { 23 | if (result[winner].safe === 1) { 24 | colorType = 'safe' 25 | } else if (result[winner].likely === 1) { 26 | colorType = 'likely' 27 | } else if (result[winner].leaning === 1){ 28 | colorType = 'leaning' 29 | } else { 30 | colorType = 'tossup' 31 | } 32 | } else { 33 | const second = parties[1] 34 | const winner_percentage = result[winner]['votes'] / result[winner]['totalVotes'] * 100 35 | const second_percentage = result[second]['votes'] / result[second]['totalVotes'] * 100 36 | 37 | const diff = winner_percentage - second_percentage 38 | 39 | if (diff > 20) { 40 | colorType = 'safe' 41 | } else if (diff > 10) { 42 | colorType = 'likely' 43 | } else { 44 | colorType = 'leaning' 45 | } 46 | } 47 | 48 | if (colorType === 'tossup') { 49 | return COLORS['tossup']['safe'] 50 | } 51 | 52 | if (COLORS[winner] === undefined) { 53 | return COLORS["Inni"][colorType]; 54 | } 55 | 56 | return COLORS[winner][colorType]; 57 | } 58 | 59 | interface Color { 60 | [name: string]: string 61 | } 62 | 63 | export const COLORS: {[name: string]: Color} = { 64 | "Prawo i Sprawiedliwość": {'safe': '#06014a', 'likely': '#231a9c', 'leaning': '#6f66ed', 'tossup': '#d6d6d6'}, 65 | "Koalicja Obywatelska": {'safe': '#ff8d03', 'likely': '#ffb303', 'leaning': '#fcda79', 'tossup': '#d6d6d6'}, 66 | "Nowa Lewica": {'safe': '#ff0000', 'likely': '#ff5340', 'leaning': '#fca297', 'tossup': '#d6d6d6'}, 67 | "Trzecia Droga": {'safe': '#ffff00', 'likely': '#ffff40', 'leaning': '#ffff80', 'tossup': '#d6d6d6'}, 68 | "Konfederacja": {'safe': '#452814', 'likely': '#754421', 'leaning': '#b86e39', 'tossup': '#d6d6d6'}, 69 | "Bezpartyjni Samorządowcy": {'safe': '#02fa02', 'likely': '#56fc56', 'leaning': '#a5faa5', 'tossup': '#d6d6d6'}, 70 | "Mniejszość Niemiecka": {'safe': '#d000fa', 'likely': '#e669ff', 'leaning': '#efacfc', 'tossup': '#d6d6d6'}, 71 | "Polska Jest Jedna": {'safe': '#03ffcd', 'likely': '#74fce1', 'leaning': '#b3fcee', 'tossup': '#d6d6d6'}, 72 | "Inni": {'safe': '#000000', 'likely': '#262626', 'leaning': '#474747', 'tossup': '#d6d6d6'}, 73 | "tossup": {'safe': '#d6d6d6', 'likely': '#d6d6d6', 'leaning': '#d6d6d6', 'tossup': '#d6d6d6'}, 74 | } 75 | 76 | export const SEATS = [12, 8, 14, 12, 13, 15, 12, 12, 10, 9, 12, 8, 14, 10, 9, 10, 9, 12, 20, 12, 12, 11, 15, 14, 12, 14, 9, 7, 9, 9, 12, 9, 16, 8, 10, 12, 9, 9, 10, 8, 12] 77 | 78 | export const OPPOSITION = ['Koalicja Obywatelska', 'Trzecia Droga', 'Nowa Lewica'] 79 | 80 | export const SHORT_NAME: {[name: string]: string} = { 81 | "Prawo i Sprawiedliwość": "PiS", 82 | "Koalicja Obywatelska": "KO", 83 | "Nowa Lewica": "NL", 84 | "Trzecia Droga": "TD", 85 | "Konfederacja": "KONF", 86 | "Bezpartyjni Samorządowcy": "BS", 87 | "Mniejszość Niemiecka": "MN", 88 | "Polska Jest Jedna": "PJJ", 89 | "Niezależni": "NIEZ", 90 | } 91 | 92 | interface Next { 93 | value: string; 94 | who: string; 95 | } 96 | 97 | export interface Data { 98 | votes: number; 99 | totalVotes: number; 100 | current: number; 101 | next: Next; 102 | safe: number; 103 | likely: number; 104 | leaning: number; 105 | tossup: number; 106 | counted: number; 107 | date: string; 108 | } 109 | 110 | export interface FullData { 111 | [name: string]: Data 112 | } 113 | 114 | export const PaktSenacki: string[] = [ 115 | "Koalicja Obywatelska", 116 | "Nowa Lewica", 117 | "Trzecia Droga", 118 | "KOMITET WYBORCZY WYBORCÓW JÓZEFA ZAJĄCA", 119 | "KOMITET WYBORCZY WYBORCÓW WADIM TYSZKIEWICZ - PAKT SENACKI", 120 | "KOMITET WYBORCZY WYBORCÓW KRZYSZTOF KWIATKOWSKI - PAKT SENACKI", 121 | "KOMITET WYBORCZY WYBORCÓW ZYGMUNT FRANKIEWICZ - PAKT SENACKI", 122 | "KOMITET WYBORCZY WYBORCÓW ANDRZEJ DZIUBA - PAKT SENACKI" 123 | ] -------------------------------------------------------------------------------- /src/Chart.tsx: -------------------------------------------------------------------------------- 1 | import {useState, useEffect} from 'react'; 2 | import { 3 | Chart as ChartJS, 4 | CategoryScale, 5 | LinearScale, 6 | BarElement, 7 | Title, 8 | Tooltip, 9 | Legend, 10 | } from 'chart.js'; 11 | import { Bar } from 'react-chartjs-2'; 12 | import { OPPOSITION } from './utils'; 13 | import { BASE_URL } from './utils'; 14 | 15 | ChartJS.register( 16 | CategoryScale, 17 | LinearScale, 18 | BarElement, 19 | Title, 20 | Tooltip, 21 | Legend 22 | ); 23 | 24 | export interface Result { 25 | safe: number; 26 | likely: number; 27 | leaning: number; 28 | lastUpdate: string; 29 | } 30 | 31 | export interface FullResults { 32 | [key: string]: Result; 33 | } 34 | 35 | const defaultTeamResults: FullResults = { 36 | 'Opozycja': {safe: 0, likely: 0, leaning: 0, lastUpdate: ''}, 37 | 'Reszta': {safe: 0, likely: 0, leaning: 0, lastUpdate: ''}, 38 | } 39 | 40 | function sumResult(result: Result): number { 41 | return result.safe + result.likely + result.leaning 42 | } 43 | 44 | export function Chart(props: {type: string}) { 45 | const [teamResults, setTeamResults] = useState(defaultTeamResults); 46 | 47 | useEffect(() => { 48 | fetch(`${BASE_URL}/results/${props.type}`) 49 | .then(response => { 50 | return response.json() 51 | }) 52 | .then(data => { 53 | // Calculate team results 54 | const teamResults: FullResults = { 55 | 'Opozycja': {safe: 0, likely: 0, leaning: 0, lastUpdate: ''}, 56 | 'Reszta': { safe: 0, likely: 0, leaning: 0, lastUpdate: ''}, 57 | } 58 | 59 | for (const key in data) { 60 | if (OPPOSITION.includes(key)) { 61 | teamResults['Opozycja'].safe += data[key].safe 62 | teamResults['Opozycja'].likely += data[key].likely 63 | teamResults['Opozycja'].leaning += data[key].leaning 64 | teamResults['Opozycja'].lastUpdate = data[key].lastUpdate 65 | } else { 66 | teamResults['Reszta'].safe += data[key].safe 67 | teamResults['Reszta'].likely += data[key].likely 68 | teamResults['Reszta'].leaning += data[key].leaning 69 | teamResults['Reszta'].lastUpdate = data[key].lastUpdate 70 | } 71 | } 72 | 73 | setTeamResults(teamResults) 74 | console.log(teamResults) 75 | }) 76 | }, []); 77 | 78 | const seats = props.type === "sejm" ? 460 : 100; 79 | const elections_to = props.type === "sejm" ? "Sejmu" : "Senatu"; 80 | 81 | const options = { 82 | plugins: { 83 | title: { 84 | display: true, 85 | text: `Wyniki wyborów do ${elections_to} 2023 w Polsce - ostatnia aktualizacja: ${teamResults['Opozycja'].lastUpdate}`, 86 | }, 87 | }, 88 | maintainAspectRatio: false, 89 | responsive: true, 90 | indexAxis: 'y' as "y", // This is bad but what can you do 91 | scales: { 92 | x: { 93 | stacked: true, 94 | max: seats, 95 | ticks: { 96 | stepSize: props.type === "sejm" ? 20 : 5, 97 | font: { 98 | size: 15, 99 | }, 100 | }, 101 | }, 102 | xAxis2: { 103 | max: seats, 104 | position: 'top' as "top", 105 | grid: { 106 | lineWidth: 3, 107 | color: 'black', 108 | z: 1 109 | }, 110 | ticks: { 111 | stepSize: seats / 2, 112 | font: { 113 | size: 25, 114 | }, 115 | }, 116 | }, 117 | y: { 118 | stacked: true 119 | } 120 | }, 121 | }; 122 | 123 | const oppositionName = props.type === "sejm" ? "KO+TD+NL" : "Pakt Senacki" 124 | 125 | const data = { 126 | labels: [''], 127 | datasets: [ 128 | { 129 | label: `${oppositionName} - Safe`, 130 | data: [teamResults['Opozycja'].safe], 131 | backgroundColor: '#ff8d03', 132 | }, 133 | { 134 | label: `${oppositionName} - Likely`, 135 | data: [teamResults['Opozycja'].likely], 136 | backgroundColor: '#ffb303', 137 | }, 138 | { 139 | label: `${oppositionName} - Leaning`, 140 | data: [teamResults['Opozycja'].leaning], 141 | backgroundColor: '#fcda79', 142 | }, 143 | { 144 | label: 'Toss-up', 145 | data: [seats - sumResult(teamResults['Opozycja']) - sumResult(teamResults['Reszta'])], 146 | backgroundColor: '#d6d6d6', 147 | }, 148 | { 149 | label: 'Reszta - Leaning', 150 | data: [teamResults['Reszta'].leaning], 151 | backgroundColor: '#6f66ed', 152 | }, 153 | { 154 | label: 'Reszta - Likely', 155 | data: [teamResults['Reszta'].likely], 156 | backgroundColor: '#231a9c', 157 | }, 158 | { 159 | label: 'Reszta - Safe', 160 | data: [teamResults['Reszta'].safe], 161 | backgroundColor: '#06014a', 162 | } 163 | ], 164 | }; 165 | 166 | return (
167 | 168 |
); 169 | } 170 | -------------------------------------------------------------------------------- /src/assets/SejmMap copy.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import createTable from "../Table" 3 | 4 | function Icon() { 5 | return ( 6 | 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 | export default Icon; -------------------------------------------------------------------------------- /src/assets/SejmMap.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 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 | -------------------------------------------------------------------------------- /src/SejmMap.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { generateSejmInfo } from "./Info" 3 | import { getColorFromResult } from "./utils" 4 | 5 | function Icon(results) { 6 | return ( 7 | 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 | export default Icon; --------------------------------------------------------------------------------