// ==UserScript==
// @name Koalitionenrechner - wahlrecht.de
// @namespace Violentmonkey Scripts
// @match https://www.wahlrecht.de/umfragen/
// @grant none
// @version 4.0
// @author Sidem
// @description 11/13/2024, 11:00:16 PM
// ==/UserScript==
/* jshint esversion: 6 */
window.addEventListener('load', function () {
var script = document.createElement('script');
script.src = 'https://cdn.jsdelivr.net/npm/chart.js@3.5.1/dist/chart.min.js';
document.head.appendChild(script);
let setNewChart = () => { };
let setNewCoalitionChart = () => { };
let seatFactor = 1.0;
let data = [
{ id: 'cdu', name: 'CDU/CSU', icon: '⚫️', color: '#000000FF', votes: 0, projectedSeats: 0, logoUrl: '<img src="https://i.ibb.co/tYm4PG9/CDUCSU.png" alt="CDUCSU" border="0">', colorUri: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQAQMAAAAlPW0iAAAAA1BMVEUAAACnej3aAAAAC0lEQVQI12MgEQAAADAAAWV61nwAAAAASUVORK5CYII=' },
{ id: 'spd', name: 'SPD', icon: '🔴', color: '#E3000FFF', votes: 0, projectedSeats: 0, logoUrl: '<img src="https://i.ibb.co/LScLCYC/SPD.png" alt="SPD" border="0">', colorUri: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQAQMAAAAlPW0iAAAAA1BMVEXjAA+cYU6yAAAAC0lEQVQI12MgEQAAADAAAWV61nwAAAAASUVORK5CYII=' },
{ id: 'gru', name: 'GRÜNE', icon: '🟢', color: '#64A12DFF', votes: 0, projectedSeats: 0, logoUrl: '<img src="https://i.ibb.co/M6cx5K3/GRUENE.png" alt="GRUENE" border="0">', colorUri: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQAQMAAAAlPW0iAAAAA1BMVEVkoS0aZ4/7AAAAC0lEQVQI12MgEQAAADAAAWV61nwAAAAASUVORK5CYII=' },
{ id: 'fdp', name: 'FDP', icon: '🟡', color: '#FFED00FF', votes: 0, projectedSeats: 0, logoUrl: '<img src="https://i.ibb.co/m093TBw/FDP.png" alt="FDP" border="0">', colorUri: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQAQMAAAAlPW0iAAAAA1BMVEX/7QDyMoSWAAAAC0lEQVQI12MgEQAAADAAAWV61nwAAAAASUVORK5CYII=' },
{ id: 'lin', name: 'LINKE', icon: '🟣', color: '#FF0000FF', votes: 0, projectedSeats: 0, logoUrl: '<img src="https://i.ibb.co/Mp4TFzX/LINKE.png" alt="LINKE" border="0">', colorUri: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQAQMAAAAlPW0iAAAAA1BMVEX/AAAZ4gk3AAAAC0lEQVQI12MgEQAAADAAAWV61nwAAAAASUVORK5CYII=' },
{ id: 'afd', name: 'AfD', icon: '🔵', color: '#009DE0FF', votes: 0, projectedSeats: 0, logoUrl: '<img src="https://i.ibb.co/3hzmWRr/AfD.png" alt="AfD" border="0">', colorUri: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQAQMAAAAlPW0iAAAAA1BMVEUAneDDnyv1AAAAC0lEQVQI12MgEQAAADAAAWV61nwAAAAASUVORK5CYII=' },
{ id: 'frw', name: 'FW', icon: '⚪️', color: '#666666FF', votes: 0, projectedSeats: 0, logoUrl: '<img src="https://i.ibb.co/3hzmWRr/AfD.png" alt="FW" border="0">', colorUri: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQAQMAAAAlPW0iAAAAA1BMVEVmZmZ8VoIEAAAAC0lEQVQI12MgEQAAADAAAWV61nwAAAAASUVORK5CYII=' },
{ id: 'bsw', name: 'BSW', icon: '🟠', color: '#FF8800FF', votes: 0, projectedSeats: 0, logoUrl: '<img src="https://i.ibb.co/3hzmWRr/AfD.png" alt="BSW" border="0">', colorUri: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQAQMAAAAlPW0iAAAAA1BMVEX/iADquBt0AAAAC0lEQVQI12MgEQAAADAAAWV61nwAAAAASUVORK5CYII=' },
{ id: 'son', name: 'Sonstige', icon: '⚪️', color: '#666666FF', votes: 0, projectedSeats: 0, logoUrl: '', colorUri: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQAQMAAAAlPW0iAAAAA1BMVEVmZmZ8VoIEAAAAC0lEQVQI12MgEQAAADAAAWV61nwAAAAASUVORK5CYII=' }
];
let voteData = [];
let coalitionData = [];
function calculateWeight(date, newestDate) {
let daysOld = (newestDate - date.getTime()) / (1000 * 60 * 60 * 24);
return 5 * Math.exp(-Math.log(5) * daysOld / 30);
}
function calculateWeightedAverage(rowId) {
let dates = [];
let values = [];
let weights = [];
let dateRow = document.getElementById('datum');
let dataRow = document.getElementById(rowId);
let cells = dataRow.getElementsByTagName('td');
let dateCells = dateRow.getElementsByTagName('td');
let thirtyDaysAgo = new Date();
thirtyDaysAgo.setDate(thirtyDaysAgo.getDate() - 30);
for (let i = 1; i < cells.length - 2; i++) {
let dateSpan = dateCells[i].querySelector('span.li');
let value = cells[i].textContent.trim();
if (dateSpan && value !== '–') {
let dateText = dateSpan.textContent;
let dateParts = dateText.split('.');
let date = new Date(dateParts[2], dateParts[1] - 1, dateParts[0]);
if (date >= thirtyDaysAgo) {
let numValue = parseFloat(value.replace(' %', '').replace(',', '.'));
if (!isNaN(numValue)) {
dates.push(date);
values.push(numValue);
}
}
}
}
if (values.length === 0) return '–';
let newest = Math.max(...dates.map(d => d.getTime()));
weights = dates.map(date => calculateWeight(date, newest));
let weightSum = weights.reduce((a, b) => a + b, 0);
let weightedSum = values.reduce((sum, value, i) => sum + value * weights[i], 0);
let result = (weightedSum / weightSum).toFixed(1);
return result % 1 === 0 ? Math.round(result) + ' %' : result + ' %';
}
function updateTooltipsWithWeights() {
let dateRow = document.getElementById('datum');
let dateCells = dateRow.getElementsByTagName('td');
let dates = [];
let newestDate = null;
for (let i = 1; i < dateCells.length - 2; i++) {
let dateSpan = dateCells[i].querySelector('span.li');
if (dateSpan) {
let dateText = dateSpan.textContent;
let dateParts = dateText.split('.');
let date = new Date(dateParts[2], dateParts[1] - 1, dateParts[0]);
dates.push({ cell: dateCells[i], date: date });
if (!newestDate || date > newestDate) {
newestDate = date;
}
}
}
for (let dateObj of dates) {
let weight = calculateWeight(dateObj.date, newestDate.getTime());
let cell = dateObj.cell;
let thirtyDaysAgo = new Date();
thirtyDaysAgo.setDate(thirtyDaysAgo.getDate() - 30);
if (dateObj.date >= thirtyDaysAgo) {
let currentTitle = cell.getAttribute('title') || '';
let weightText = `\nGewichtung: ${weight.toFixed(2)}x`;
if (currentTitle.includes('Gewichtung:')) {
cell.setAttribute('title', currentTitle.replace(/\nGewichtung:.*/, weightText));
} else {
cell.setAttribute('title', currentTitle + weightText);
}
}
}
}
function insertAverageColumn() {
let headerRow = document.querySelector('thead tr');
let newHeader = document.createElement('th');
newHeader.className = 'in';
newHeader.innerHTML = 'Durch-<br>schnitt';
headerRow.insertBefore(newHeader, headerRow.lastElementChild.previousElementSibling);
let dateRow = document.getElementById('datum');
let dateTd = document.createElement('td');
dateTd.className = 'di';
dateRow.insertBefore(dateTd, dateRow.lastElementChild.previousElementSibling);
let partyIds = ["cdu", "spd", "gru", "fdp", "lin", "afd", "frw", "bsw", "son"];
partyIds.forEach(partyId => {
let row = document.getElementById(partyId);
if (row) {
let avgTd = document.createElement('td');
avgTd.className = 'di col-avg';
avgTd.style.cursor = 'pointer';
avgTd.textContent = calculateWeightedAverage(partyId);
avgTd.onclick = getChart;
avgTd.addEventListener("mouseenter", function (e) {
let column = document.getElementsByClassName('col-avg');
for (let item of column) {
item.style.backgroundColor = 'lightblue';
}
});
avgTd.addEventListener("mouseleave", function (e) {
let column = document.getElementsByClassName('col-avg');
for (let item of column) {
item.style.backgroundColor = 'white';
}
});
row.insertBefore(avgTd, row.lastElementChild.previousElementSibling);
}
});
}
function getCombinations(array) {
var result = [];
for (var i = 1; i < (1 << array.length); i++) {
var subset = [];
for (var j = 0; j < array.length; j++)
if (i & (1 << j))
subset.push(array[j]);
result.push(subset);
}
return result;
}
function cleanVote(str) {
if (str.includes('\n')) {
let split = str.split('\n');
return cleanVote(split[0].split(' ')[1]) + cleanVote(split[1].split(' ')[1]);
}
if (str.includes('–')) return 0;
return parseFloat(str.replace(',', '.').replace(' %', ''));
}
function getPartyByIdentifier(identifier) {
for (let p of voteData) {
if (p.name == identifier || p.color == identifier || p.icon == identifier || p.id == identifier) {
return p;
}
}
return { id: 'son', name: 'Sonstige', icon: '⚪️', color: '#666666', votes: 0 };
}
let allPossibleCoalitions = getCombinations(['⚫️', '🔴', '🟣', '🟡', '🟢', '🔵', '🟠', '⚪️']);
allPossibleCoalitions = allPossibleCoalitions.filter((el) => { return (el.length >= 1 && el.length < 5) }); // filter to only 2-4 party coalitions
let coalitions = allPossibleCoalitions.filter((el) => {
return true;
if (el.includes('🔵') && el.includes('🟢')) return false;
if (el.includes('🔵') && el.includes('🔴')) return false;
if (el.includes('🔵') && el.includes('🟣')) return false;
if (el.includes('🟣') && el.includes('🟡')) return false;
if (el.includes('🟣') && el.includes('⚫️')) return false;
return true;
});
function isPermutation(a, b) {
return b.filter(x => !a.includes(x)).length === 0 && a.length == 3;
}
function coalitionSymbol(coalition) {
if (isPermutation(coalition, ['⚫️', '🔴', '🟢'])) return "<img src='https://flagcdn.com/w20/ke.png' alt='🇰🇪' title='Kenia'>";
if (isPermutation(coalition, ['⚫️', '🔴', '🟡'])) return "<img src='https://flagcdn.com/w20/de.png' alt='🇩🇪' title='Deutschland'>";
if (isPermutation(coalition, ['⚫️', '🟡', '🟢'])) return "<img src='https://flagcdn.com/w20/jm.png' alt='🇯🇲' title='Jamaika'>";
if (isPermutation(coalition, ['🟣', '🔴', '🟢'])) return "<img src='https://flagcdn.com/w20/by.png' alt='🇧🇾' title='Weissrussland'>";
if (isPermutation(coalition, ['⚫️', '🟡', '🔵'])) return "<img src='https://flagcdn.com/w20/bs.png' alt='🇧🇸' title='Bahamas'>";
if (isPermutation(coalition, ['🟡', '🔴', '🟢'])) return "🚦";
return "";
}
function getCoalitions() {
let str = "<strong>Anteil der Sitze im Bundestag</strong>";
let results = [];
for (let coalition of coalitions) {
let currentCoalitionVotes = 0;
let containsBelow = false;
coalition.sort((p1, p2) => { return getPartyByIdentifier(p2).votes - getPartyByIdentifier(p1).votes });
for (let p of coalition) {
if (currentCoalitionVotes >= 50.0) containsBelow = true;
currentCoalitionVotes += getPartyByIdentifier(p).votes * seatFactor;
if (getPartyByIdentifier(p).name == "Sonstige") containsBelow = true;
if (getPartyByIdentifier(p).votes < 5.0) containsBelow = true;
}
if (!containsBelow) results.push({ coalition: coalition.join(""), parties: coalition, votes: currentCoalitionVotes });
}
results.sort((a, b) => { return b.votes - a.votes; });
let line = false;
let weight = "bold;";
for (let c of results) {
if (!line && c.votes < 50) {
line = true;
weight = '100;';
}
str += "<p style='margin: 0px; font-size: 1rem; font-weight: " + weight + "'><strong style='display: inline-block; vertical-align: middle; width: 64px;'>" + c.parties.map((e) => { return '<img class="partyColor" src="' + getPartyByIdentifier(e).colorUri + '" alt="' + getPartyByIdentifier(e).icon + '" title="' + getPartyByIdentifier(e).name + '" />'; }).join("") + "</strong>" + c.votes.toFixed(1) + "% " + coalitionSymbol(c.parties) + "</p>";
}
coalitionData = results;
//str += "<button id='copyCoalitionBtn'>Copy</button>";
coalitionBox.innerHTML = str;
let copyBtn = document.createElement('button');
copyBtn.innerText = "Copy";
copyBtn.onclick = (e) => {
navigator.clipboard.writeText(getCoalitionString(coalitionBox)).then(() => {
console.log("success");
}, function () {
console.log("failed writing clipboard");
});
};
coalitionBox.appendChild(copyBtn);
setNewCoalitionChart();
}
const getCoalitionString = (container) => {
let str = "";
for (let p of container.childNodes) {
for (let item of p.childNodes) {
if (item.nodeName == 'STRONG') {
for (let color of item.childNodes) {
str += color.alt;
}
} else if (item.nodeName == '#text' && item.data != 'Copy') {
str += item.data;
} else if (item.nodeName == 'IMG') {
str += item.alt;
}
}
str += "\n";
}
return str;
};
let getBelowFivePercentage = () => {
let total = getPartyByIdentifier('son').votes;
let partyIds = ["cdu", "spd", "gru", "fdp", "lin", "afd", "frw", "bsw"];
for (let id of partyIds) {
let thisVotes = getPartyByIdentifier(id).votes;
if (thisVotes < 5.0) total += thisVotes;
}
console.log(total);
return total;
};
let getChart = (e) => {
let colDataElements = document.getElementsByClassName(e.target.classList[e.target.classList.length - 1]);
let i = 0;
voteData = [];
for (let el of colDataElements) {
voteData.push({ ...data[i], votes: cleanVote(el.innerText) })
i++;
}
voteData.sort((a, b) => { return b.votes - a.votes; });
seatFactor = 1 / ((100 - getBelowFivePercentage()) / 100);
getCoalitions();
setNewChart();
};
let getCoalitionDatasets = () => {
let datasets = [];
let numberset = [];
for (let party of voteData) {
numberset = [];
if (party.name != 'Sonstige' && party.votes >= 5.0) {
for (let coalition of coalitionData) {
if (coalition.parties.includes(party.icon)) {
numberset.push(party.votes * seatFactor);
} else {
numberset.push(0);
}
}
datasets.push({ label: party.name, data: numberset, backgroundColor: party.color });
}
}
return datasets;
}
let partyIds = ["cdu", "spd", "gru", "fdp", "lin", "afd", "frw", "bsw", "son"];
let rows = document.getElementsByTagName('tr');
for (let row of rows) {
if (partyIds.includes(row.id)) {
let numbers = row.getElementsByTagName('td');
let n = 0;
for (let number of numbers) {
if (n != 0 && n != 9) {
number.classList.add('col-' + n);
number.onclick = getChart;
number.style.cursor = 'pointer';
number.addEventListener("mouseenter", function (e) {
let columnClass = e.target.classList[e.target.classList.length - 1];
let column = document.getElementsByClassName(columnClass);
for (let item of column) {
item.style.backgroundColor = 'lightblue';
}
});
number.addEventListener("mouseleave", function (e) {
let columnClass = e.target.classList[e.target.classList.length - 1];
let column = document.getElementsByClassName(columnClass);
for (let item of column) {
item.style.backgroundColor = 'white';
}
});
}
n++;
}
}
}
let info = document.getElementById('info');
let coalitionBox = document.createElement('div');
coalitionBox.setAttribute('id', 'coalitionBox');
info.parentNode.insertBefore(coalitionBox, info);
let table = document.querySelector('.wilko');
table.id = 'tableBox';
table.parentNode.id = 'tableContainer';
script.onload = function () {
let ctx = document.getElementById('myChart').getContext('2d');
let myChart = {};
let ctx2 = document.getElementById('myCoalitionChart').getContext('2d');
let myCoalitionChart = {};
setNewChart = () => {
if (myChart instanceof Chart) {
myChart.data.labels = voteData.map(a => a.name);
myChart.data.datasets[0].data = voteData.map(a => a.votes);
myChart.data.datasets[0].backgroundColor = voteData.map(a => a.color);
myChart.update();
} else {
myChart = new Chart(ctx, {
type: 'bar',
data: {
labels: voteData.map(a => a.name),
datasets: [{
label: '% der Stimmen',
data: voteData.map(a => a.votes),
backgroundColor: voteData.map(a => a.color),
borderWidth: 1,
circumference: 180,
rotation: 270
}]
},
options: {
scales: {
y: {
suggestedMin: 0,
//suggestedMax: 50
}
}
}
});
}
};
setNewCoalitionChart = () => {
if (myCoalitionChart instanceof Chart) {
myCoalitionChart.data.labels = coalitionData.map(a => "");
myCoalitionChart.data.datasets = [...getCoalitionDatasets(), {
label: '50% Grenze',
data: coalitionData.map(a => 50),
borderColor: "#000000",
backgroundColor: "#000000",
type: 'line',
order: 10
}];
myCoalitionChart.update();
} else {
myCoalitionChart = new Chart(ctx2, {
type: 'bar',
data: {
labels: coalitionData.map(a => ""),
datasets: [...getCoalitionDatasets(), {
label: '50% Grenze',
data: coalitionData.map(a => 50),
borderColor: "#000000",
backgroundColor: "#000000",
type: 'line',
order: 10
}]
},
options: {
radius: 0,
indexAxis: 'y',
scales: {
x: {
suggestedMin: 0,
suggestedMax: 50,
stacked: true
},
y: {
stacked: true
}
},
responsive: true,
plugins: {
legend: {
position: 'right',
},
title: {
display: true,
text: 'Mögliche Koalitionen (% der projezierten Sitze)'
}
}
}
});
}
};
};
let chartContainer = document.createElement('div');
chartContainer.id = 'chartBox';
info.parentNode.insertBefore(chartContainer, info);
let chartBox = document.createElement('canvas');
chartBox.id = 'myChart';
chartContainer.appendChild(chartBox);
let coalitionChartContainer = document.createElement('div');
coalitionChartContainer.id = 'coalitionContainer';
info.parentNode.insertBefore(coalitionChartContainer, info);
let coalitionChartBox = document.createElement('canvas');
coalitionChartBox.id = 'myCoalitionChart';
coalitionChartContainer.appendChild(coalitionChartBox);
var style = document.createElement("style");
style.type = "text/css";
style.innerHTML = `
#tableContainer {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
}
#tableBox {
width: 90%;
}
#coalitionBox {
}
#chartBox {
width: 45%;
}
#coalitionContainer {
width: 45%;
}
#myChart {
}
#resultContainer {
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: center;
}
`;
document.head.appendChild(style);
let container = document.createElement('div');
container.id = 'resultContainer';
info.parentNode.insertBefore(container, info);
container.appendChild(coalitionBox);
container.appendChild(chartContainer);
container.appendChild(coalitionChartContainer);
insertAverageColumn();
updateTooltipsWithWeights();
}, false);