您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Krijg inzicht in de data rondom je reacties op Centrada.
当前为
// ==UserScript== // @name Centrada Statistieken // @namespace http://tampermonkey.net/ // @version 2025-03-21 // @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); const statistics = [ ['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 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; 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 tijdTussenNuEnOudsteReactie(reacties) { let dagen = Math.floor((new Date() - new Date(oudsteReactie(reacties).datumreactie)) / 1000 / 60 / 60 / 24); 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 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); }