Created
April 2, 2025 10:41
-
-
Save lepinkainen/0eed512e359670c82114d329fd0bfe04 to your computer and use it in GitHub Desktop.
A SVG representation of the finnish parliament with the ability to visualise votes per seat
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!DOCTYPE html> | |
<html> | |
<head> | |
<title>Finnish Parliament Seating Chart</title> | |
<style> | |
body { | |
font-family: sans-serif; | |
display: flex; | |
flex-direction: column; | |
align-items: center; | |
padding: 20px; | |
background-color: #f0f0f0; | |
} | |
svg { | |
border: 1px solid #ccc; | |
background-color: #fff; | |
max-width: 100%; | |
height: auto; | |
border-radius: 8px; | |
} | |
.seat { | |
fill: #d3d3d3; /* Default gray */ | |
stroke: #555; | |
stroke-width: 0.5; | |
cursor: pointer; | |
transition: fill 0.3s ease; | |
} | |
.seat:hover { | |
stroke: #000; | |
stroke-width: 1; | |
} | |
.seat.vote-yes { | |
fill: #4CAF50; /* Green */ | |
} | |
.seat.vote-no { | |
fill: #F44336; /* Red */ | |
} | |
.seat.vote-absent { | |
fill: #9E9E9E; /* Darker Gray */ | |
} | |
.platform { | |
fill: #e0e0e0; | |
stroke: #999; | |
stroke-width: 1; | |
} | |
.platform-text { | |
font-size: 10px; | |
text-anchor: middle; | |
fill: #333; | |
} | |
#tooltip { | |
position: absolute; | |
background-color: rgba(0, 0, 0, 0.7); | |
color: white; | |
padding: 5px 10px; | |
border-radius: 4px; | |
font-size: 12px; | |
pointer-events: none; /* So it doesn't interfere with mouse events on seats */ | |
display: none; /* Hidden by default */ | |
white-space: nowrap; | |
} | |
.controls { | |
margin-top: 20px; | |
padding: 15px; | |
background-color: #fff; | |
border-radius: 8px; | |
box-shadow: 0 2px 5px rgba(0,0,0,0.1); | |
} | |
.controls button { | |
margin: 0 5px; | |
padding: 8px 15px; | |
border: none; | |
border-radius: 4px; | |
background-color: #007bff; | |
color: white; | |
cursor: pointer; | |
transition: background-color 0.3s ease; | |
} | |
.controls button:hover { | |
background-color: #0056b3; | |
} | |
</style> | |
</head> | |
<body> | |
<h1>Finnish Parliament Seating Chart (Schematic)</h1> | |
<svg id="parliament-chart" viewBox="0 0 800 500" xmlns="http://www.w3.org/2000/svg"> | |
<title>Finnish Parliament Seating Chart</title> | |
<desc>A schematic representation of the 200 seats in the Finnish Parliament.</desc> | |
<path id="platform" class="platform" d="M 250,480 Q 400,400 550,480 L 550, 495 L 250, 495 Z" /> | |
<text x="400" y="490" class="platform-text">Puhemies / Talman</text> | |
<g id="seating-area"> | |
</g> | |
</svg> | |
<div id="tooltip"></div> | |
<div class="controls"> | |
<button onclick="applySampleVotes()">Apply Sample Votes</button> | |
<button onclick="clearVotes()">Clear Votes</button> | |
</div> | |
<script> | |
const svgNS = "http://www.w3.org/2000/svg"; | |
const seatingArea = document.getElementById('seating-area'); | |
const tooltip = document.getElementById('tooltip'); | |
const svg = document.getElementById('parliament-chart'); | |
// --- Layout Parameters --- | |
const centerX = 400; | |
const centerY = 480; // Focus point slightly below the center bottom | |
const numSeats = 200; | |
const numRows = 10; // Adjust as needed | |
const rowSpacing = 25; // Distance between rows | |
const firstRowRadius = 150; // Radius of the innermost row | |
const seatRadius = 8; // Visual size of the seat | |
const startAngle = -Math.PI * 0.9; // Start angle (a bit past -180 degrees) | |
const endAngle = -Math.PI * 0.1; // End angle (a bit before 0 degrees) | |
const totalAngle = endAngle - startAngle; | |
let seatsPerRow = []; | |
let seatsInPreviousRows = 0; | |
// Distribute seats somewhat evenly, more seats in outer rows | |
for (let i = 0; i < numRows; i++) { | |
// Simple linear distribution - can be refined | |
let seats = Math.round(14 + i * 1.5); | |
seatsPerRow.push(seats); | |
} | |
// Adjust last row count to ensure total is exactly numSeats | |
let currentTotal = seatsPerRow.reduce((sum, count) => sum + count, 0); | |
seatsPerRow[numRows - 1] += (numSeats - currentTotal); | |
let seatCounter = 1; | |
// --- Generate Seats --- | |
for (let r = 0; r < numRows; r++) { | |
const radius = firstRowRadius + r * rowSpacing; | |
const seatsInThisRow = seatsPerRow[r]; | |
const angleStep = totalAngle / (seatsInThisRow > 1 ? seatsInThisRow -1 : 1); // Angle between seats in this row | |
for (let i = 0; i < seatsInThisRow; i++) { | |
if (seatCounter > numSeats) break; // Stop if we exceed 200 | |
const angle = startAngle + (seatsInThisRow > 1 ? i * angleStep : totalAngle / 2); // Center single seat | |
// Convert polar to cartesian coordinates | |
const cx = centerX + radius * Math.cos(angle); | |
const cy = centerY + radius * Math.sin(angle); | |
const seat = document.createElementNS(svgNS, 'circle'); | |
const seatId = `seat-${seatCounter}`; | |
seat.setAttribute('id', seatId); | |
seat.setAttribute('class', 'seat'); | |
seat.setAttribute('cx', cx); | |
seat.setAttribute('cy', cy); | |
seat.setAttribute('r', seatRadius); | |
// Add tooltip title | |
const title = document.createElementNS(svgNS, 'title'); | |
title.textContent = `Seat ${seatCounter}`; // Placeholder - replace with actual data | |
seat.appendChild(title); | |
// Add event listeners for tooltip | |
seat.addEventListener('mousemove', (event) => { | |
// Get the title text (representative name/info) | |
const seatInfo = seat.querySelector('title').textContent; | |
tooltip.textContent = seatInfo; | |
tooltip.style.display = 'block'; | |
// Position tooltip near the cursor | |
// Get SVG position relative to viewport | |
const svgRect = svg.getBoundingClientRect(); | |
// Calculate position relative to the SVG container | |
tooltip.style.left = `${event.clientX - svgRect.left + 15}px`; | |
tooltip.style.top = `${event.clientY - svgRect.top + 15}px`; | |
}); | |
seat.addEventListener('mouseleave', () => { | |
tooltip.style.display = 'none'; | |
}); | |
seatingArea.appendChild(seat); | |
seatCounter++; | |
} | |
if (seatCounter > numSeats) break; | |
} | |
// --- Voting Data and Coloring Logic --- | |
// ** IMPORTANT ** | |
// Replace this sample data with your actual voting data. | |
// The keys should match the seat IDs ('seat-1', 'seat-2', etc.) | |
// You'll need a way to map representative names/IDs from your data | |
// source to these sequential seat IDs. | |
const sampleVoteData = {}; | |
for (let i = 1; i <= numSeats; i++) { | |
const rand = Math.random(); | |
if (rand < 0.5) { | |
sampleVoteData[`seat-${i}`] = 'yes'; | |
} else if (rand < 0.85) { | |
sampleVoteData[`seat-${i}`] = 'no'; | |
} else { | |
sampleVoteData[`seat-${i}`] = 'absent'; | |
} | |
} | |
// Function to apply colors based on vote data | |
function applyVotes(voteData) { | |
clearVotes(); // Clear previous votes first | |
for (const seatId in voteData) { | |
const seatElement = document.getElementById(seatId); | |
if (seatElement) { | |
const vote = voteData[seatId]; | |
switch (vote) { | |
case 'yes': | |
seatElement.classList.add('vote-yes'); | |
break; | |
case 'no': | |
seatElement.classList.add('vote-no'); | |
break; | |
case 'absent': | |
seatElement.classList.add('vote-absent'); | |
break; | |
default: | |
// Keep default color if vote type is unknown | |
break; | |
} | |
// You might want to update the <title> here as well | |
// e.g., title.textContent = `Seat ${seatId.split('-')[1]} - Voted: ${vote}`; | |
} else { | |
console.warn(`Seat element with ID ${seatId} not found.`); | |
} | |
} | |
} | |
// Function to clear all vote colors | |
function clearVotes() { | |
const seats = document.querySelectorAll('.seat'); | |
seats.forEach(seat => { | |
seat.classList.remove('vote-yes', 'vote-no', 'vote-absent'); | |
// Reset title if you modified it | |
// const seatNum = seat.id.split('-')[1]; | |
// seat.querySelector('title').textContent = `Seat ${seatNum}`; | |
}); | |
} | |
// Function for the sample button | |
function applySampleVotes() { | |
console.log("Applying sample votes..."); | |
applyVotes(sampleVoteData); | |
} | |
// Initial clear state | |
clearVotes(); | |
</script> | |
</body> | |
</html> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment