Skip to content

Instantly share code, notes, and snippets.

@lepinkainen
Created April 2, 2025 10:41
Show Gist options
  • Save lepinkainen/0eed512e359670c82114d329fd0bfe04 to your computer and use it in GitHub Desktop.
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
<!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