Greasy Fork is available in English.

Koalitionenrechner - wahlrecht.de

11/13/2024, 11:00:16 PM

// ==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);