This repository has been archived on 2021-05-25. You can view files and clone it, but cannot push or open issues or pull requests.
pyRCV2/html/index.js

405 lines
17 KiB
JavaScript

/*
pyRCV2: Preferential vote counting
Copyright © 2020–2021 Lee Yingtong Li (RunasSudo)
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
function clickAdvancedOptions() {
if (document.getElementById('divAdvancedOptions').style.display === 'none') {
document.getElementById('divAdvancedOptions').style.display = 'block';
document.getElementById('btnAdvancedOptions').innerHTML = 'Hide advanced options';
} else {
document.getElementById('divAdvancedOptions').style.display = 'none';
document.getElementById('btnAdvancedOptions').innerHTML = 'Show advanced options';
}
}
function changePreset() {
if (document.getElementById('selPreset').value === 'scottish') {
document.getElementById('selQuotaCriterion').value = 'geq';
document.getElementById('selQuota').value = 'droop';
document.getElementById('selQuotaMode').value = 'static';
document.getElementById('chkBulkElection').checked = true;
document.getElementById('chkBulkExclusion').checked = false;
document.getElementById('chkDeferSurpluses').checked = false;
document.getElementById('selNumbers').value = 'fixed';
document.getElementById('txtDP').value = '5';
document.getElementById('chkRoundQuota').checked = true;
document.getElementById('txtRoundQuota').value = '0';
document.getElementById('chkRoundVotes').checked = false;
document.getElementById('chkRoundTVs').checked = false;
document.getElementById('chkRoundWeights').checked = false;
document.getElementById('selSurplus').value = 'size';
document.getElementById('selTransfers').value = 'wig';
document.getElementById('selPapers').value = 'both';
document.getElementById('selExclusion').value = 'one_round';
document.getElementById('selTies').value = 'backwards_random';
} else if (document.getElementById('selPreset').value === 'stvc') {
document.getElementById('selQuotaCriterion').value = 'gt';
document.getElementById('selQuota').value = 'droop_exact';
document.getElementById('selQuotaMode').value = 'progressive';
document.getElementById('chkBulkElection').checked = true;
document.getElementById('chkBulkExclusion').checked = true;
document.getElementById('chkDeferSurpluses').checked = false;
document.getElementById('selNumbers').value = 'rational';
document.getElementById('chkRoundQuota').checked = false;
document.getElementById('chkRoundVotes').checked = false;
document.getElementById('chkRoundTVs').checked = false;
document.getElementById('chkRoundWeights').checked = false;
document.getElementById('selSurplus').value = 'size';
document.getElementById('selTransfers').value = 'wig';
document.getElementById('selPapers').value = 'both';
document.getElementById('selExclusion').value = 'one_round';
document.getElementById('selTies').value = 'backwards_random';
} else if (document.getElementById('selPreset').value === 'senate') {
document.getElementById('selQuotaCriterion').value = 'geq';
document.getElementById('selQuota').value = 'droop';
document.getElementById('selQuotaMode').value = 'static';
document.getElementById('chkBulkElection').checked = true;
document.getElementById('chkBulkExclusion').checked = true;
document.getElementById('chkDeferSurpluses').checked = false;
document.getElementById('selNumbers').value = 'fixed';
document.getElementById('txtDP').value = '5';
document.getElementById('chkRoundQuota').checked = true;
document.getElementById('txtRoundQuota').value = '0';
document.getElementById('chkRoundVotes').checked = true;
document.getElementById('txtRoundVotes').value = '0';
document.getElementById('chkRoundTVs').checked = false;
document.getElementById('chkRoundWeights').checked = false;
document.getElementById('selSurplus').value = 'order';
document.getElementById('selTransfers').value = 'uig';
document.getElementById('selPapers').value = 'both';
document.getElementById('selExclusion').value = 'by_value';
document.getElementById('selTies').value = 'backwards_random';
} else if (document.getElementById('selPreset').value === 'wright') {
document.getElementById('selQuotaCriterion').value = 'geq';
document.getElementById('selQuota').value = 'droop';
document.getElementById('selQuotaMode').value = 'static';
document.getElementById('chkBulkElection').checked = true;
document.getElementById('chkBulkExclusion').checked = true;
document.getElementById('chkDeferSurpluses').checked = false;
document.getElementById('selNumbers').value = 'fixed';
document.getElementById('txtDP').value = '5';
document.getElementById('chkRoundQuota').checked = true;
document.getElementById('txtRoundQuota').value = '0';
document.getElementById('chkRoundVotes').checked = false;
document.getElementById('chkRoundTVs').checked = false;
document.getElementById('chkRoundWeights').checked = false;
document.getElementById('selSurplus').value = 'size';
document.getElementById('selTransfers').value = 'wig';
document.getElementById('selPapers').value = 'both';
document.getElementById('selExclusion').value = 'wright';
document.getElementById('selTies').value = 'backwards_random';
} else if (document.getElementById('selPreset').value === 'prsa77') {
document.getElementById('selQuotaCriterion').value = 'geq';
document.getElementById('selQuota').value = 'droop';
document.getElementById('selQuotaMode').value = 'static';
document.getElementById('chkBulkElection').checked = true;
document.getElementById('chkBulkExclusion').checked = false;
document.getElementById('chkDeferSurpluses').checked = true;
document.getElementById('selNumbers').value = 'fixed';
document.getElementById('txtDP').value = '5';
document.getElementById('chkRoundQuota').checked = true;
document.getElementById('txtRoundQuota').value = '3';
document.getElementById('chkRoundVotes').checked = true;
document.getElementById('txtRoundVotes').value = '3';
document.getElementById('chkRoundTVs').checked = true;
document.getElementById('txtRoundTVs').value = '3';
document.getElementById('chkRoundWeights').checked = true;
document.getElementById('txtRoundWeights').value = '3';
document.getElementById('selSurplus').value = 'order';
document.getElementById('selTransfers').value = 'eg';
document.getElementById('selPapers').value = 'transferable';
document.getElementById('selExclusion').value = 'parcels_by_order';
document.getElementById('selTies').value = 'backwards_random';
} else if (document.getElementById('selPreset').value === 'ers97') {
document.getElementById('selQuotaCriterion').value = 'geq';
document.getElementById('selQuota').value = 'droop_exact';
document.getElementById('selQuotaMode').value = 'ers97';
document.getElementById('chkBulkElection').checked = true;
document.getElementById('chkBulkExclusion').checked = true;
document.getElementById('chkDeferSurpluses').checked = true;
document.getElementById('selNumbers').value = 'fixed';
document.getElementById('txtDP').value = '5';
document.getElementById('chkRoundQuota').checked = true;
document.getElementById('txtRoundQuota').value = '2';
document.getElementById('chkRoundVotes').checked = true;
document.getElementById('txtRoundVotes').value = '2';
document.getElementById('chkRoundTVs').checked = true;
document.getElementById('txtRoundTVs').value = '2';
document.getElementById('chkRoundWeights').checked = true;
document.getElementById('txtRoundTVs').value = '2';
document.getElementById('selSurplus').value = 'size';
document.getElementById('selTransfers').value = 'eg';
document.getElementById('selPapers').value = 'transferable';
document.getElementById('selExclusion').value = 'by_value';
document.getElementById('selTies').value = 'backwards_random';
}
}
async function clickCount() {
// Read BLT file
let bltFile = document.getElementById('bltFile').files[0];
let text = await bltFile.text();
// Initialise table rows
let tblResults = document.getElementById('result');
tblResults.innerHTML = '';
let candMap = {}; // candidate name -> rows
// Step election
let worker = new Worker('worker.js');
let election, elComment, elExhausted1, elExhausted2, elLTF1, elLTF2, elTotal, elQuota;
worker.onmessage = function(evt) {
if (evt.data.type === 'require_input') {
let input = window.prompt(evt.data.message);
if (input !== null) {
worker.postMessage({'type': 'require_input', 'input': input});
}
}
if (evt.data.type === 'stv_exception') {
window.alert('An error occurred while counting the votes:\n\n' + evt.data.message);
}
if (evt.data.type === 'init') {
election = evt.data.election;
// Comment row
elComment = document.createElement('tr');
let elTd = document.createElement('td');
elComment.appendChild(elTd);
tblResults.appendChild(elComment);
// Candidates
for (let candidate of election.candidates) {
let elTr1 = document.createElement('tr');
let elTr2 = document.createElement('tr');
elTd = document.createElement('td');
elTd.setAttribute('rowspan', '2');
elTd.style.borderTop = '1px solid black';
elTd.innerText = candidate;
elTr1.appendChild(elTd);
tblResults.appendChild(elTr1);
tblResults.appendChild(elTr2);
candMap[candidate] = [elTr1, elTr2];
}
// Exhausted votes row
elExhausted1 = document.createElement('tr');
elExhausted1.classList.add('info');
elExhausted2 = document.createElement('tr');
elExhausted2.classList.add('info');
elTd = document.createElement('td');
elTd.setAttribute('rowspan', '2');
elTd.style.borderTop = '1px solid black';
elTd.innerText = 'Exhausted';
elExhausted1.appendChild(elTd);
tblResults.appendChild(elExhausted1);
tblResults.appendChild(elExhausted2);
// Loss to fraction row
elLTF1 = document.createElement('tr');
elLTF1.classList.add('info');
elLTF2 = document.createElement('tr');
elLTF2.classList.add('info');
elTd = document.createElement('td');
elTd.setAttribute('rowspan', '2');
elTd.style.borderTop = '1px solid black';
elTd.innerText = 'Loss to fraction';
elLTF1.appendChild(elTd);
tblResults.appendChild(elLTF1);
tblResults.appendChild(elLTF2);
// Total row
elTotal = document.createElement('tr');
elTotal.classList.add('info');
elTd = document.createElement('td');
elTd.style.borderTop = '1px solid black';
elTd.innerText = 'Total';
elTotal.appendChild(elTd);
tblResults.appendChild(elTotal);
// Quota row
elQuota = document.createElement('tr');
elQuota.classList.add('info');
elTd = document.createElement('td');
elTd.style.borderTop = '1px solid black';
elTd.style.borderBottom = '1px solid black';
elTd.innerText = 'Quota';
elQuota.appendChild(elTd);
tblResults.appendChild(elQuota);
}
if (evt.data.type === 'result') {
let result = evt.data.result;
// Display results
elTd = document.createElement('td');
elTd.innerText = result.stage + '. ' + result.comment;
elComment.appendChild(elTd);
for (let [candidate, countCard] of result.candidates) {
[elTr1, elTr2] = candMap[candidate];
elTd = document.createElement('td');
elTd.classList.add('count');
if (countCard.state === py.pyRCV2.model.CandidateState.WITHDRAWN || countCard.state === py.pyRCV2.model.CandidateState.EXCLUDED || countCard.state === py.pyRCV2.model.CandidateState.EXCLUDING) {
elTd.classList.add('excluded');
} else if (countCard.state === py.pyRCV2.model.CandidateState.ELECTED || countCard.state === py.pyRCV2.model.CandidateState.PROVISIONALLY_ELECTED || countCard.state === py.pyRCV2.model.CandidateState.DISTRIBUTING_SURPLUS) {
elTd.classList.add('elected');
}
elTd.style.borderTop = '1px solid black';
elTd.innerHTML = ppVotes(countCard.transfers);
elTr1.appendChild(elTd);
elTd = document.createElement('td');
elTd.classList.add('count');
if (countCard.state === py.pyRCV2.model.CandidateState.ELECTED || countCard.state === py.pyRCV2.model.CandidateState.PROVISIONALLY_ELECTED || countCard.state === py.pyRCV2.model.CandidateState.DISTRIBUTING_SURPLUS) {
elTd.classList.add('elected');
elTd.innerHTML = ppVotes(countCard.votes);
elTr1.querySelector('td:first-child').classList.add('elected');
} else {
elTr1.querySelector('td:first-child').classList.remove('elected');
if (countCard.state === py.pyRCV2.model.CandidateState.WITHDRAWN) {
elTd.classList.add('excluded');
elTd.innerText = 'WD';
} else if (countCard.state === py.pyRCV2.model.CandidateState.EXCLUDED) {
elTd.classList.add('excluded');
elTd.innerText = 'Ex';
} else if (countCard.state === py.pyRCV2.model.CandidateState.EXCLUDING) {
elTd.classList.add('excluded');
elTd.innerHTML = ppVotes(countCard.votes);
} else {
elTd.innerHTML = ppVotes(countCard.votes);
}
}
elTr2.appendChild(elTd);
}
// Display exhausted votes
elTd = document.createElement('td');
elTd.classList.add('count');
elTd.style.borderTop = '1px solid black';
elTd.innerHTML = ppVotes(result.exhausted.transfers);
elExhausted1.appendChild(elTd);
elTd = document.createElement('td');
elTd.classList.add('count');
elTd.innerHTML = ppVotes(result.exhausted.votes);
elExhausted2.appendChild(elTd);
// Display loss to fraction
elTd = document.createElement('td');
elTd.classList.add('count');
elTd.style.borderTop = '1px solid black';
elTd.innerHTML = ppVotes(result.loss_fraction.transfers);
elLTF1.appendChild(elTd);
elTd = document.createElement('td');
elTd.classList.add('count');
elTd.innerHTML = ppVotes(result.loss_fraction.votes);
elLTF2.appendChild(elTd);
// Display total
elTd = document.createElement('td');
elTd.classList.add('count');
elTd.style.borderTop = '1px solid black';
elTd.innerHTML = ppVotes(result.total);
elTotal.appendChild(elTd);
// Display quota
elTd = document.createElement('td');
elTd.classList.add('count');
elTd.style.borderTop = '1px solid black';
elTd.style.borderBottom = '1px solid black';
elTd.innerHTML = ppVotes(result.quota);
elQuota.appendChild(elTd);
}
}
worker.onerror = function(evt) {
window.alert('An unknown error occurred while counting the votes. More details may be available in the browser\'s developer console.');
throw evt;
}
worker.postMessage({
'type': 'init',
'data': {
'numbers': document.getElementById('selNumbers').value,
'fixedDPs': parseInt(document.getElementById('txtDP').value),
'transfers': document.getElementById('selTransfers').value,
'options': {
'quota_criterion': document.getElementById('selQuotaCriterion').value,
'quota': document.getElementById('selQuota').value,
'quota_mode': document.getElementById('selQuotaMode').value,
'bulk_elect': document.getElementById('chkBulkElection').checked,
'bulk_exclude': document.getElementById('chkBulkExclusion').checked,
'defer_surpluses': document.getElementById('chkDeferSurpluses').checked,
'surplus_order': document.getElementById('selSurplus').value,
'papers': document.getElementById('selPapers').value,
'exclusion': document.getElementById('selExclusion').value,
'ties': document.getElementById('selTies').value,
'round_quota': document.getElementById('chkRoundQuota').checked ? parseInt(document.getElementById('txtRoundQuota').value) : null,
'round_votes': document.getElementById('chkRoundVotes').checked ? parseInt(document.getElementById('txtRoundVotes').value) : null,
'round_tvs': document.getElementById('chkRoundTVs').checked ? parseInt(document.getElementById('txtRoundTVs').value) : null,
'round_weights': document.getElementById('chkRoundWeights').checked ? parseInt(document.getElementById('txtRoundWeights').value) : null,
},
'seed': document.getElementById('txtSeed').value,
'data': text
}
});
// Pretty printing helper functions
let ppDPs = 2;
if (document.getElementById('chkRoundVotes').checked) {
let ppDPs2 = parseInt(document.getElementById('txtRoundVotes').value);
if (ppDPs2 < ppDPs) {
ppDPs = ppDPs2;
}
}
if (document.getElementById('selNumbers').value === 'fixed') {
let ppDPs2 = parseInt(document.getElementById('txtDP').value);
if (ppDPs2 < ppDPs) {
ppDPs = ppDPs2;
}
}
function ppVotes(v) {
result = parseFloat(v).toFixed(ppDPs);
if (parseFloat(result) == 0) {
return '&nbsp;'
}
return result;
}
}
// Provide a default seed
if (document.getElementById('txtSeed').value === '') {
function pad(x) { if (x < 10) { return '0' + x; } return '' + x; }
let d = new Date();
document.getElementById('txtSeed').value = d.getFullYear() + pad(d.getMonth() + 1) + pad(d.getDate());
}