Implement papers+votes report
This commit is contained in:
parent
c9faa2ef01
commit
8a3361f20d
|
@ -166,6 +166,19 @@ OpenTally applies constraints using the Grey–Fitzgerald method. Whenever a can
|
|||
|
||||
Multiple constraints are supported using the method described by Hill ([*Voting Matters* 1998;(9):2–4](http://www.votingmatters.org.uk/ISSUE9/P1.HTM)) and Otten ([*Voting Matters* 2001;(13):4–7](http://www.votingmatters.org.uk/ISSUE13/P3.HTM)).
|
||||
|
||||
## Report options
|
||||
|
||||
### Report style
|
||||
|
||||
* *Votes only*: The result sheet displays the number of votes held by each candidate at each stage of the count.
|
||||
* *Ballots and votes*: The result sheet displays the number of votes *and ballot papers* held by each candidate at each stage of the count.
|
||||
|
||||
This functionality is not available on the command line.
|
||||
|
||||
### Display up to [n] d.p. (--pp-decimals)
|
||||
|
||||
This option allows you to specify to how many decimal places votes will be reported in the results report. It does not affect the internal precision of calculations.
|
||||
|
||||
## Numeric representation
|
||||
|
||||
### Numbers (-n/--numbers), Decimal places (--decimals)
|
||||
|
@ -177,10 +190,6 @@ This dropdown allows you to select how numbers (vote totals, etc.) are represent
|
|||
* *Rational* (default): Numbers are represented exactly as fractions, resulting in the elimination of rounding error, but increasing computational complexity when the number of surplus transfers is very large.
|
||||
* *Float (64-bit)*: Numbers are represented as native 64-bit floating-point numbers. This is fast, but not recommended as unexpectedly large rounding errors may be introduced in some circumstances.
|
||||
|
||||
### Display up to [n] d.p. (--pp-decimals)
|
||||
|
||||
This option allows you to specify to how many decimal places votes will be reported in the results report. It does not affect the internal precision of calculations.
|
||||
|
||||
### Normalise ballots (--normalise-ballots)
|
||||
|
||||
In the BLT file format, each set of preferences can have a specified weight – this is typically used to indicate multiple voters who had the same preferences.
|
||||
|
|
|
@ -182,6 +182,27 @@
|
|||
<div>
|
||||
<input type="file" id="conFile">
|
||||
</div>
|
||||
<div class="subheading">
|
||||
Report options:
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<label style="margin-right:1em;">
|
||||
Report style:
|
||||
<select id="selReport">
|
||||
<option value="votes" selected>Votes only</option>
|
||||
<option value="ballots_votes">Ballots and votes</option>
|
||||
</select>
|
||||
</label>
|
||||
<label style="display:none;">
|
||||
<input type="checkbox" id="chkReportTranspose" disabled>
|
||||
Transpose transfers/<wbr>totals
|
||||
</label>
|
||||
</div>
|
||||
<label class="col-12">
|
||||
Display up to
|
||||
<input type="number" id="txtPPDP" value="2" min="0" style="width: 3em;">
|
||||
d.p.
|
||||
</label>
|
||||
</div>
|
||||
<div class="col-6 cols-12" style="align-self: start;">
|
||||
<div class="col-12 subheading">
|
||||
|
@ -203,11 +224,6 @@
|
|||
<input type="number" id="txtDP" value="5" min="0" style="width: 3em;">
|
||||
</label>
|
||||
</div>
|
||||
<label class="col-12">
|
||||
Display up to
|
||||
<input type="number" id="txtPPDP" value="2" min="0" style="width: 3em;">
|
||||
d.p.
|
||||
</label>
|
||||
<label class="col-12">
|
||||
<input type="checkbox" id="chkNormaliseBallots">
|
||||
Normalise ballots
|
||||
|
|
|
@ -58,16 +58,19 @@ worker.onmessage = function(evt) {
|
|||
document.getElementById('resultLogs1').innerHTML = evt.data.content;
|
||||
|
||||
} else if (evt.data.type === 'updateResultsTable') {
|
||||
for (let i = 0; i < evt.data.result.length; i++) {
|
||||
if (evt.data.result[i]) {
|
||||
tblResult.rows[i].insertAdjacentHTML('beforeend', evt.data.result[i]);
|
||||
for (let row = 0; row < evt.data.result.length; row++) {
|
||||
if (evt.data.result[row]) {
|
||||
tblResult.rows[row].insertAdjacentHTML('beforeend', evt.data.result[row]);
|
||||
|
||||
// Update candidate status
|
||||
if (i >= 3 && i % 2 == 1) {
|
||||
if (tblResult.rows[i].lastElementChild.classList.contains('elected')) {
|
||||
tblResult.rows[i].cells[0].classList.add('elected');
|
||||
if (
|
||||
(document.getElementById('selReport').value == 'votes' && row >= 3 && row % 2 == 1) ||
|
||||
(document.getElementById('selReport').value == 'ballots_votes' && row >= 4 && row % 2 == 0)
|
||||
) {
|
||||
if (tblResult.rows[row].lastElementChild.classList.contains('elected')) {
|
||||
tblResult.rows[row].cells[0].classList.add('elected');
|
||||
} else {
|
||||
tblResult.rows[i].cells[0].classList.remove('elected');
|
||||
tblResult.rows[row].cells[0].classList.remove('elected');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -170,14 +173,18 @@ async function clickCount() {
|
|||
// Dispatch to worker
|
||||
worker.postMessage({
|
||||
'type': 'countElection',
|
||||
// Data
|
||||
'bltData': bltData,
|
||||
'conData': conData,
|
||||
'optsStr': optsStr,
|
||||
'bltPath': bltPath,
|
||||
'conPath': conPath,
|
||||
// Options
|
||||
'optsStr': optsStr,
|
||||
'numbers': document.getElementById('selNumbers').value,
|
||||
'decimals': document.getElementById('txtDP').value,
|
||||
'normaliseBallots': document.getElementById('chkNormaliseBallots').checked,
|
||||
'reportStyle': document.getElementById('selReport').value,
|
||||
'reportTranspose': document.getElementById('chkReportTranspose').checked,
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -54,6 +54,10 @@ li.highlight {
|
|||
.menudiv .subheading {
|
||||
font-size: 0.8em;
|
||||
font-weight: 600;
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
.menudiv > div > .subheading:first-child {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.pill-grey {
|
||||
|
@ -101,7 +105,7 @@ td.count sup {
|
|||
font-size: 0.6rem;
|
||||
top: 0;
|
||||
}
|
||||
tr.stage-no td, tr.stage-kind td, tr.stage-comment td {
|
||||
tr.stage-no td, tr.stage-kind td, tr.stage-comment td, tr.hint-papers-votes td {
|
||||
text-align: center;
|
||||
}
|
||||
tr.stage-kind td {
|
||||
|
@ -110,6 +114,10 @@ tr.stage-kind td {
|
|||
color: #1b2839;
|
||||
background-color: #f0f5fb;
|
||||
}
|
||||
tr.hint-papers-votes td {
|
||||
font-size: 0.75em;
|
||||
font-style: italic;
|
||||
}
|
||||
td.excluded {
|
||||
background-color: #fde2e2;
|
||||
}
|
||||
|
@ -119,7 +127,7 @@ td.elected {
|
|||
tr.info td {
|
||||
background-color: #f0f5fb;
|
||||
}
|
||||
tr.stage-no td:not(:empty), tr.transfers td {
|
||||
tr.stage-no td:not(:empty), tr.hint-papers-votes td:not(:empty), tr.transfers td {
|
||||
border-top: 1px solid #76858c;
|
||||
}
|
||||
tr.info:last-child td, .bb {
|
||||
|
@ -134,6 +142,7 @@ tr.info:last-child td, .bb {
|
|||
|
||||
tr.stage-no td:nth-child(even):not([rowspan]),
|
||||
tr.stage-comment td:nth-child(odd),
|
||||
tr.hint-papers-votes td:nth-child(even),
|
||||
tr.candidate.transfers td:nth-child(even):not(.elected):not(.excluded),
|
||||
tr.candidate.votes td:nth-child(odd):not(.elected):not(.excluded) {
|
||||
background-color: #f9f9f9;
|
||||
|
|
|
@ -17,6 +17,7 @@ async function initWasm() {
|
|||
}
|
||||
initWasm();
|
||||
|
||||
var reportStyle;
|
||||
var numbers, election, opts, state, stageNum;
|
||||
|
||||
onmessage = function(evt) {
|
||||
|
@ -38,6 +39,8 @@ onmessage = function(evt) {
|
|||
throw 'Unknown --numbers';
|
||||
}
|
||||
|
||||
reportStyle = evt.data.reportStyle;
|
||||
|
||||
// Init election
|
||||
election = wasm['election_from_blt_' + numbers](evt.data.bltData);
|
||||
|
||||
|
@ -60,13 +63,13 @@ onmessage = function(evt) {
|
|||
postMessage({'type': 'describeCount', 'content': wasm['describe_count_' + numbers](evt.data.bltPath, election, opts)});
|
||||
|
||||
// Init results table
|
||||
postMessage({'type': 'initResultsTable', 'content': wasm['init_results_table_' + numbers](election, opts)});
|
||||
postMessage({'type': 'initResultsTable', 'content': wasm['init_results_table_' + numbers](election, opts, reportStyle)});
|
||||
|
||||
// Step election
|
||||
state = wasm['CountState' + numbers].new(election);
|
||||
wasm['count_init_' + numbers](state, opts);
|
||||
|
||||
postMessage({'type': 'updateResultsTable', 'result': wasm['update_results_table_' + numbers](1, state, opts)});
|
||||
postMessage({'type': 'updateResultsTable', 'result': wasm['update_results_table_' + numbers](1, state, opts, reportStyle)});
|
||||
postMessage({'type': 'updateStageComments', 'comment': wasm['update_stage_comments_' + numbers](state)});
|
||||
|
||||
stageNum = 2;
|
||||
|
@ -104,11 +107,11 @@ function resumeCount() {
|
|||
break;
|
||||
}
|
||||
|
||||
postMessage({'type': 'updateResultsTable', 'result': wasm['update_results_table_' + numbers](stageNum, state, opts)});
|
||||
postMessage({'type': 'updateResultsTable', 'result': wasm['update_results_table_' + numbers](stageNum, state, opts, reportStyle)});
|
||||
postMessage({'type': 'updateStageComments', 'comment': wasm['update_stage_comments_' + numbers](state)});
|
||||
}
|
||||
|
||||
postMessage({'type': 'updateResultsTable', 'result': wasm['finalise_results_table_' + numbers](state)});
|
||||
postMessage({'type': 'updateResultsTable', 'result': wasm['finalise_results_table_' + numbers](state, reportStyle)});
|
||||
postMessage({'type': 'finalResultSummary', 'summary': wasm['final_result_summary_' + numbers](state, opts)});
|
||||
}
|
||||
|
||||
|
|
|
@ -304,6 +304,9 @@ pub struct CountCard<'a, N> {
|
|||
/// Votes of the candidate at the end of this stage
|
||||
pub votes: N,
|
||||
|
||||
/// Net ballots transferred to this candidate in this stage
|
||||
pub ballot_transfers: N,
|
||||
|
||||
/// Parcels of ballots assigned to this candidate
|
||||
pub parcels: Vec<Parcel<'a, N>>,
|
||||
|
||||
|
@ -320,6 +323,7 @@ impl<'a, N: Number> CountCard<'a, N> {
|
|||
finalised: false,
|
||||
transfers: N::new(),
|
||||
votes: N::new(),
|
||||
ballot_transfers: N::new(),
|
||||
parcels: Vec::new(),
|
||||
keep_value: None,
|
||||
};
|
||||
|
@ -333,8 +337,8 @@ impl<'a, N: Number> CountCard<'a, N> {
|
|||
|
||||
/// Set [transfers](CountCard::transfers) to 0
|
||||
pub fn step(&mut self) {
|
||||
//self.orig_votes = self.votes.clone();
|
||||
self.transfers = N::new();
|
||||
self.ballot_transfers = N::new();
|
||||
}
|
||||
|
||||
/// Concatenate all parcels into a single parcel, leaving [parcels](CountCard::parcels) empty
|
||||
|
@ -345,6 +349,11 @@ impl<'a, N: Number> CountCard<'a, N> {
|
|||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/// Return the number of ballots across all parcels
|
||||
pub fn num_ballots(&self) -> N {
|
||||
return self.parcels.iter().fold(N::new(), |acc, p| acc + p.num_ballots());
|
||||
}
|
||||
}
|
||||
|
||||
/// Parcel of [Vote]s during a count
|
||||
|
|
|
@ -46,6 +46,7 @@ pub fn distribute_first_preferences<N: Number>(state: &mut CountState<N>) {
|
|||
let count_card = state.candidates.get_mut(candidate).unwrap();
|
||||
count_card.parcels.push(parcel);
|
||||
count_card.transfer(&entry.num_ballots);
|
||||
count_card.ballot_transfers += entry.num_ballots;
|
||||
}
|
||||
|
||||
// Transfer exhausted votes
|
||||
|
@ -56,6 +57,7 @@ pub fn distribute_first_preferences<N: Number>(state: &mut CountState<N>) {
|
|||
};
|
||||
state.exhausted.parcels.push(parcel);
|
||||
state.exhausted.transfer(&result.exhausted.num_ballots);
|
||||
state.exhausted.ballot_transfers += result.exhausted.num_ballots;
|
||||
|
||||
state.kind = None;
|
||||
state.title = "First preferences".to_string();
|
||||
|
@ -251,7 +253,9 @@ where
|
|||
state.title = String::from(&elected_candidate.name);
|
||||
state.logger.log_literal(format!("Surplus of {} distributed.", elected_candidate.name));
|
||||
|
||||
let count_card = &state.candidates[elected_candidate];
|
||||
let count_card = state.candidates.get_mut(elected_candidate).unwrap();
|
||||
count_card.ballot_transfers = -count_card.num_ballots();
|
||||
|
||||
let surplus = &count_card.votes - state.quota.as_ref().unwrap();
|
||||
|
||||
// Determine which votes to examine
|
||||
|
@ -361,13 +365,14 @@ where
|
|||
candidate_transfers.insert(candidate, transfers_orig + transfers_add);
|
||||
|
||||
// Transfer candidate votes
|
||||
let new_parcel = Parcel {
|
||||
let parcel = Parcel {
|
||||
votes: entry.votes,
|
||||
value_fraction: reweight_value_fraction(&value_fraction, &surplus, is_weighted, &surplus_fraction, &surplus_denom, opts.round_surplus_fractions),
|
||||
source_order: state.num_elected + state.num_excluded,
|
||||
};
|
||||
let count_card = state.candidates.get_mut(candidate).unwrap();
|
||||
count_card.parcels.push(new_parcel);
|
||||
count_card.ballot_transfers += parcel.num_ballots();
|
||||
count_card.parcels.push(parcel);
|
||||
}
|
||||
|
||||
// Record exhausted votes
|
||||
|
@ -387,12 +392,14 @@ where
|
|||
value_fraction: value_fraction, // TODO: Reweight exhausted votes
|
||||
source_order: state.num_elected + state.num_excluded,
|
||||
};
|
||||
state.exhausted.ballot_transfers += parcel.num_ballots();
|
||||
state.exhausted.parcels.push(parcel);
|
||||
}
|
||||
|
||||
let mut checksum = N::new();
|
||||
|
||||
// Credit transferred votes
|
||||
// ballot_transfers updated above
|
||||
for (candidate, mut votes) in candidate_transfers {
|
||||
if let Some(dps) = opts.round_votes {
|
||||
votes.floor_mut(dps);
|
||||
|
@ -453,6 +460,7 @@ where
|
|||
// Exclude in one round
|
||||
for excluded_candidate in excluded_candidates.iter() {
|
||||
let count_card = state.candidates.get_mut(excluded_candidate).unwrap();
|
||||
count_card.ballot_transfers = -count_card.num_ballots();
|
||||
count_card.finalised = true;
|
||||
|
||||
parcels.append(&mut count_card.parcels);
|
||||
|
@ -493,6 +501,8 @@ where
|
|||
|
||||
for mut parcel in cc_parcels {
|
||||
if parcel.value_fraction == max_value {
|
||||
count_card.ballot_transfers -= parcel.num_ballots();
|
||||
|
||||
let votes_transferred = parcel.num_votes();
|
||||
votes.append(&mut parcel.votes);
|
||||
|
||||
|
@ -546,6 +556,8 @@ where
|
|||
|
||||
for parcel in cc_parcels {
|
||||
if parcel.source_order == min_order {
|
||||
count_card.ballot_transfers -= parcel.num_ballots();
|
||||
|
||||
let votes_transferred = parcel.num_votes();
|
||||
parcels.push(parcel);
|
||||
|
||||
|
@ -581,6 +593,8 @@ where
|
|||
parcels.push(count_card.parcels.remove(0));
|
||||
votes_remain = !count_card.parcels.is_empty();
|
||||
|
||||
count_card.ballot_transfers -= parcels.first().unwrap().num_ballots();
|
||||
|
||||
// Update votes
|
||||
let votes_transferred = parcels.first().unwrap().num_votes();
|
||||
checksum -= &votes_transferred;
|
||||
|
@ -621,6 +635,7 @@ where
|
|||
candidate_transfers.insert(candidate, transfers_orig + &entry.num_ballots * &parcel.value_fraction);
|
||||
|
||||
let count_card = state.candidates.get_mut(candidate).unwrap();
|
||||
count_card.ballot_transfers += parcel.num_ballots();
|
||||
count_card.parcels.push(parcel);
|
||||
}
|
||||
|
||||
|
@ -632,8 +647,8 @@ where
|
|||
};
|
||||
|
||||
// Record transfers
|
||||
state.exhausted.ballot_transfers += parcel.num_ballots();
|
||||
exhausted_transfers += &result.exhausted.num_ballots * &parcel.value_fraction;
|
||||
|
||||
state.exhausted.parcels.push(parcel);
|
||||
|
||||
// TODO: Detailed transfers logs
|
||||
|
@ -656,6 +671,7 @@ where
|
|||
}
|
||||
|
||||
// Credit transferred votes
|
||||
// ballot_transfers updated above
|
||||
for (candidate, mut votes) in candidate_transfers {
|
||||
if let Some(dps) = opts.round_votes {
|
||||
votes.floor_mut(dps);
|
||||
|
|
206
src/stv/wasm.rs
206
src/stv/wasm.rs
|
@ -118,8 +118,8 @@ macro_rules! impl_type {
|
|||
/// Wrapper for [init_results_table]
|
||||
#[wasm_bindgen]
|
||||
#[allow(non_snake_case)]
|
||||
pub fn [<init_results_table_$type>](election: &[<Election$type>], opts: &STVOptions) -> String {
|
||||
return init_results_table(&election.0, &opts.0);
|
||||
pub fn [<init_results_table_$type>](election: &[<Election$type>], opts: &STVOptions, report_style: &str) -> String {
|
||||
return init_results_table(&election.0, &opts.0, report_style);
|
||||
}
|
||||
|
||||
/// Wrapper for [describe_count]
|
||||
|
@ -132,8 +132,8 @@ macro_rules! impl_type {
|
|||
/// Wrapper for [update_results_table]
|
||||
#[wasm_bindgen]
|
||||
#[allow(non_snake_case)]
|
||||
pub fn [<update_results_table_$type>](stage_num: usize, state: &[<CountState$type>], opts: &STVOptions) -> Array {
|
||||
return update_results_table(stage_num, &state.0, &opts.0);
|
||||
pub fn [<update_results_table_$type>](stage_num: usize, state: &[<CountState$type>], opts: &STVOptions, report_style: &str) -> Array {
|
||||
return update_results_table(stage_num, &state.0, &opts.0, report_style);
|
||||
}
|
||||
|
||||
/// Wrapper for [update_stage_comments]
|
||||
|
@ -146,8 +146,8 @@ macro_rules! impl_type {
|
|||
/// Wrapper for [finalise_results_table]
|
||||
#[wasm_bindgen]
|
||||
#[allow(non_snake_case)]
|
||||
pub fn [<finalise_results_table_$type>](state: &[<CountState$type>]) -> Array {
|
||||
return finalise_results_table(&state.0);
|
||||
pub fn [<finalise_results_table_$type>](state: &[<CountState$type>], report_style: &str) -> Array {
|
||||
return finalise_results_table(&state.0, report_style);
|
||||
}
|
||||
|
||||
/// Wrapper for [final_result_summary]
|
||||
|
@ -321,8 +321,14 @@ fn should_show_vre(opts: &stv::STVOptions) -> bool {
|
|||
}
|
||||
|
||||
/// Generate the first column of the HTML results table
|
||||
fn init_results_table<N: Number>(election: &Election<N>, opts: &stv::STVOptions) -> String {
|
||||
fn init_results_table<N: Number>(election: &Election<N>, opts: &stv::STVOptions, report_style: &str) -> String {
|
||||
let mut result = String::from(r#"<tr class="stage-no"><td rowspan="3"></td></tr><tr class="stage-kind"></tr><tr class="stage-comment"></tr>"#);
|
||||
match report_style {
|
||||
"ballots_votes" => {
|
||||
result.push_str(&r#"<tr class="hint-papers-votes"><td></td></tr>"#);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
for candidate in election.candidates.iter() {
|
||||
result.push_str(&format!(r#"<tr class="candidate transfers"><td rowspan="2">{}</td></tr><tr class="candidate votes"></tr>"#, candidate.name));
|
||||
}
|
||||
|
@ -334,7 +340,7 @@ fn init_results_table<N: Number>(election: &Election<N>, opts: &stv::STVOptions)
|
|||
}
|
||||
|
||||
/// Generate subsequent columns of the HTML results table
|
||||
fn update_results_table<N: Number>(stage_num: usize, state: &CountState<N>, opts: &stv::STVOptions) -> Array {
|
||||
fn update_results_table<N: Number>(stage_num: usize, state: &CountState<N>, opts: &stv::STVOptions, report_style: &str) -> Array {
|
||||
let result = Array::new();
|
||||
|
||||
// Insert borders to left of new exclusions in Wright STV
|
||||
|
@ -345,55 +351,152 @@ fn update_results_table<N: Number>(stage_num: usize, state: &CountState<N>, opts
|
|||
tdclasses2 = r#"blw "#;
|
||||
}
|
||||
|
||||
result.push(&format!(r##"<td{0}><a href="#stage{1}">{1}</a></td>"##, tdclasses1, stage_num).into());
|
||||
result.push(&format!(r#"<td{}>{}</td>"#, tdclasses1, state.kind.unwrap_or("")).into());
|
||||
result.push(&format!(r#"<td{}>{}</td>"#, tdclasses1, state.title).into());
|
||||
// Header rows
|
||||
match report_style {
|
||||
"votes" => {
|
||||
result.push(&format!(r##"<td{0}><a href="#stage{1}">{1}</a></td>"##, tdclasses1, stage_num).into());
|
||||
result.push(&format!(r#"<td{}>{}</td>"#, tdclasses1, state.kind.unwrap_or("")).into());
|
||||
result.push(&format!(r#"<td{}>{}</td>"#, tdclasses1, state.title).into());
|
||||
}
|
||||
"ballots_votes" => {
|
||||
result.push(&format!(r##"<td{0} colspan="2"><a href="#stage{1}">{1}</a></td>"##, tdclasses1, stage_num).into());
|
||||
result.push(&format!(r#"<td{} colspan="2">{}</td>"#, tdclasses1, state.kind.unwrap_or("")).into());
|
||||
result.push(&format!(r#"<td{} colspan="2">{}</td>"#, tdclasses1, state.title).into());
|
||||
result.push(&format!(r#"<td{}>Ballots</td><td>Votes</td>"#, tdclasses1).into());
|
||||
}
|
||||
_ => unreachable!("Invalid report_style")
|
||||
}
|
||||
|
||||
for candidate in state.election.candidates.iter() {
|
||||
let count_card = &state.candidates[candidate];
|
||||
match count_card.state {
|
||||
CandidateState::Hopeful | CandidateState::Guarded => {
|
||||
result.push(&format!(r#"<td class="{}count">{}</td>"#, tdclasses2, pp(&count_card.transfers, opts.pp_decimals)).into());
|
||||
result.push(&format!(r#"<td class="{}count">{}</td>"#, tdclasses2, pp(&count_card.votes, opts.pp_decimals)).into());
|
||||
}
|
||||
CandidateState::Elected => {
|
||||
result.push(&format!(r#"<td class="{}count elected">{}</td>"#, tdclasses2, pp(&count_card.transfers, opts.pp_decimals)).into());
|
||||
result.push(&format!(r#"<td class="{}count elected">{}</td>"#, tdclasses2, pp(&count_card.votes, opts.pp_decimals)).into());
|
||||
}
|
||||
CandidateState::Doomed => {
|
||||
result.push(&format!(r#"<td class="{}count excluded">{}</td>"#, tdclasses2, pp(&count_card.transfers, opts.pp_decimals)).into());
|
||||
result.push(&format!(r#"<td class="{}count excluded">{}</td>"#, tdclasses2, pp(&count_card.votes, opts.pp_decimals)).into());
|
||||
}
|
||||
CandidateState::Withdrawn => {
|
||||
result.push(&format!(r#"<td class="{}count excluded"></td>"#, tdclasses2).into());
|
||||
result.push(&format!(r#"<td class="{}count excluded">WD</td>"#, tdclasses2).into());
|
||||
}
|
||||
CandidateState::Excluded => {
|
||||
result.push(&format!(r#"<td class="{}count excluded">{}</td>"#, tdclasses2, pp(&count_card.transfers, opts.pp_decimals)).into());
|
||||
if count_card.votes.is_zero() && count_card.parcels.iter().all(|p| p.votes.is_empty()) {
|
||||
result.push(&format!(r#"<td class="{}count excluded">Ex</td>"#, tdclasses2).into());
|
||||
} else {
|
||||
result.push(&format!(r#"<td class="{}count excluded">{}</td>"#, tdclasses2, pp(&count_card.votes, opts.pp_decimals)).into());
|
||||
|
||||
match report_style {
|
||||
"votes" => {
|
||||
match count_card.state {
|
||||
CandidateState::Hopeful | CandidateState::Guarded => {
|
||||
result.push(&format!(r#"<td class="{}count">{}</td>"#, tdclasses2, pp(&count_card.transfers, opts.pp_decimals)).into());
|
||||
result.push(&format!(r#"<td class="{}count">{}</td>"#, tdclasses2, pp(&count_card.votes, opts.pp_decimals)).into());
|
||||
}
|
||||
CandidateState::Elected => {
|
||||
result.push(&format!(r#"<td class="{}count elected">{}</td>"#, tdclasses2, pp(&count_card.transfers, opts.pp_decimals)).into());
|
||||
result.push(&format!(r#"<td class="{}count elected">{}</td>"#, tdclasses2, pp(&count_card.votes, opts.pp_decimals)).into());
|
||||
}
|
||||
CandidateState::Doomed => {
|
||||
result.push(&format!(r#"<td class="{}count excluded">{}</td>"#, tdclasses2, pp(&count_card.transfers, opts.pp_decimals)).into());
|
||||
result.push(&format!(r#"<td class="{}count excluded">{}</td>"#, tdclasses2, pp(&count_card.votes, opts.pp_decimals)).into());
|
||||
}
|
||||
CandidateState::Withdrawn => {
|
||||
result.push(&format!(r#"<td class="{}count excluded"></td>"#, tdclasses2).into());
|
||||
result.push(&format!(r#"<td class="{}count excluded">WD</td>"#, tdclasses2).into());
|
||||
}
|
||||
CandidateState::Excluded => {
|
||||
result.push(&format!(r#"<td class="{}count excluded">{}</td>"#, tdclasses2, pp(&count_card.transfers, opts.pp_decimals)).into());
|
||||
if count_card.votes.is_zero() && count_card.parcels.iter().all(|p| p.votes.is_empty()) {
|
||||
result.push(&format!(r#"<td class="{}count excluded">Ex</td>"#, tdclasses2).into());
|
||||
} else {
|
||||
result.push(&format!(r#"<td class="{}count excluded">{}</td>"#, tdclasses2, pp(&count_card.votes, opts.pp_decimals)).into());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
"ballots_votes" => {
|
||||
match count_card.state {
|
||||
CandidateState::Hopeful | CandidateState::Guarded => {
|
||||
result.push(&format!(r#"<td class="{}count">{}</td><td class="count">{}</td>"#, tdclasses2, pp(&count_card.ballot_transfers, 0), pp(&count_card.transfers, opts.pp_decimals)).into());
|
||||
result.push(&format!(r#"<td class="{}count">{}</td><td class="count">{}</td>"#, tdclasses2, pp(&count_card.num_ballots(), 0), pp(&count_card.votes, opts.pp_decimals)).into());
|
||||
}
|
||||
CandidateState::Elected => {
|
||||
result.push(&format!(r#"<td class="{}count elected">{}</td><td class="count elected">{}</td>"#, tdclasses2, pp(&count_card.ballot_transfers, 0), pp(&count_card.transfers, opts.pp_decimals)).into());
|
||||
result.push(&format!(r#"<td class="{}count elected">{}</td><td class="count elected">{}</td>"#, tdclasses2, pp(&count_card.num_ballots(), 0), pp(&count_card.votes, opts.pp_decimals)).into());
|
||||
}
|
||||
CandidateState::Doomed => {
|
||||
result.push(&format!(r#"<td class="{}count excluded">{}</td><td class="count excluded">{}</td>"#, tdclasses2, pp(&count_card.ballot_transfers, 0), pp(&count_card.transfers, opts.pp_decimals)).into());
|
||||
result.push(&format!(r#"<td class="{}count excluded">{}</td><td class="count excluded">{}</td>"#, tdclasses2, pp(&count_card.num_ballots(), 0), pp(&count_card.votes, opts.pp_decimals)).into());
|
||||
}
|
||||
CandidateState::Withdrawn => {
|
||||
result.push(&format!(r#"<td class="{}count excluded"></td><td class="count excluded"></td>"#, tdclasses2).into());
|
||||
result.push(&format!(r#"<td class="{}count excluded"></td><td class="count excluded">WD</td>"#, tdclasses2).into());
|
||||
}
|
||||
CandidateState::Excluded => {
|
||||
result.push(&format!(r#"<td class="{}count excluded">{}</td><td class="count excluded">{}</td>"#, tdclasses2, pp(&count_card.ballot_transfers, 0), pp(&count_card.transfers, opts.pp_decimals)).into());
|
||||
if count_card.votes.is_zero() && count_card.parcels.iter().all(|p| p.votes.is_empty()) {
|
||||
result.push(&format!(r#"<td class="{}count excluded"></td><td class="count excluded">Ex</td>"#, tdclasses2).into());
|
||||
} else {
|
||||
result.push(&format!(r#"<td class="{}count excluded">{}</td><td class="count excluded">{}</td>"#, tdclasses2, pp(&count_card.num_ballots(), 0), pp(&count_card.votes, opts.pp_decimals)).into());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => unreachable!("Invalid report_style")
|
||||
}
|
||||
}
|
||||
result.push(&format!(r#"<td class="{}count">{}</td>"#, tdclasses2, pp(&state.exhausted.transfers, opts.pp_decimals)).into());
|
||||
result.push(&format!(r#"<td class="{}count">{}</td>"#, tdclasses2, pp(&state.exhausted.votes, opts.pp_decimals)).into());
|
||||
result.push(&format!(r#"<td class="{}count">{}</td>"#, tdclasses2, pp(&state.loss_fraction.transfers, opts.pp_decimals)).into());
|
||||
result.push(&format!(r#"<td class="{}count">{}</td>"#, tdclasses2, pp(&state.loss_fraction.votes, opts.pp_decimals)).into());
|
||||
|
||||
match report_style {
|
||||
"votes" => {
|
||||
result.push(&format!(r#"<td class="{}count">{}</td>"#, tdclasses2, pp(&state.exhausted.transfers, opts.pp_decimals)).into());
|
||||
result.push(&format!(r#"<td class="{}count">{}</td>"#, tdclasses2, pp(&state.exhausted.votes, opts.pp_decimals)).into());
|
||||
result.push(&format!(r#"<td class="{}count">{}</td>"#, tdclasses2, pp(&state.loss_fraction.transfers, opts.pp_decimals)).into());
|
||||
result.push(&format!(r#"<td class="{}count">{}</td>"#, tdclasses2, pp(&state.loss_fraction.votes, opts.pp_decimals)).into());
|
||||
}
|
||||
"ballots_votes" => {
|
||||
result.push(&format!(r#"<td class="{}count">{}</td><td class="count">{}</td>"#, tdclasses2, pp(&state.exhausted.ballot_transfers, 0), pp(&state.exhausted.transfers, opts.pp_decimals)).into());
|
||||
result.push(&format!(r#"<td class="{}count">{}</td><td class="count">{}</td>"#, tdclasses2, pp(&state.exhausted.num_ballots(), 0), pp(&state.exhausted.votes, opts.pp_decimals)).into());
|
||||
result.push(&format!(r#"<td class="{}count"></td><td class="count">{}</td>"#, tdclasses2, pp(&state.loss_fraction.transfers, opts.pp_decimals)).into());
|
||||
result.push(&format!(r#"<td class="{}count"></td><td class="count">{}</td>"#, tdclasses2, pp(&state.loss_fraction.votes, opts.pp_decimals)).into());
|
||||
}
|
||||
_ => unreachable!("Invalid report_style")
|
||||
}
|
||||
|
||||
// Calculate total votes
|
||||
let mut total_vote = state.candidates.values().fold(N::zero(), |acc, cc| { acc + &cc.votes });
|
||||
total_vote += &state.exhausted.votes;
|
||||
total_vote += &state.loss_fraction.votes;
|
||||
result.push(&format!(r#"<td class="{}count">{}</td>"#, tdclasses2, pp(&total_vote, opts.pp_decimals)).into());
|
||||
|
||||
result.push(&format!(r#"<td class="{}count">{}</td>"#, tdclasses2, pp(state.quota.as_ref().unwrap(), opts.pp_decimals)).into());
|
||||
match report_style {
|
||||
"votes" => {
|
||||
result.push(&format!(r#"<td class="{}count">{}</td>"#, tdclasses2, pp(&total_vote, opts.pp_decimals)).into());
|
||||
}
|
||||
"ballots_votes" => {
|
||||
// Calculate total ballots
|
||||
let mut total_ballots = state.candidates.values().fold(N::zero(), |acc, cc| { acc + cc.num_ballots() });
|
||||
total_ballots += state.exhausted.num_ballots();
|
||||
|
||||
result.push(&format!(r#"<td class="{}count">{}</td><td class="count">{}</td>"#, tdclasses2, pp(&total_ballots, 0), pp(&total_vote, opts.pp_decimals)).into());
|
||||
}
|
||||
_ => unreachable!("Invalid report_style")
|
||||
}
|
||||
|
||||
match report_style {
|
||||
"votes" => {
|
||||
result.push(&format!(r#"<td class="{}count">{}</td>"#, tdclasses2, pp(state.quota.as_ref().unwrap(), opts.pp_decimals)).into());
|
||||
}
|
||||
"ballots_votes" => {
|
||||
result.push(&format!(r#"<td class="{}count"></td><td class="count">{}</td>"#, tdclasses2, pp(state.quota.as_ref().unwrap(), opts.pp_decimals)).into());
|
||||
}
|
||||
_ => unreachable!("Invalid report_style")
|
||||
}
|
||||
|
||||
if should_show_vre(opts) {
|
||||
if let Some(vre) = &state.vote_required_election {
|
||||
result.push(&format!(r#"<td class="{}count">{}</td>"#, tdclasses2, pp(vre, opts.pp_decimals)).into());
|
||||
match report_style {
|
||||
"votes" => {
|
||||
result.push(&format!(r#"<td class="{}count">{}</td>"#, tdclasses2, pp(vre, opts.pp_decimals)).into());
|
||||
}
|
||||
"ballots_votes" => {
|
||||
result.push(&format!(r#"<td class="{}count"></td><td class="count">{}</td>"#, tdclasses2, pp(vre, opts.pp_decimals)).into());
|
||||
}
|
||||
_ => unreachable!("Invalid report_style")
|
||||
}
|
||||
} else {
|
||||
result.push(&format!(r#"<td class="{}count"></td>"#, tdclasses2).into());
|
||||
match report_style {
|
||||
"votes" => {
|
||||
result.push(&format!(r#"<td class="{}count"></td>"#, tdclasses2).into());
|
||||
}
|
||||
"ballots_votes" => {
|
||||
result.push(&format!(r#"<td class="{}count"></td><td class="count"></td>"#, tdclasses2).into());
|
||||
}
|
||||
_ => unreachable!("Invalid report_style")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -406,13 +509,24 @@ fn update_stage_comments<N: Number>(state: &CountState<N>) -> String {
|
|||
}
|
||||
|
||||
/// Generate the final column of the HTML results table
|
||||
fn finalise_results_table<N: Number>(state: &CountState<N>) -> Array {
|
||||
fn finalise_results_table<N: Number>(state: &CountState<N>, report_style: &str) -> Array {
|
||||
let result = Array::new();
|
||||
|
||||
// Header rows
|
||||
result.push(&r#"<td rowspan="3"></td>"#.into());
|
||||
result.push(&"".into());
|
||||
result.push(&"".into());
|
||||
match report_style {
|
||||
"votes" => {
|
||||
result.push(&r#"<td rowspan="3"></td>"#.into());
|
||||
result.push(&"".into());
|
||||
result.push(&"".into());
|
||||
}
|
||||
"ballots_votes" => {
|
||||
result.push(&r#"<td rowspan="4"></td>"#.into());
|
||||
result.push(&"".into());
|
||||
result.push(&"".into());
|
||||
result.push(&"".into());
|
||||
}
|
||||
_ => unreachable!("Invalid report_style")
|
||||
}
|
||||
|
||||
// Candidate states
|
||||
for candidate in state.election.candidates.iter() {
|
||||
|
|
Loading…
Reference in New Issue