Centrada Statistieken

Krijg inzicht in de data rondom je reacties op Centrada.

目前为 2025-03-21 提交的版本。查看 最新版本

// ==UserScript==
// @name         Centrada Statistieken
// @namespace    http://tampermonkey.net/
// @version      2025-03-22
// @description  Krijg inzicht in de data rondom je reacties op Centrada.
// @author       Mubbletm
// @match        https://woningzoeken.centrada.nl/portaal/mijn-reacties/
// @icon         https://www.google.com/s2/favicons?sz=64&domain=centrada.nl
// @grant        none
// @license MIT
// ==/UserScript==

// Common filters to use as array filter.
const filters = {
    lotingen: o => o.verdeelmethode === 'Loting',
    inschrijfduur: o => o.verdeelmethode === 'Inschrijfduur',
    top(percentage) {
        return o => (o.eindpositie / o.aantalreacties) < percentage;
    }
}

// Downloads given text as file.
function download(filename, text) {
    var element = document.createElement('a');
    element.setAttribute('href', 'data:text/plain;charset=utf-8,' + encodeURIComponent(text));
    element.setAttribute('download', filename);

    element.style.display = 'none';
    document.body.appendChild(element);

    element.click();

    document.body.removeChild(element);
}

// Insert script's buttons.
function insertButton() {
    const historieHeader = document.querySelector('div.js-reacties-historie > div.box > h2');
    historieHeader.insertAdjacentHTML('afterend', '<div id="statistics-script" style="display: flex; flex-direction: row; gap: 10px;"><button id="CSVDownload" class="c-button  c-button--red c-button--large  c-button--pointer">Download CSV</button><button id="statistics" style="margin: 0" class="c-button  c-button--red c-button--large  c-button--pointer">Statistieken weergeven</button></div>');
    const CSVbtn = document.querySelector('#CSVDownload');
    CSVbtn.addEventListener('click', async () => {
        const stop = startDownload('CSVDownload');
        const reacties = await fetchReacties();
        stop();
        download('CentradaHistorischeReacties.csv', convertReactiesToCSV(reacties));
    });
    const statsBtn = document.querySelector('#statistics');
    statsBtn.addEventListener('click', () => {
        statsBtn.disabled = true;
        showStatistics()
    });
}

let _statistiek;

async function showStatistics() {
    const stop = startDownload('statistics');
    const reacties = (await fetchReacties()).filter(o => o.eindpositie !== 0);
    const lotingen = reacties.filter(filters.lotingen);
    const inschrijfduur = reacties.filter(filters.inschrijfduur);
    let data = inschrijfduur.map(o => [+(new Date(o.publstop)), o.eindpositie / o.aantalreacties]);

    // https://math.stackexchange.com/questions/204020/what-is-the-equation-used-to-calculate-a-linear-trendline
    const sumXtimesY = data.reduce(((acc, cur) => acc + (cur[0] * cur[1])), 0);
    const sumX = data.reduce(((acc, cur) => acc + cur[0]), 0);
    const sumY = data.reduce(((acc, cur) => acc + cur[1]), 0);
    const sumXsquared = data.reduce(((acc, cur) => acc + (cur[0] ** 2)), 0);
    const slope = ((data.length * sumXtimesY) - (sumX * sumY)) / ((data.length * sumXsquared) - (sumX ** 2));
    const offset = (sumY - (slope * sumX)) / data.length;
    const trendX = (-offset) / slope;
    const firstX = data.reduce(((acc, cur) => Math.min(acc, cur[0])), Number.MAX_VALUE);
    const trendY = (slope * firstX) + offset;

    const eersteDatum = new Date(oudsteReactie(reacties).datumreactie);
    const maanden = ['Januari', 'Februari', 'Maart', 'April', 'Mei', 'Juni', 'Juli', 'Augustus', 'September', 'Oktober', 'November', 'December'];

    const statistics = [
        [`Voorspelde wachttijd om een inschrijfduur woning te krijgen sinds ${maanden[eersteDatum.getMonth()]} ${eersteDatum.getFullYear()} (datum van je eerste reactie)`, formatPeriode(Math.floor((trendX - (+eersteDatum)) / 1000 / 60 / 60 / 24))],
        ['Voorspelde betaalde registratiekosten bij het krijgen van je eerste inschrijfwoning', `€${15 * Math.ceil((trendX - (+eersteDatum)) / 1000 / 60 / 60 / 24 / 365)},00`],
        ['Aantal reacties op woningen', reacties.length],
        ['Aantal woningen op inschrijfduur', inschrijfduur.length],
        ['Aantal woningen op loting', lotingen.length],
        ['Inschrijfduur/Loting ratio', woningTypeRatio(reacties, 'Inschrijfduur', 'Loting').join(' / ')],
        ['Laagst behaalde positie', laagstePositie(reacties)],
        ['Aantal keer in de top 5%', reacties.filter(filters.top(.05)).length],
        ['Aantal keer in de top 10%', reacties.filter(filters.top(.1)).length],
        ['Aantal keer in de top 25%', reacties.filter(filters.top(.25)).length],
        ['Aantal keer in de top 50%', reacties.filter(filters.top(.5)).length],
        ['Gemiddeld aantal reacties per woning totaal/inschrijfduur/loting', [
            gemiddeldAantalReacties(reacties),
            gemiddeldAantalReacties(inschrijfduur),
            gemiddeldAantalReacties(lotingen)
        ].join(' / ')],
        ['Gemiddelde positie totaal/inschrijfduur/loting', [
            gemiddeldePositie(reacties),
            gemiddeldePositie(inschrijfduur),
            gemiddeldePositie(lotingen)
        ].join(' / ')],
        ['Gemiddelde positie ten opzichte van aantal reacties totaal/inschrijfduur/loting', [
            gemiddeldeRelatievePositie(reacties),
            gemiddeldeRelatievePositie(inschrijfduur),
            gemiddeldeRelatievePositie(lotingen)
        ].join(' / ')],
        ['Periode op de wachtlijst sinds 1e reactie', tijdTussenNuEnOudsteReactie(reacties) ],
    ];
    document.querySelector('#statistics-script').insertAdjacentHTML('afterend', `
    <div id="chart" style="width: 100%; margin-top: 20px; height: 200px;"></div>
    <table class="u-full-width">
  <tbody>
    <tr>
      <th>Statistiek</th>
      <th>Waarde</th>
    </tr>
  </tbody>
  ${statistics.map(o => `<tr>${o.map(d => `<td style="color: #4a4a4a;">${d}</td>`).join('\n')}</tr>`).join('\n')}
</table><a id="StatDownload" href="javascript:void(0)" class="c-thumblist__link">Download Statistieken als CSV</a>`);
    let script = document.createElement('script');
    script.onload = function () {
        let options = {
            chart: {
                type: 'line',
                height: 500,
                toolbar: {
                    show: false
                },
                zoom: {
                    enabled: false
                },
            },
            stroke: {
                width: 2
            },
            series: [{
                name: 'Top % van reacties bij inschrijfduur',
                data
            },
                     {
                         name: 'Trend',
                         data: [
                             [firstX, trendY],
                             [trendX, 0]
                         ]
                     }],
            xaxis: {
                type: 'datetime',
                max: trendX
            },
            yaxis: {
                min: 0,
                max: 1,
                tickAmount: 4,
                decimalsInFloat: 2
            },
            annotations: {
                xaxis: [
                    {
                        x: trendX,
                        strokeDashArray: 0,
                        borderColor: "#775DD0",
                        label: {
                            borderColor: "#775DD0",
                            style: {
                                color: "#fff",
                                background: "#775DD0"
                            },
                            text: (new Date(trendX)).toDateString()
                        }
                    }
                ]
            }
        }

        let chart = new ApexCharts(document.querySelector("#chart"), options);

        chart.render();
    };
    script.src = "https://cdn.jsdelivr.net/npm/apexcharts";

    document.head.appendChild(script);
    stop();
    _statistiek = statistics;
    document.querySelector('#StatDownload').addEventListener('click', async () => {
        download('CentradaHistorischeReactiesStatistiek.csv', ['Statistiek', 'Waarde'].join(';') + '\n' + statistics.map(arr => arr.join(';')).join('\n'));
    });
}

// Give visual feedback to show the user that the request is still ongoing.
function startDownload(id) {
    const el = document.querySelector(`#${id}`);
    const defaultText = el.innerText;
    const defaultState = el.disabled;
    let isDownloading = true;
    (async () => {
        el.disabled = true;
        el.innerText = 'Downloading';
        let dotsCount = 0;
        while (isDownloading) {
            await new Promise(res => setTimeout(res, 300));
            dotsCount++;
            if (dotsCount > 3) dotsCount = 0;
            el.innerText = `Downloading${new Array(dotsCount).fill('.').join('')}`;
        }
        el.innerText = defaultText;
        el.disabled = defaultState;
    })();
    return function stop() {
        isDownloading = false;
    }
}

// Fetch past reactions, authorization is done with cookies that are already present in the browser.
function fetchPage(count = 100, page = 1) {
    return fetch('/WebServices/Reacties.asmx/GetHistorischeReacties', {method: 'POST', body: JSON.stringify({aantalreacties: count, pagenumber: page}), headers: {'content-type': 'application/json'}});
}

function fetchOnGoing() {
    return fetch('/Webservices/Reacties.asmx/GetReactiesInBehandeling', {method: 'POST', headers: {'content-type': 'application/json'}});
}

let _reacties;

// In case there's more than 100 reactions, gather all from pagination.
async function fetchReacties() {
    if (_reacties) return _reacties;
    const count = 100;
    let page = 0;
    let reacties = ((await (await fetchOnGoing()).json()).d.reacties) || [];
    const offset = reacties.length;
    while (page * count === reacties.length - offset) {
        reacties = [...reacties, ...((await (await fetchPage(count, ++page)).json()).d.reacties)];
    }
    _reacties = reacties;
    return reacties;
}

function convertReactiesToCSV(reacties) {
    return ['wijk', 'adres', 'verdeelmethode', 'aantal reacties', 'positie', 'datum'].join(';') + '\n' +
        reacties.map(o => [o.wijk, o.adres, o.verdeelmethode, o.aantalreacties, o.eindpositie, o.publstop].join(';')).join('\n');
}

if (document.readyState !== 'loading') {
    insertButton();
} else {
    document.addEventListener('DOMContentLoaded', insertButton);
}

// =============================================================================================
//
//                                          Statistieken
//
// =============================================================================================

function gemiddeldAantalReacties(reacties) {
    return Math.round(reacties.reduce(((acc, cur) => acc + cur.aantalreacties), 0) / reacties.length);
}

function gemiddeldePositie(reacties) {
    return Math.round(reacties.reduce(((acc, cur) => acc + cur.eindpositie), 0) / reacties.length);
}

function gemiddeldeRelatievePositie(reacties) {
    const percentages = reacties.map(o => o.eindpositie / o.aantalreacties);
    const average = percentages.reduce((acc, cur) => acc + cur) / reacties.length;
    return Math.round(average * 100) + '%';
}

function oudsteReactie(reacties) {
    return reacties.reduce(((acc, cur) => {
        const smallest = Math.min(new Date(acc.publstop), new Date(cur.publstop))
        if (+(new Date(acc.publstop)) === smallest) return acc;
        return cur;
    }));
}

function formatPeriode(dagen) {
    const jaren = Math.floor(dagen / 365);
    dagen %= 365;
    return `${jaren > 0 ? `${jaren} jaar ${dagen > 0 ? 'en ': ''}`: ''}${dagen > 0 || jaren === 0 ? `${dagen} ${dagen === 1 ? 'dag':'dagen'}`:''}`
}

function tijdTussenNuEnOudsteReactie(reacties) {
    let dagen = Math.floor((new Date() - new Date(oudsteReactie(reacties).datumreactie)) / 1000 / 60 / 60 / 24);
    return formatPeriode(dagen);
}

function woningTypeRatio(reacties, ...woningTypes) {
    return woningTypes.map(type => `${Math.round(reacties.filter(reactie => reactie.verdeelmethode === type).length / reacties.length * 100)}%`);
}

function laagstePositie(reacties) {
    return reacties.reduce(((acc, cur) => Math.min(cur.eindpositie, acc)), 1000);
}