Visual improvements to HTML/JS interface

This commit is contained in:
RunasSudo 2021-05-25 01:12:26 +10:00
parent 5c185b386c
commit 1ad1684e67
Signed by: RunasSudo
GPG Key ID: 7234E476BF21C61A
10 changed files with 151 additions and 73 deletions

View File

@ -202,7 +202,8 @@ async function clickCount() {
// Step election
let worker = new Worker('worker.js');
let election;
let trComment, trExhausted1, trExhausted2, trLTF1, trLTF2, trTotal, trQuota, trVRE;
let trStageNo, trStageKind, trComment;
let trExhausted1, trExhausted2, trLTF1, trLTF2, trTotal, trQuota, trVRE;
let olLogs;
worker.onmessage = function(evt) {
@ -220,9 +221,22 @@ async function clickCount() {
if (evt.data.type === 'init') {
election = evt.data.election;
// Comment row
trComment = document.createElement('tr');
// Comment rows
trStageNo = document.createElement('tr');
trStageNo.classList.add('stage-no');
let elTd = document.createElement('td');
trStageNo.appendChild(elTd);
tblResults.appendChild(trStageNo);
trStageKind = document.createElement('tr');
trStageKind.classList.add('stage-kind');
elTd = document.createElement('td');
trStageKind.appendChild(elTd);
tblResults.appendChild(trStageKind);
trComment = document.createElement('tr');
trComment.classList.add('stage-comment');
elTd = document.createElement('td');
trComment.appendChild(elTd);
tblResults.appendChild(trComment);
@ -233,7 +247,7 @@ async function clickCount() {
elTd = document.createElement('td');
elTd.setAttribute('rowspan', '2');
elTd.style.borderTop = '1px solid black';
elTd.classList.add('bt');
elTd.innerText = candidate;
elTr1.appendChild(elTd);
@ -251,7 +265,7 @@ async function clickCount() {
elTd = document.createElement('td');
elTd.setAttribute('rowspan', '2');
elTd.style.borderTop = '1px solid black';
elTd.classList.add('bt');
elTd.innerText = 'Exhausted';
trExhausted1.appendChild(elTd);
@ -268,7 +282,7 @@ async function clickCount() {
elTd = document.createElement('td');
elTd.setAttribute('rowspan', '2');
elTd.style.borderTop = '1px solid black';
elTd.classList.add('bt');
elTd.innerText = 'Loss to fraction';
trLTF1.appendChild(elTd);
@ -279,7 +293,7 @@ async function clickCount() {
trTotal = document.createElement('tr');
trTotal.classList.add('info');
elTd = document.createElement('td');
elTd.style.borderTop = '1px solid black';
elTd.classList.add('bt');
elTd.innerText = 'Total';
trTotal.appendChild(elTd);
tblResults.appendChild(trTotal);
@ -288,8 +302,8 @@ async function clickCount() {
trQuota = document.createElement('tr');
trQuota.classList.add('info');
elTd = document.createElement('td');
elTd.style.borderTop = '1px solid black';
elTd.style.borderBottom = '1px solid black';
elTd.classList.add('bt');
elTd.classList.add('bb');
elTd.innerText = 'Quota';
trQuota.appendChild(elTd);
tblResults.appendChild(trQuota);
@ -299,8 +313,8 @@ async function clickCount() {
trVRE = document.createElement('tr');
trVRE.classList.add('info');
elTd = document.createElement('td');
elTd.style.borderTop = '1px solid black';
elTd.style.borderBottom = '1px solid black';
elTd.classList.add('bt');
elTd.classList.add('bb');
elTd.innerText = 'Vote required for election';
trVRE.appendChild(elTd);
tblResults.appendChild(trVRE);
@ -336,7 +350,15 @@ async function clickCount() {
// Display results
elTd = document.createElement('td');
elTd.innerText = result.stage + '. ' + result.comment;
elTd.innerText = result.stage;
trStageNo.appendChild(elTd);
elTd = document.createElement('td');
elTd.innerText = result.stage_kind;
trStageKind.appendChild(elTd);
elTd = document.createElement('td');
elTd.innerText = result.comment;
trComment.appendChild(elTd);
for (let [candidate, countCard] of result.candidates) {
@ -349,7 +371,7 @@ async function clickCount() {
} 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.classList.add('bt');
elTd.innerHTML = ppVotes(countCard.transfers);
elTr1.appendChild(elTd);
@ -383,7 +405,7 @@ async function clickCount() {
// Display exhausted votes
elTd = document.createElement('td');
elTd.classList.add('count');
elTd.style.borderTop = '1px solid black';
elTd.classList.add('bt');
elTd.innerHTML = ppVotes(result.exhausted.transfers);
trExhausted1.appendChild(elTd);
@ -395,7 +417,7 @@ async function clickCount() {
// Display loss to fraction
elTd = document.createElement('td');
elTd.classList.add('count');
elTd.style.borderTop = '1px solid black';
elTd.classList.add('bt');
elTd.innerHTML = ppVotes(result.loss_fraction.transfers);
trLTF1.appendChild(elTd);
@ -413,15 +435,15 @@ async function clickCount() {
// Display total
elTd = document.createElement('td');
elTd.classList.add('count');
elTd.style.borderTop = '1px solid black';
elTd.classList.add('bt');
elTd.innerHTML = ppVotes(result.total);
trTotal.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.classList.add('bt');
elTd.classList.add('bb');
elTd.innerHTML = ppVotes(result.quota);
trQuota.appendChild(elTd);
@ -429,8 +451,8 @@ async function clickCount() {
if (result.vote_required_election !== null) {
elTd = document.createElement('td');
elTd.classList.add('count');
elTd.style.borderTop = '1px solid black';
elTd.style.borderBottom = '1px solid black';
elTd.classList.add('bt');
elTd.classList.add('bb');
elTd.innerHTML = ppVotes(result.vote_required_election);
trVRE.appendChild(elTd);
}
@ -462,11 +484,11 @@ async function clickCount() {
elTd.innerHTML = 'ELECTED ' + countCard.order_elected;
winners.append([candidate, countCard]);
}
elTd.style.borderTop = '1px solid black';
elTd.classList.add('bt');
elTr1.appendChild(elTd);
}
elTd.style.borderBottom = '1px solid black';
elTd.classList.add('bb');
let elP = document.createElement('p');
elP.innerText = 'Count complete. The winning candidates are, in order of election:'
@ -541,6 +563,7 @@ async function clickCount() {
if (result.indexOf('.') >= 0) {
result = result.substring(0, result.indexOf('.')) + '<sup>' + result.substring(result.indexOf('.')) + '</sup>';
}
result = result.replace('-', '−');
return result;
}
}

View File

@ -16,14 +16,25 @@
along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
@import url('https://fonts.googleapis.com/css2?family=Source+Sans+Pro&display=swap');
html, body {
font-family: 'Liberation Sans', FreeSans, Helvetica, Arial, sans-serif;
font-family: 'Source Sans Pro', sans-serif;
}
body {
padding: 0.5em;
}
a {
color: #1d46c4;
text-decoration: none;
}
a:hover {
color: #1d3da2;
text-decoration: underline;
}
/* Menu styling */
.menudiv {
@ -69,24 +80,41 @@ td.count sup {
font-size: 0.6rem;
top: 0;
}
.result tr:first-child td {
vertical-align: bottom;
tr.stage-no td, tr.stage-kind td, tr.stage-comment td {
text-align: center;
}
tr.stage-no td:not(:first-child) {
border-top: 1px solid #76858c;
}
tr.stage-kind td:not(:first-child) {
font-size: 0.75em;
min-width: 5rem;
color: #1b2839;
background-color: #f0f5fb;
color-adjust: exact;
-webkit-print-color-adjust: exact;
}
td.excluded {
background-color: #fecfcfff;
background-color: #fde2e2;
color-adjust: exact;
-webkit-print-color-adjust: exact;
}
td.elected {
background-color: #d1fca7ff;
background-color: #e0fdc5;
color-adjust: exact;
-webkit-print-color-adjust: exact;
}
tr.info td {
background-color: #edededff;
background-color: #f0f5fb;
color-adjust: exact;
-webkit-print-color-adjust: exact;
}
td.bt {
border-top: 1px solid #76858c;
}
td.bb {
border-bottom: 1px solid #76858c;
}
/* BLT input tool */

View File

@ -145,6 +145,7 @@ function handleException(ex) {
function resultToJS(result) {
return {
'stage': stage,
'stage_kind': result.stage_kind,
'comment': result.comment,
'logs': result.logs,
'candidates': result.candidates.py_items().map(([c, cc]) => [c.py_name, {

View File

@ -61,7 +61,10 @@ def add_parser(subparsers):
parser.add_argument('--pp-decimals', type=int, default=2, help='print votes to specified decimal places in results report (default: 2)')
def print_step(args, stage, result):
print('{}. {}'.format(stage, result.comment))
if result.stage_kind:
print('{}. {} {}'.format(stage, result.stage_kind, result.comment))
else:
print('{}. {}'.format(stage, result.comment))
if result.logs:
print(' '.join(result.logs))

View File

@ -99,7 +99,7 @@ class BaseSTVCounter:
self.compute_quota()
self.elect_meeting_quota()
return self.make_result('First preferences')
return self.make_result(None, 'First preferences')
def distribute_first_preferences(self):
"""
@ -180,6 +180,7 @@ class BaseSTVCounter:
if self.num_elected >= self.election.seats:
__pragma__('opov')
return CountCompleted(
None,
'Count complete',
self.logs,
self.candidates,
@ -193,7 +194,6 @@ class BaseSTVCounter:
# Are there just enough candidates to fill all the seats?
if self.options['bulk_elect']:
# Include EXCLUDING to avoid interrupting an exclusion
if len(self.election.candidates) - self.num_withdrawn - self.num_excluded + sum(1 for c, cc in self.candidates.items() if cc.state == CandidateState.EXCLUDING) <= self.election.seats:
# Declare elected all remaining candidates
candidates_elected = [(c, cc) for c, cc in self.candidates.items() if cc.state == CandidateState.HOPEFUL or cc.state == CandidateState.GUARDED]
@ -212,7 +212,7 @@ class BaseSTVCounter:
constraints.stabilise_matrix(self)
self.logs.extend(constraints.guard_or_doom(self))
return self.make_result('Bulk election')
return self.make_result(None, 'Bulk election')
def can_defer_surpluses(self, has_surplus):
"""
@ -301,7 +301,7 @@ class BaseSTVCounter:
self.elect_meeting_quota()
return self.make_result('Surplus of ' + candidate_surplus.name)
return self.make_result('Surplus of', candidate_surplus.name)
def do_surplus(self, candidate_surplus, count_card, surplus):
"""
@ -317,7 +317,8 @@ class BaseSTVCounter:
# If we did not perform bulk election in before_surpluses: Are there just enough candidates to fill all the seats?
if not self.options['bulk_elect']:
if len(self.election.candidates) - self.num_withdrawn - self.num_excluded <= self.election.seats:
# Include EXCLUDING to avoid interrupting an exclusion
if len(self.election.candidates) - self.num_withdrawn - self.num_excluded + sum(1 for c, cc in self.candidates.items() if cc.state == CandidateState.EXCLUDING) <= self.election.seats:
# Declare elected one remaining candidate at a time
hopefuls = [(c, cc) for c, cc in self.candidates.items() if cc.state == CandidateState.HOPEFUL or cc.state == CandidateState.GUARDED]
hopefuls.sort(key=lambda x: x[1].votes, reverse=True)
@ -344,7 +345,7 @@ class BaseSTVCounter:
else:
self.logs.append(self.pretty_join(order_elected) + ' are elected to fill the remaining vacancies.')
return self.make_result('Bulk election')
return self.make_result(None, 'Bulk election')
def exclude_doomed(self):
"""
@ -369,13 +370,21 @@ class BaseSTVCounter:
Exclude the lowest ranked hopeful(s)
"""
candidates_excluded = self.candidates_to_exclude()
# Continue current exclusion if applicable
if self._exclusion is not None:
self.logs.append('Continuing exclusion of ' + self.pretty_join([c.name for c, cc in self._exclusion[0]]) + '.')
__pragma__('opov')
candidates_excluded = self._exclusion[0]
__pragma__('noopov')
else:
candidates_excluded = self.candidates_to_exclude()
if len(candidates_excluded) > 0:
if len(candidates_excluded) == 1:
self.logs.append('No surpluses to distribute, so ' + candidates_excluded[0][0].name + ' is excluded.')
else:
self.logs.append('No surpluses to distribute, so ' + self.pretty_join([c.name for c, cc in candidates_excluded]) + ' are excluded.')
if len(candidates_excluded) > 0:
if len(candidates_excluded) == 1:
self.logs.append('No surpluses to distribute, so ' + candidates_excluded[0][0].name + ' is excluded.')
else:
self.logs.append('No surpluses to distribute, so ' + self.pretty_join([c.name for c, cc in candidates_excluded]) + ' are excluded.')
return self.exclude_candidates(candidates_excluded)
def exclude_candidates(self, candidates_excluded):
@ -434,7 +443,7 @@ class BaseSTVCounter:
self.elect_meeting_quota()
return self.make_result('Exclusion of ' + ', '.join([c.name for c, cc in candidates_excluded]))
return self.make_result('Exclusion of', ', '.join([c.name for c, cc in candidates_excluded]))
def candidates_to_bulk_exclude(self, hopefuls):
"""
@ -479,13 +488,6 @@ class BaseSTVCounter:
Returns List[Tuple[Candidate, CountCard]]
"""
# Continue current exclusion if applicable
if self._exclusion is not None:
self.logs.append('Continuing exclusion of ' + self.pretty_join([c.name for c, cc in self._exclusion[0]]) + '.')
__pragma__('opov')
return self._exclusion[0]
__pragma__('noopov')
hopefuls = [(c, cc) for c, cc in self.candidates.items() if cc.state == CandidateState.HOPEFUL]
hopefuls.sort(key=lambda x: x[1].votes)
@ -775,9 +777,10 @@ class BaseSTVCounter:
return num
return num.round(self.options['round_tvs'], num.ROUND_DOWN)
def make_result(self, comment):
def make_result(self, stage_kind, comment):
__pragma__('opov')
result = CountStepResult(
stage_kind,
comment,
self.logs,
self.candidates,

View File

@ -141,7 +141,7 @@ class MeekSTVCounter(BaseSTVCounter):
self.logs.append(self.total.pp(2) + ' usable votes, so the quota is ' + self.quota.pp(2) + '.')
self.elect_meeting_quota()
return self.make_result('First preferences')
return self.make_result(None, 'First preferences')
def distribute_recursively(self, tree, remaining_multiplier):
if tree.next_exhausted is None:
@ -266,7 +266,7 @@ class MeekSTVCounter(BaseSTVCounter):
# NB: We could do this earlier, but this shows the flow of the election more clearly in the count sheet
self.elect_meeting_quota()
return self.make_result('Surpluses distributed')
return self.make_result(None, 'Surpluses distributed')
def do_exclusion(self, candidates_excluded):
"""

View File

@ -155,7 +155,8 @@ class CountCard:
__pragma__('noopov')
class CountStepResult:
def __init__(self, comment, logs, candidates, exhausted, loss_fraction, total, quota, vote_required_election):
def __init__(self, stage_kind, comment, logs, candidates, exhausted, loss_fraction, total, quota, vote_required_election):
self.stage_kind = stage_kind
self.comment = comment
self.logs = logs
@ -176,7 +177,7 @@ class CountStepResult:
candidates[c] = cc.clone()
__pragma__('noopov')
return CountStepResult(self.comment, candidates, self.exhausted.clone(), self.loss_fraction.clone(), self.total, self.quota)
return CountStepResult(self.stage_kind, self.comment, candidates, self.exhausted.clone(), self.loss_fraction.clone(), self.total, self.quota)
class CountCompleted(CountStepResult):
pass

View File

@ -60,7 +60,8 @@ def test_aec_tas19():
result = counter.step()
comment = data[1][i]
assert result.comment == comment, 'Failed to verify comment'
result_comment = (result.stage_kind + ' ' + result.comment) if result.stage_kind else result.comment
assert result_comment == comment, 'Failed to verify comment'
for j, cand in enumerate(candidates):
votes = pyRCV2.numbers.Num(data[j + 2][i])

View File

@ -67,7 +67,8 @@ def test_ers97_py():
result = counter.step()
comment = data[1][i]
assert result.comment == comment, 'Failed to verify comment'
result_comment = (result.stage_kind + ' ' + result.comment) if result.stage_kind else result.comment
assert result_comment == comment, 'Failed to verify comment'
for j, cand in enumerate(candidates):
votes = pyRCV2.numbers.Num(data[j + 2][i])
@ -112,7 +113,8 @@ def test_ers97_js():
assert ctx.eval('result = counter.step();')
comment = data[1][i]
assert ctx.eval('result.comment') == comment, 'Failed to verify comment'
result_comment = ctx.eval('result.stage_kind ? (result.stage_kind + " " + result.comment) : result.comment')
assert result_comment == comment, 'Failed to verify comment'
for j, cand in enumerate(candidates):
ctx.eval('votes = py.pyRCV2.numbers.Num("{}");'.format(data[j + 2][i]))

View File

@ -69,7 +69,8 @@ def test_prsa1():
# Stage 2
result = counter.step()
assert result.comment == 'Surplus of Grey'
assert result.stage_kind == 'Surplus of'
assert result.comment == 'Grey'
assert isclose(result.candidates[c_evans].votes, 2234)
assert isclose(result.candidates[c_grey].votes, 13001)
assert isclose(result.candidates[c_thomson].votes, 7468)
@ -81,7 +82,8 @@ def test_prsa1():
# Stage 3
result = counter.step()
assert result.comment == 'Surplus of Ames'
assert result.stage_kind == 'Surplus of'
assert result.comment == 'Ames'
assert isclose(result.candidates[c_evans].votes, 3038)
assert isclose(result.candidates[c_grey].votes, 13001)
assert isclose(result.candidates[c_thomson].votes, 8674)
@ -93,7 +95,8 @@ def test_prsa1():
# Stage 4
result = counter.step()
assert result.comment == 'Surplus of Spears'
assert result.stage_kind == 'Surplus of'
assert result.comment == 'Spears'
assert isclose(result.candidates[c_evans].votes, 4823)
assert isclose(result.candidates[c_grey].votes, 13001)
assert isclose(result.candidates[c_thomson].votes, 8674)
@ -105,25 +108,29 @@ def test_prsa1():
# Stage 5
result = counter.step()
assert result.comment == 'Exclusion of Reid'
assert result.stage_kind == 'Exclusion of'
assert result.comment == 'Reid'
assert isclose(result.candidates[c_reid].transfers, -1000)
assert isclose(result.candidates[c_white].transfers, 1000)
# Stage 6
result = counter.step()
assert result.comment == 'Exclusion of Reid'
assert result.stage_kind == 'Exclusion of'
assert result.comment == 'Reid'
assert isclose(result.candidates[c_reid].transfers, -617)
assert isclose(result.candidates[c_white].transfers, 617)
# Stage 7
result = counter.step()
assert result.comment == 'Exclusion of Reid'
assert result.stage_kind == 'Exclusion of'
assert result.comment == 'Reid'
assert isclose(result.candidates[c_reid].transfers, -402)
assert isclose(result.candidates[c_evans].transfers, 402)
# Stage 8
result = counter.step()
assert result.comment == 'Exclusion of Reid'
assert result.stage_kind == 'Exclusion of'
assert result.comment == 'Reid'
assert isclose(result.candidates[c_reid].transfers, -1785)
assert isclose(result.candidates[c_evans].transfers, 595)
assert isclose(result.candidates[c_thomson].transfers, 1190)
@ -139,39 +146,45 @@ def test_prsa1():
# Stage 9
result = counter.step()
assert result.comment == 'Exclusion of Evans'
assert result.stage_kind == 'Exclusion of'
assert result.comment == 'Evans'
assert isclose(result.candidates[c_evans].transfers, -1000)
assert isclose(result.candidates[c_thomson].transfers, 1000)
# Stage 10
result = counter.step()
assert result.comment == 'Exclusion of Evans'
assert result.stage_kind == 'Exclusion of'
assert result.comment == 'Evans'
assert isclose(result.candidates[c_evans].transfers, -1234)
assert isclose(result.exhausted.transfers, 1234)
# Stage 11
result = counter.step()
assert result.comment == 'Exclusion of Evans'
assert result.stage_kind == 'Exclusion of'
assert result.comment == 'Evans'
assert isclose(result.candidates[c_evans].transfers, -804)
assert isclose(result.candidates[c_thomson].transfers, 402)
assert isclose(result.candidates[c_white].transfers, 402)
# Stage 12
result = counter.step()
assert result.comment == 'Exclusion of Evans'
assert result.stage_kind == 'Exclusion of'
assert result.comment == 'Evans'
assert isclose(result.candidates[c_evans].transfers, -1785)
assert isclose(result.candidates[c_white].transfers, 1190)
assert isclose(result.exhausted.transfers, 595)
# Stage 13
result = counter.step()
assert result.comment == 'Exclusion of Evans'
assert result.stage_kind == 'Exclusion of'
assert result.comment == 'Evans'
assert isclose(result.candidates[c_evans].transfers, -402)
assert isclose(result.candidates[c_thomson].transfers, 402)
# Stage 14
result = counter.step()
assert result.comment == 'Exclusion of Evans'
assert result.stage_kind == 'Exclusion of'
assert result.comment == 'Evans'
assert isclose(result.candidates[c_evans].transfers, -595)
assert isclose(result.candidates[c_white].transfers, 595)
@ -186,19 +199,22 @@ def test_prsa1():
# Stage 15
result = counter.step()
assert result.comment == 'Exclusion of Thomson'
assert result.stage_kind == 'Exclusion of'
assert result.comment == 'Thomson'
assert isclose(result.candidates[c_thomson].transfers, -5000)
assert isclose(result.exhausted.transfers, 5000)
# Stage 16
result = counter.step()
assert result.comment == 'Exclusion of Thomson'
assert result.stage_kind == 'Exclusion of'
assert result.comment == 'Thomson'
assert isclose(result.candidates[c_thomson].transfers, -2468)
assert isclose(result.exhausted.transfers, 2468)
# Stage 17
result = counter.step()
assert result.comment == 'Exclusion of Thomson'
assert result.stage_kind == 'Exclusion of'
assert result.comment == 'Thomson'
assert isclose(result.candidates[c_thomson].transfers, -1206)
assert isclose(result.candidates[c_white].transfers, 1206)