Implement detailed transfers in web UI
This commit is contained in:
parent
9817d6c199
commit
df1b2f7bdc
|
@ -517,6 +517,15 @@ dependencies = [
|
|||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "html-escape"
|
||||
version = "0.2.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "816ea801a95538fc5f53c836697b3f8b64a9d664c4f0b91efe1fe7c92e4dbcb7"
|
||||
dependencies = [
|
||||
"utf8-width",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ibig"
|
||||
version = "0.3.2"
|
||||
|
@ -637,9 +646,9 @@ checksum = "61807f77802ff30975e01f4f071c8ba10c022052f98b3294119f3e615d13e5be"
|
|||
|
||||
[[package]]
|
||||
name = "num-bigint"
|
||||
version = "0.4.0"
|
||||
version = "0.4.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4e0d047c1062aa51e256408c560894e5251f08925980e53cf1aa5bd00eec6512"
|
||||
checksum = "74e768dff5fb39a41b3bcd30bb25cf989706c90d028d1ad71971987aa309d535"
|
||||
dependencies = [
|
||||
"autocfg",
|
||||
"num-integer",
|
||||
|
@ -710,6 +719,7 @@ dependencies = [
|
|||
"derive_more",
|
||||
"flate2",
|
||||
"git-version",
|
||||
"html-escape",
|
||||
"ibig",
|
||||
"itertools",
|
||||
"js-sys",
|
||||
|
@ -1124,6 +1134,12 @@ dependencies = [
|
|||
"arrayvec",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "utf8-width"
|
||||
version = "0.1.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7cf7d77f457ef8dfa11e4cd5933c5ddb5dc52a94664071951219a97710f0a32b"
|
||||
|
||||
[[package]]
|
||||
name = "vec_map"
|
||||
version = "0.8.2"
|
||||
|
|
|
@ -14,15 +14,16 @@ git-version = "0.3.4"
|
|||
ibig = "0.3.2"
|
||||
itertools = "0.10.1"
|
||||
ndarray = "0.15.3"
|
||||
predicates = "1.0.8"
|
||||
num-traits = "0.2"
|
||||
predicates = "1.0.8"
|
||||
sha2 = "0.9.5"
|
||||
wasm-bindgen = "0.2.74"
|
||||
wasm-bindgen = "=0.2.74" # 0.2.77 causes "remaining data" error
|
||||
|
||||
# Only for WebAssembly - include here for syntax highlighting
|
||||
#[target.'cfg(target_arch = "wasm32")'.dependencies]
|
||||
console_error_panic_hook = "0.1.6"
|
||||
js-sys = "0.3.51"
|
||||
html-escape = "0.2.9"
|
||||
num-bigint = "0.4.0"
|
||||
num-rational = "0.4.0"
|
||||
paste = "1.0.5"
|
||||
|
|
|
@ -29,6 +29,8 @@ var tblResult = document.getElementById('result');
|
|||
var divLogs2 = document.getElementById('resultLogs2');
|
||||
var olStageComments;
|
||||
|
||||
var detailedTransfers = {};
|
||||
|
||||
var worker = new Worker('worker.js?v=GITVERSION');
|
||||
|
||||
worker.onmessage = function(evt) {
|
||||
|
@ -78,10 +80,13 @@ worker.onmessage = function(evt) {
|
|||
|
||||
} else if (evt.data.type === 'updateStageComments') {
|
||||
let elLi = document.createElement('li');
|
||||
elLi.id = 'stage' + (olStageComments.childElementCount + 1);
|
||||
elLi.id = 'stage' + evt.data.stageNum;
|
||||
elLi.innerHTML = evt.data.comment;
|
||||
olStageComments.append(elLi);
|
||||
|
||||
} else if (evt.data.type === 'updateDetailedTransfers') {
|
||||
detailedTransfers[evt.data.stageNum] = evt.data.table;
|
||||
|
||||
} else if (evt.data.type === 'finalResultSummary') {
|
||||
divLogs2.insertAdjacentHTML('beforeend', evt.data.summary);
|
||||
document.getElementById('printPane').style.display = 'block';
|
||||
|
@ -170,6 +175,8 @@ async function clickCount() {
|
|||
tblResult.innerHTML = '';
|
||||
divLogs2.innerHTML = '';
|
||||
|
||||
detailedTransfers = {};
|
||||
|
||||
// Dispatch to worker
|
||||
worker.postMessage({
|
||||
'type': 'countElection',
|
||||
|
@ -187,6 +194,26 @@ async function clickCount() {
|
|||
});
|
||||
}
|
||||
|
||||
function viewDetailedTransfers(stageNum) {
|
||||
let wtransfers = window.open('', '', 'location=0,width=800,height=600');
|
||||
wtransfers.document.title = 'OpenTally Detailed Transfers: Stage ' + stageNum;
|
||||
|
||||
// Add stylesheets
|
||||
for (let elCSSBase of document.querySelectorAll('head link')) {
|
||||
let elCSS = wtransfers.document.createElement('link');
|
||||
elCSS.rel = elCSSBase.rel;
|
||||
elCSS.type = elCSSBase.type;
|
||||
if (elCSSBase.href.endsWith('?v=GITVERSION')) {
|
||||
elCSS.href = elCSSBase.href.replace('?v=GITVERSION', '?v=' + Math.random());
|
||||
} else {
|
||||
elCSS.href = elCSSBase.href;
|
||||
}
|
||||
wtransfers.document.head.appendChild(elCSS);
|
||||
}
|
||||
|
||||
wtransfers.document.body.innerHTML = detailedTransfers[stageNum];
|
||||
}
|
||||
|
||||
// Provide a default seed
|
||||
if (document.getElementById('txtSeed').value === '') {
|
||||
function pad(x) { if (x < 10) { return '0' + x; } return '' + x; }
|
||||
|
|
|
@ -94,7 +94,7 @@ table {
|
|||
color-adjust: exact;
|
||||
-webkit-print-color-adjust: exact;
|
||||
}
|
||||
.result td {
|
||||
table.result td, table.transfers td {
|
||||
padding: 0px 8px;
|
||||
height: 1em;
|
||||
}
|
||||
|
@ -127,10 +127,12 @@ td.elected {
|
|||
tr.info td {
|
||||
background-color: #f0f5fb;
|
||||
}
|
||||
tr.stage-no td:not(:empty), tr.hint-papers-votes td:not(:empty), tr.transfers td {
|
||||
tr.stage-no td:not(:empty), tr.hint-papers-votes td:not(:empty), tr.transfers td,
|
||||
table.transfers tr:first-child td, table.transfers tr:nth-last-child(2) td, table.transfers tr:last-child td {
|
||||
border-top: 1px solid #76858c;
|
||||
}
|
||||
tr.info:last-child td, .bb {
|
||||
tr.info:last-child td, .bb,
|
||||
table.transfers tr:first-child td, table.transfers tr:nth-last-child(2) td, table.transfers tr:last-child td {
|
||||
border-bottom: 1px solid #76858c;
|
||||
}
|
||||
.blw {
|
||||
|
@ -138,13 +140,18 @@ tr.info:last-child td, .bb {
|
|||
border-left: 2px solid #76858c;
|
||||
}
|
||||
|
||||
table.transfers tr:first-child td {
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
/* Table stripes */
|
||||
|
||||
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) {
|
||||
tr.candidate.votes td:nth-child(odd):not(.elected):not(.excluded),
|
||||
table.transfers td:nth-child(even) {
|
||||
background-color: #f9f9f9;
|
||||
}
|
||||
tr.candidate.transfers td.elected:nth-child(even),
|
||||
|
@ -161,33 +168,8 @@ tr.info.votes td:nth-child(odd) {
|
|||
background-color: #e8eef7;
|
||||
}
|
||||
|
||||
/* BLT input tool */
|
||||
|
||||
#selBallots {
|
||||
min-width: 10em;
|
||||
margin-right: 1em;
|
||||
}
|
||||
|
||||
#bltMain {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
#tblBallot {
|
||||
margin-top: 0.5em;
|
||||
margin-bottom: 0.5em;
|
||||
}
|
||||
|
||||
#tblBallot input {
|
||||
margin-right: 0.5ex;
|
||||
}
|
||||
|
||||
#divEditCandidates div {
|
||||
margin-bottom: 0.5em;
|
||||
}
|
||||
|
||||
#txtCandidates {
|
||||
min-width: 20em;
|
||||
min-height: 10em;
|
||||
a.detailedTransfersLink {
|
||||
color: #aaa;
|
||||
}
|
||||
|
||||
/* Print stylesheet */
|
||||
|
|
|
@ -105,7 +105,12 @@ function resumeCount() {
|
|||
}
|
||||
|
||||
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': 'updateStageComments', 'comment': wasm['update_stage_comments_' + numbers](state, stageNum), 'stageNum': stageNum});
|
||||
|
||||
let transfers_table = state.transfer_table_render_html(opts);
|
||||
if (transfers_table) {
|
||||
postMessage({'type': 'updateDetailedTransfers', 'table': transfers_table, 'stageNum': stageNum});
|
||||
}
|
||||
}
|
||||
|
||||
postMessage({'type': 'updateResultsTable', 'result': wasm['finalise_results_table_' + numbers](state, reportStyle)});
|
||||
|
|
|
@ -15,9 +15,19 @@
|
|||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
// --------------
|
||||
// Child packages
|
||||
|
||||
/// Transfer tables
|
||||
mod transfers;
|
||||
pub use transfers::{TransferTable, TransferTableCell, TransferTableColumn};
|
||||
|
||||
/// prettytable-compatible API for HTML table output in WebAssembly
|
||||
pub mod prettytable_html;
|
||||
|
||||
// --------
|
||||
// STV code
|
||||
|
||||
use super::{ExclusionMethod, STVError, STVOptions, SurplusMethod, SurplusOrder};
|
||||
use super::sample;
|
||||
|
||||
|
|
|
@ -0,0 +1,107 @@
|
|||
/* OpenTally: Open-source election vote counting
|
||||
* Copyright © 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/>.
|
||||
*/
|
||||
|
||||
use itertools::Itertools;
|
||||
|
||||
/// Table
|
||||
pub struct Table {
|
||||
/// Rows in the table
|
||||
rows: Vec<Row>,
|
||||
}
|
||||
|
||||
impl Table {
|
||||
/// Return a new [Table]
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
rows: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Add a [Row] to the table
|
||||
pub fn add_row(&mut self, row: Row) {
|
||||
self.rows.push(row);
|
||||
}
|
||||
|
||||
/// Alias for [add_row]
|
||||
pub fn set_titles(&mut self, row: Row) {
|
||||
self.add_row(row);
|
||||
}
|
||||
|
||||
/// Render the table as HTML
|
||||
pub fn to_string(&self) -> String {
|
||||
return format!(r#"<table class="transfers">{}</table>"#, self.rows.iter().map(|r| r.to_string()).join(""));
|
||||
}
|
||||
}
|
||||
|
||||
/// Row in a [Table]
|
||||
pub struct Row {
|
||||
/// Cells in the row
|
||||
cells: Vec<Cell>,
|
||||
}
|
||||
|
||||
impl Row {
|
||||
/// Return a new [Row]
|
||||
pub fn new(cells: Vec<Cell>) -> Self {
|
||||
Self {
|
||||
cells
|
||||
}
|
||||
}
|
||||
|
||||
/// Render the row as HTML
|
||||
fn to_string(&self) -> String {
|
||||
return format!(r#"<tr>{}</tr>"#, self.cells.iter().map(|c| c.to_string()).join(""));
|
||||
}
|
||||
}
|
||||
|
||||
/// Cell in a [Row]
|
||||
pub struct Cell {
|
||||
/// Content of the cell
|
||||
content: String,
|
||||
/// HTML tag/attributes
|
||||
attrs: Vec<&'static str>,
|
||||
}
|
||||
|
||||
impl Cell {
|
||||
/// Return a new [Cell]
|
||||
pub fn new(content: &str) -> Self {
|
||||
Self {
|
||||
content: String::from(content),
|
||||
attrs: vec!["td"],
|
||||
}
|
||||
}
|
||||
|
||||
/// Apply a style to the cell
|
||||
#[allow(unused_mut)]
|
||||
pub fn style_spec(mut self, spec: &str) -> Self {
|
||||
if spec.contains("H2") {
|
||||
self.attrs.push(r#"colspan="2""#);
|
||||
}
|
||||
if spec.contains("c") {
|
||||
self.attrs.push(r#"style="text-align:center""#);
|
||||
}
|
||||
if spec.contains("r") {
|
||||
self.attrs.push(r#"style="text-align:right""#);
|
||||
}
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
/// Render the cell as HTML
|
||||
fn to_string(&self) -> String {
|
||||
return format!(r#"<{}>{}</td>"#, self.attrs.join(" "), html_escape::encode_text(&self.content));
|
||||
}
|
||||
}
|
|
@ -15,7 +15,10 @@
|
|||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
use prettytable::{Cell, Row, Table};
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
use super::prettytable_html::{Cell, Row, Table};
|
||||
|
||||
use crate::election::{Candidate, CountState};
|
||||
use crate::numbers::Number;
|
||||
|
@ -287,10 +290,10 @@ impl<'e, N: Number> TransferTable<'e, N> {
|
|||
return checksum;
|
||||
}
|
||||
|
||||
/// Render table as plain text
|
||||
pub fn render_text(&self, state: &CountState<N>, opts: &STVOptions) -> String {
|
||||
/// Render table as [Table]
|
||||
fn render(&self, state: &CountState<N>, opts: &STVOptions) -> Table {
|
||||
let mut table = Table::new();
|
||||
table.set_format(*prettytable::format::consts::FORMAT_NO_LINESEP_WITH_TITLE);
|
||||
set_table_format(&mut table);
|
||||
|
||||
let show_transfers_per_ballot = !self.surpfrac.is_none() && opts.sum_surplus_transfers == SumSurplusTransfersMode::PerBallot;
|
||||
|
||||
|
@ -448,12 +451,18 @@ impl<'e, N: Number> TransferTable<'e, N> {
|
|||
|
||||
table.add_row(Row::new(row));
|
||||
|
||||
return table.to_string();
|
||||
return table;
|
||||
}
|
||||
|
||||
/// Render table as plain text
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
pub fn render_text(&self, state: &CountState<N>, opts: &STVOptions) -> String {
|
||||
return self.render(state, opts).to_string();
|
||||
}
|
||||
|
||||
/// Render table as HTML
|
||||
pub fn render_html(&self) -> String {
|
||||
todo!();
|
||||
pub fn render_html(&self, state: &CountState<N>, opts: &STVOptions) -> String {
|
||||
return self.render(state, opts).to_string();
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -497,3 +506,13 @@ pub struct TransferTableCell<N: Number> {
|
|||
/// Votes transferred to the continuing candidate
|
||||
pub votes_out: N,
|
||||
}
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
fn set_table_format(table: &mut Table) {
|
||||
table.set_format(*prettytable::format::consts::FORMAT_NO_LINESEP_WITH_TITLE);
|
||||
}
|
||||
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
fn set_table_format(_table: &mut Table) {
|
||||
// No op
|
||||
}
|
||||
|
|
|
@ -141,8 +141,8 @@ macro_rules! impl_type {
|
|||
/// Wrapper for [update_stage_comments]
|
||||
#[wasm_bindgen]
|
||||
#[allow(non_snake_case)]
|
||||
pub fn [<update_stage_comments_$type>](state: &[<CountState$type>]) -> String {
|
||||
return update_stage_comments(&state.0);
|
||||
pub fn [<update_stage_comments_$type>](state: &[<CountState$type>], stage_num: usize) -> String {
|
||||
return update_stage_comments(&state.0, stage_num);
|
||||
}
|
||||
|
||||
/// Wrapper for [finalise_results_table]
|
||||
|
@ -173,6 +173,14 @@ macro_rules! impl_type {
|
|||
pub fn new(election: &[<Election$type>]) -> Self {
|
||||
return [<CountState$type>](CountState::new(election.as_static()));
|
||||
}
|
||||
|
||||
/// Call [render_html](crate::stv::transfers::TransferTable::render_html) on [CountState::transfer_table]
|
||||
pub fn transfer_table_render_html(&self, opts: &STVOptions) -> Option<String> {
|
||||
return match &self.0.transfer_table {
|
||||
Some(tt) => Some(tt.render_html(&self.0, &opts.0)), // TODO
|
||||
None => None,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/// Wrapper for [Election]
|
||||
|
@ -613,8 +621,12 @@ fn update_results_table<N: Number>(stage_num: usize, state: &CountState<N>, opts
|
|||
}
|
||||
|
||||
/// Get the comment for the current stage
|
||||
fn update_stage_comments<N: Number>(state: &CountState<N>) -> String {
|
||||
return state.logger.render().join(" ");
|
||||
fn update_stage_comments<N: Number>(state: &CountState<N>, stage_num: usize) -> String {
|
||||
let mut comments = state.logger.render().join(" ");
|
||||
if let Some(_) = state.transfer_table {
|
||||
comments.push_str(&format!(r##" <a href="#" class="detailedTransfersLink" onclick="viewDetailedTransfers({});return false;">[View detailed transfers]</a>"##, stage_num));
|
||||
}
|
||||
return comments;
|
||||
}
|
||||
|
||||
/// Generate the final column of the HTML results table
|
||||
|
|
Loading…
Reference in New Issue