upload code
This commit is contained in:
commit
3f507064ce
111 changed files with 9258 additions and 0 deletions
16
stdlib/Cargo.toml
Normal file
16
stdlib/Cargo.toml
Normal file
|
@ -0,0 +1,16 @@
|
|||
# -*- eval: (cargo-minor-mode 1) -*-
|
||||
|
||||
[package]
|
||||
name = "stdlib"
|
||||
version = "0.0.0"
|
||||
publish = false
|
||||
edition = "2021"
|
||||
authors = ["Dominic Meiser <git@msrd0.de>"]
|
||||
|
||||
[dependencies]
|
||||
indexmap = "1.9"
|
||||
itoa = "1"
|
||||
num-integer = "0.1.45"
|
||||
paste = "1"
|
||||
rand = { version = "0.8", features = ["getrandom"], default-features = false }
|
||||
svgwriter = "0.1"
|
145
stdlib/src/lib.rs
Normal file
145
stdlib/src/lib.rs
Normal file
|
@ -0,0 +1,145 @@
|
|||
use rand::{
|
||||
distributions::{Distribution as _, Uniform},
|
||||
rngs::OsRng,
|
||||
Rng
|
||||
};
|
||||
use std::{
|
||||
fmt::{self, Debug, Formatter, Write as _},
|
||||
panic::panic_any
|
||||
};
|
||||
pub use std::{
|
||||
option::Option,
|
||||
primitive::{bool, isize as int}
|
||||
};
|
||||
|
||||
mod output;
|
||||
pub use output::{
|
||||
BarChartPrinter, BubbleChartPrinter, ListPrinter, OutputList, OutputPrinter,
|
||||
OutputPrinterAny, OutputPrinterTwoVars, OutputTarget
|
||||
};
|
||||
|
||||
#[doc(hidden)]
|
||||
pub struct FnNoReturn {
|
||||
msg: String
|
||||
}
|
||||
|
||||
/// Called as the last part of every function to abnormally terminate in case there is a
|
||||
/// missing return statement.
|
||||
#[track_caller]
|
||||
pub fn fn_no_return(fun_ident: &str) -> FnNoReturn {
|
||||
FnNoReturn {
|
||||
msg: format!(
|
||||
r#"Function `{}` has finished without returning a value, but it declares a return type.
|
||||
|
||||
Help: The function was called with the following arguments:"#,
|
||||
fun_ident
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl FnNoReturn {
|
||||
pub fn add_input<T>(mut self, key: &'static str, value: &T) -> Self
|
||||
where
|
||||
T: Debug
|
||||
{
|
||||
write!(self.msg, "\n\t{key}: {value:#?}").unwrap();
|
||||
self
|
||||
}
|
||||
|
||||
pub fn panic(self) -> ! {
|
||||
panic_any(self.msg)
|
||||
}
|
||||
}
|
||||
|
||||
/// Return the default (pseudo) random number generator.
|
||||
fn rng() -> impl Rng {
|
||||
OsRng::default()
|
||||
}
|
||||
|
||||
/// Used for input argument parsing.
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub enum ArgValue {
|
||||
Value(int),
|
||||
Range { min: int, max: int }
|
||||
}
|
||||
|
||||
impl ArgValue {
|
||||
pub fn get_value(self) -> int {
|
||||
match self {
|
||||
Self::Value(value) => value,
|
||||
Self::Range { min, max } => {
|
||||
Uniform::new_inclusive(min, max).sample(&mut rng())
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The result of a coin flip.
|
||||
pub enum Coin {
|
||||
Head,
|
||||
Tail
|
||||
}
|
||||
|
||||
impl Coin {
|
||||
/// Flip a coin, returning [`Coin::Head`] with probability `prob`.
|
||||
pub fn flip(prob: f32) -> Self {
|
||||
debug_assert!(prob > 0.0);
|
||||
debug_assert!(prob < 1.0);
|
||||
|
||||
if rng().gen::<f32>() < prob {
|
||||
Self::Head
|
||||
} else {
|
||||
Self::Tail
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The `Rc` reference-counting smart pointer of the language.
|
||||
pub struct Rc<T>(std::rc::Rc<std::cell::RefCell<T>>);
|
||||
|
||||
impl<T> Rc<T> {
|
||||
pub fn new(inner: T) -> Self {
|
||||
Self(std::rc::Rc::new(std::cell::RefCell::new(inner)))
|
||||
}
|
||||
|
||||
pub fn into_inner(this: Rc<T>) -> T
|
||||
where
|
||||
T: Clone
|
||||
{
|
||||
match std::rc::Rc::try_unwrap(this.0) {
|
||||
Ok(cell) => cell.into_inner(),
|
||||
Err(rc) => rc.borrow().clone()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn with_mut<F>(&mut self, callback: F)
|
||||
where
|
||||
F: FnOnce(&mut T)
|
||||
{
|
||||
let mut this = self.0.borrow_mut();
|
||||
callback(&mut this);
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> Clone for Rc<T> {
|
||||
fn clone(&self) -> Self {
|
||||
Self(std::rc::Rc::clone(&self.0))
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Debug> Debug for Rc<T> {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
|
||||
f.debug_tuple("Rc")
|
||||
.field(&DebugPtr(self.0.as_ptr()))
|
||||
.field(&self.0.borrow())
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
struct DebugPtr<T>(*const T);
|
||||
|
||||
impl<T> Debug for DebugPtr<T> {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
|
||||
write!(f, "{:#X}", self.0 as usize)
|
||||
}
|
||||
}
|
356
stdlib/src/output/bar.rs
Normal file
356
stdlib/src/output/bar.rs
Normal file
|
@ -0,0 +1,356 @@
|
|||
use super::{OutputList, OutputPrinter, Svg};
|
||||
use crate::int;
|
||||
use std::{
|
||||
cmp::Ordering,
|
||||
collections::BTreeMap,
|
||||
fmt::Write as _,
|
||||
io::{self, Write},
|
||||
mem
|
||||
};
|
||||
use svgwriter::{
|
||||
tags::{Group, Path, Rect, Style, TSpan, TagWithPresentationAttributes as _, Text},
|
||||
Data, Transform
|
||||
};
|
||||
|
||||
/// Output printer for SVG Bar Charts. Works best with one
|
||||
/// output variable.
|
||||
pub struct BarChartPrinter {
|
||||
/// Set to true to enable SVG/XML pretty printing.
|
||||
pretty: bool
|
||||
}
|
||||
|
||||
impl BarChartPrinter {
|
||||
pub fn new(pretty: bool) -> Self {
|
||||
Self { pretty }
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
struct YAxis {
|
||||
percentages: Vec<f32>,
|
||||
max: f32
|
||||
}
|
||||
|
||||
impl YAxis {
|
||||
fn add_percentage(&mut self, percentage: f32) {
|
||||
self.percentages.push(percentage);
|
||||
self.max = self.max.max(percentage).ceil();
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Eq)]
|
||||
enum Range {
|
||||
Single(int),
|
||||
Range { start: int, len: int }
|
||||
}
|
||||
|
||||
impl Range {
|
||||
fn start(&self) -> int {
|
||||
match self {
|
||||
Self::Single(value) => *value,
|
||||
Self::Range { start, .. } => *start
|
||||
}
|
||||
}
|
||||
|
||||
fn end(&self) -> int {
|
||||
match self {
|
||||
Self::Single(value) => *value,
|
||||
Self::Range { start, len } => start + len - 1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialEq for Range {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
self.start() == other.start() && self.end() == other.end()
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialOrd for Range {
|
||||
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
|
||||
Some(self.cmp(other))
|
||||
}
|
||||
}
|
||||
|
||||
impl Ord for Range {
|
||||
fn cmp(&self, other: &Self) -> Ordering {
|
||||
if self == other {
|
||||
Ordering::Equal
|
||||
} else if self.start() == other.start() {
|
||||
self.end().cmp(&other.end())
|
||||
} else {
|
||||
self.start().cmp(&other.start())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
struct XAxis {
|
||||
values: BTreeMap<Range, usize>,
|
||||
max: Option<int>,
|
||||
min: Option<int>
|
||||
}
|
||||
|
||||
impl XAxis {
|
||||
fn add_value(&mut self, value: int, qty: usize) {
|
||||
let range = Range::Single(value);
|
||||
if let Some(range_qty) = self.values.get_mut(&range) {
|
||||
*range_qty += qty;
|
||||
} else {
|
||||
self.values.insert(range, qty);
|
||||
}
|
||||
|
||||
if self.max.map(|max| max < value).unwrap_or(true) {
|
||||
self.max = Some(value);
|
||||
}
|
||||
if self.min.map(|min| min > value).unwrap_or(true) {
|
||||
self.min = Some(value);
|
||||
}
|
||||
}
|
||||
|
||||
fn with_step(&mut self, step: int) {
|
||||
let values = mem::take(&mut self.values);
|
||||
for (range, qty) in values {
|
||||
let value = match range {
|
||||
Range::Single(value) => value,
|
||||
_ => panic!("with_step may only be called once")
|
||||
};
|
||||
let offset = value - self.min.unwrap();
|
||||
let start = self.min.unwrap() + (offset / step) * step;
|
||||
let range = Range::Range { start, len: step };
|
||||
|
||||
if let Some(range_qty) = self.values.get_mut(&range) {
|
||||
*range_qty += qty;
|
||||
} else {
|
||||
self.values.insert(range, qty);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl OutputPrinter for BarChartPrinter {
|
||||
fn print<I, E, W>(self, list: OutputList<I>, mut out: W) -> io::Result<()>
|
||||
where
|
||||
I: IntoIterator<Item = (E, usize)>,
|
||||
E: IntoIterator<Item = (&'static str, int)>,
|
||||
W: Write
|
||||
{
|
||||
let mut yaxis = YAxis::default();
|
||||
let mut xaxis = XAxis::default();
|
||||
let mut xaxis_name = None;
|
||||
|
||||
for (entry, qty) in list.samples {
|
||||
for (key, value) in entry {
|
||||
if xaxis_name.is_none() {
|
||||
xaxis_name = Some(key);
|
||||
} else if xaxis_name.unwrap() != key {
|
||||
continue;
|
||||
}
|
||||
|
||||
xaxis.add_value(value, qty);
|
||||
}
|
||||
}
|
||||
let xaxis_name = xaxis_name.expect("Missing output variable");
|
||||
|
||||
// group the xaxis together
|
||||
let mut xaxis_step = 1;
|
||||
while (xaxis.max.unwrap() - xaxis.min.unwrap()) / xaxis_step > 12 {
|
||||
xaxis_step += 1;
|
||||
}
|
||||
xaxis.with_step(xaxis_step);
|
||||
|
||||
// build the yaxis
|
||||
for qty in xaxis.values.values().copied() {
|
||||
yaxis.add_percentage(qty as f32 / list.n as f32 * 100.0);
|
||||
}
|
||||
|
||||
let chart_margin = 30;
|
||||
let chart_margin_x = 70;
|
||||
let chart_margin_y = 100;
|
||||
let axis_step_dist = 45;
|
||||
let bar_width = 20;
|
||||
let yaxis_steps = 7;
|
||||
// note that the number of datapoints is equivalent on both axis, therefore
|
||||
// we can use the yaxis to calculate the width
|
||||
let chart_width = (yaxis.percentages.len() as i32 + 1) * axis_step_dist;
|
||||
let chart_height = yaxis_steps * axis_step_dist + axis_step_dist / 2;
|
||||
let svg_width = chart_width + chart_margin + chart_margin_x;
|
||||
let svg_height = chart_height + chart_margin + chart_margin_y;
|
||||
let mut svg = Svg::new(svg_width, svg_height);
|
||||
|
||||
// add svg style
|
||||
let mut style = concat!(
|
||||
"*{font-family:\"Source Serif 4\",serif}",
|
||||
".ui{stroke:#000;stroke-width:1px;fill:#000;font-size:12pt}",
|
||||
".ui text,.bg,.b{stroke-width:0}",
|
||||
".p{font-size:10pt}",
|
||||
".bg{fill:#fff}",
|
||||
".b{stroke:#f60;fill:#f60}",
|
||||
// dark theme
|
||||
"@media(prefers-color-scheme: dark){",
|
||||
".ui{stroke:#ccc;fill:#ccc}",
|
||||
".bg{fill:#222}",
|
||||
"}"
|
||||
)
|
||||
.to_owned();
|
||||
// allow svg to be printed to pdf by headless chromium
|
||||
write!(
|
||||
style,
|
||||
"@page{{margin:0;size:calc(20cm*{svg_width}/{svg_height}) 20cm}}"
|
||||
)
|
||||
.unwrap();
|
||||
svg.push(Style::new().append(style));
|
||||
|
||||
// add svg background
|
||||
svg.push(
|
||||
Rect::new()
|
||||
.with_class("bg")
|
||||
.with_x(0)
|
||||
.with_y(0)
|
||||
.with_width(chart_width + chart_margin + chart_margin_x)
|
||||
.with_height(chart_height + chart_margin + chart_margin_y)
|
||||
);
|
||||
|
||||
let mut group = Group::new()
|
||||
.with_transform(
|
||||
Transform::new().translate(chart_margin_x, chart_height + chart_margin)
|
||||
)
|
||||
.with_class("ui");
|
||||
|
||||
// build vertical bars
|
||||
let mut bars_data = Data::new();
|
||||
let mut bars_text = Text::new().with_text_anchor("middle");
|
||||
for (i, percentage) in yaxis.percentages.iter().enumerate() {
|
||||
let offset = (i + 1) as i32 * axis_step_dist;
|
||||
let height = percentage / yaxis.max * (yaxis_steps * axis_step_dist) as f32;
|
||||
bars_data
|
||||
.move_to(offset - bar_width / 2, 0)
|
||||
.horiz_line_by(bar_width)
|
||||
.vert_line_by(-height)
|
||||
.horiz_line_by(-bar_width)
|
||||
.close();
|
||||
bars_text.push(
|
||||
TSpan::new()
|
||||
.with_x(offset)
|
||||
.with_y(-height - 5.0)
|
||||
.append(format!("{percentage:.2}%"))
|
||||
);
|
||||
}
|
||||
group.push(Path::new().with_d(bars_data).with_class("b"));
|
||||
group.push(bars_text.with_class("b p"));
|
||||
|
||||
// build y axis line
|
||||
let mut axis_line = Data::new();
|
||||
axis_line
|
||||
.move_to(0, -chart_height)
|
||||
.line_by(3, 5)
|
||||
.horiz_line_by(-6)
|
||||
.close()
|
||||
.vert_line_to(0);
|
||||
for (i, _) in (axis_step_dist .. chart_height)
|
||||
.step_by(axis_step_dist as usize)
|
||||
.enumerate()
|
||||
{
|
||||
axis_line
|
||||
.move_by((i == 0).then_some(-3).unwrap_or(-6), -axis_step_dist)
|
||||
.horiz_line_by(6);
|
||||
}
|
||||
|
||||
// build x axis line
|
||||
axis_line
|
||||
.move_to(chart_width, 0)
|
||||
.line_by(-5, 3)
|
||||
.vert_line_by(-6)
|
||||
.close()
|
||||
.horiz_line_to(0);
|
||||
for (i, _) in (axis_step_dist .. chart_width)
|
||||
.step_by(axis_step_dist as usize)
|
||||
.enumerate()
|
||||
{
|
||||
axis_line
|
||||
.move_by(axis_step_dist, (i == 0).then_some(-3).unwrap_or(-6))
|
||||
.vert_line_by(6);
|
||||
}
|
||||
group.push(Path::new().with_d(axis_line).with_stroke_linejoin("arcs"));
|
||||
|
||||
// build y axis descriptions
|
||||
let mut yaxis_text = Text::new()
|
||||
.with_text_anchor("end")
|
||||
.with_dominant_baseline("middle");
|
||||
for i in 0 .. yaxis_steps {
|
||||
yaxis_text.push(
|
||||
TSpan::new()
|
||||
.with_x(-5)
|
||||
.with_y((i as i32 + 1) * -axis_step_dist)
|
||||
.append(format!(
|
||||
"{:.2}%",
|
||||
yaxis.max / yaxis_steps as f32 * (i + 1) as f32
|
||||
))
|
||||
);
|
||||
}
|
||||
group.push(yaxis_text);
|
||||
|
||||
// build x axis descriptions
|
||||
let mut xaxis_text = Text::new()
|
||||
.with_class("b")
|
||||
.with_text_anchor("middle")
|
||||
.with_dominant_baseline("hanging");
|
||||
for (i, range) in xaxis.values.keys().copied().enumerate() {
|
||||
let x = (i + 1) as i32 * axis_step_dist;
|
||||
let y = 5;
|
||||
match range {
|
||||
Range::Single(value) => {
|
||||
xaxis_text.push(
|
||||
TSpan::new()
|
||||
.with_x(x)
|
||||
.with_y(y)
|
||||
.append(format!("{}", value))
|
||||
);
|
||||
},
|
||||
Range::Range { start, len } => {
|
||||
xaxis_text.push(
|
||||
TSpan::new()
|
||||
.with_x(x)
|
||||
.with_y(y)
|
||||
.append(format!("{}", start))
|
||||
);
|
||||
xaxis_text.push(TSpan::new().with_x(x).with_dy(".8em").append("-"));
|
||||
xaxis_text.push(
|
||||
TSpan::new()
|
||||
.with_x(x)
|
||||
.with_dy("1em")
|
||||
.append(format!("{}", start + len - 1))
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
group.push(xaxis_text);
|
||||
|
||||
// build y axis legend
|
||||
let mut ylegend = Text::new().with_x(-10).with_y(-chart_height - 5);
|
||||
ylegend.push(format!("Percentage of {} samples", list.n));
|
||||
group.push(ylegend);
|
||||
|
||||
// build x axis legend
|
||||
let mut xlegend = Text::new()
|
||||
.with_x(0)
|
||||
.with_y(chart_margin_y - chart_margin)
|
||||
.with_dominant_baseline("hanging");
|
||||
xlegend.push("Legend: ");
|
||||
xlegend.push(
|
||||
TSpan::new()
|
||||
.with_class("b")
|
||||
.append(format!("{}", xaxis.min.unwrap_or(1)))
|
||||
);
|
||||
xlegend.push(format!(": {xaxis_name}"));
|
||||
group.push(xlegend);
|
||||
|
||||
svg.push(group);
|
||||
let svg = if self.pretty {
|
||||
svg.to_string_pretty()
|
||||
} else {
|
||||
svg.to_string()
|
||||
};
|
||||
writeln!(out, "{}", svg)
|
||||
}
|
||||
}
|
21
stdlib/src/output/bubble.js
Normal file
21
stdlib/src/output/bubble.js
Normal file
|
@ -0,0 +1,21 @@
|
|||
var svg,xn,yn,tt,ttp,ttx,tty;
|
||||
function e(c){return document.getElementById(c)}
|
||||
function i(xn0,yn0){
|
||||
xn=xn0;yn=yn0;
|
||||
svg=document.getElementsByTagName("svg")[0];
|
||||
tt=e("tt");ttp=e("ttp");ttx=e("ttx");tty=e("tty");
|
||||
c()
|
||||
}
|
||||
function tr(x,y){tt.setAttribute('transform','translate('+x+','+y+')')}
|
||||
function s(ev,p,x,y) {
|
||||
ttp.textContent=p+"%";
|
||||
ttx.textContent=xn+": "+x;
|
||||
tty.textContent=yn+": "+y;
|
||||
var r = ev.target.getBoundingClientRect();
|
||||
var p = ev.target.ownerSVGElement.createSVGPoint();
|
||||
p.x=r.x+r.width/2;
|
||||
p.y=r.top;
|
||||
p=p.matrixTransform(svg.getScreenCTM().inverse());
|
||||
tr(p.x,p.y-5);
|
||||
}
|
||||
function c(){tr(-999,-999)}
|
379
stdlib/src/output/bubble.rs
Normal file
379
stdlib/src/output/bubble.rs
Normal file
|
@ -0,0 +1,379 @@
|
|||
use super::{OutputList, OutputPrinter, Svg};
|
||||
use crate::int;
|
||||
use indexmap::IndexMap;
|
||||
use std::{fmt::Write as _, io};
|
||||
use svgwriter::{
|
||||
tags::{
|
||||
Circle, Group, Path, Rect, Script, Style, TSpan, TagWithCoreAttributes as _,
|
||||
TagWithGlobalEventAttributes as _, TagWithPresentationAttributes as _, Text
|
||||
},
|
||||
Data, Transform
|
||||
};
|
||||
use num_integer::div_ceil;
|
||||
|
||||
/// Output printer for SVG Bar Charts. Works only with two
|
||||
/// output variables.
|
||||
pub struct BubbleChartPrinter {
|
||||
/// Set to true to enable SVG/XML pretty printing.
|
||||
pretty: bool
|
||||
}
|
||||
|
||||
impl BubbleChartPrinter {
|
||||
pub fn new(pretty: bool) -> Self {
|
||||
Self { pretty }
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
struct Axis {
|
||||
values: Vec<int>,
|
||||
min: Option<int>,
|
||||
max: Option<int>
|
||||
}
|
||||
|
||||
impl Axis {
|
||||
fn add_value(&mut self, value: int) {
|
||||
self.values.push(value);
|
||||
if self.max.map(|max| max < value).unwrap_or(true) {
|
||||
self.max = Some(value);
|
||||
}
|
||||
if self.min.map(|min| min > value).unwrap_or(true) {
|
||||
self.min = Some(value);
|
||||
}
|
||||
}
|
||||
|
||||
fn min(&self) -> int {
|
||||
self.min.unwrap_or(0)
|
||||
}
|
||||
|
||||
fn max(&self) -> int {
|
||||
self.max.unwrap_or(0)
|
||||
}
|
||||
|
||||
fn step(&self, preferred_step_count: usize) -> (int, usize) {
|
||||
let range = self.max() - self.min();
|
||||
if range == 0 {
|
||||
return (0, 1);
|
||||
}
|
||||
|
||||
let mut step = 1;
|
||||
loop {
|
||||
let count = div_ceil(range, step) + 1;
|
||||
if (count - preferred_step_count as int).unsigned_abs() as usize
|
||||
<= preferred_step_count / 2
|
||||
{
|
||||
return (step, count as usize);
|
||||
}
|
||||
step += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl OutputPrinter for BubbleChartPrinter {
|
||||
fn print<I, E, W>(self, list: OutputList<I>, mut out: W) -> io::Result<()>
|
||||
where
|
||||
I: IntoIterator<Item = (E, usize)>,
|
||||
E: IntoIterator<Item = (&'static str, int)>,
|
||||
W: io::Write
|
||||
{
|
||||
let mut percentages = Vec::new();
|
||||
let mut min_percentage: f32 = 100.0;
|
||||
let mut max_percentage: f32 = 0.0;
|
||||
let mut axis: IndexMap<&'static str, Axis> = IndexMap::new();
|
||||
for (entry, qty) in list.samples {
|
||||
let percentage = qty as f32 / list.n as f32 * 100.0;
|
||||
percentages.push(percentage);
|
||||
min_percentage = min_percentage.min(percentage);
|
||||
max_percentage = max_percentage.max(percentage);
|
||||
for (key, value) in entry {
|
||||
if !axis.contains_key(key) {
|
||||
axis.insert(key, Axis::default());
|
||||
}
|
||||
axis.get_mut(key).unwrap().add_value(value);
|
||||
}
|
||||
}
|
||||
|
||||
let (yaxis_name, yaxis) = axis
|
||||
.swap_remove_index(1)
|
||||
.expect("Cannot use BubbleChartPrinter with less than two variables");
|
||||
let (xaxis_name, xaxis) = axis.swap_remove_index(0).unwrap();
|
||||
|
||||
let chart_margin_x = 60;
|
||||
let chart_margin_y = 60;
|
||||
let bubble_min_r: f32 = 2.5;
|
||||
let bubble_max_r: f32 = 15.0;
|
||||
let axis_step_dist = 40;
|
||||
let (xaxis_step, xaxis_step_count) = xaxis.step(8);
|
||||
let (yaxis_step, yaxis_step_count) = yaxis.step(6);
|
||||
let chart_width = axis_step_dist * xaxis_step_count as i32;
|
||||
let chart_height = axis_step_dist * yaxis_step_count as i32;
|
||||
let svg_width = chart_width + 2 * chart_margin_x;
|
||||
let svg_height = chart_height + 2 * chart_margin_y;
|
||||
let mut svg = Svg::new(svg_width, svg_height)
|
||||
.with_onload(format!("i({xaxis_name:?},{yaxis_name:?})"));
|
||||
|
||||
// add svg style and script
|
||||
let mut style = concat!(
|
||||
"*{font-family:\"Source Serif 4\",serif}",
|
||||
".ui,#tt{stroke:#000;stroke-width:1px;fill:#000;font-size:12pt}",
|
||||
"#tt{font-size:10pt}",
|
||||
"#tt path{fill:rgba(238,238,238,0.85)}",
|
||||
"#ttp{font-weight:bold}",
|
||||
"text,.bg,.b{stroke-width:0}",
|
||||
".bg{fill:#fff}",
|
||||
".b,#ttx,#tty{fill:#f60}",
|
||||
".b circle,circle.b{opacity:0.8}",
|
||||
// dark theme
|
||||
"@media(prefers-color-scheme: dark){",
|
||||
"#tt path{fill:rgba(34,34,34,0.7)}",
|
||||
".ui,#tt{stroke:#ccc;fill:#ccc}",
|
||||
".bg{fill:#222}",
|
||||
"}"
|
||||
)
|
||||
.to_owned();
|
||||
// allow svg to be printed to pdf by headless chromium
|
||||
write!(
|
||||
style,
|
||||
"@page{{margin:0;size:calc(20cm*{svg_width}/{svg_height}) 20cm}}"
|
||||
)
|
||||
.unwrap();
|
||||
svg.push(Style::new().append(style));
|
||||
svg.push(
|
||||
Script::new()
|
||||
.with_ty("text/ecmascript")
|
||||
.append(include_str!("bubble.js").trim())
|
||||
);
|
||||
|
||||
// add svg background
|
||||
svg.push(
|
||||
Rect::new()
|
||||
.with_class("bg")
|
||||
.with_x(0)
|
||||
.with_y(0)
|
||||
.with_width(chart_width + 2 * chart_margin_x)
|
||||
.with_height(chart_height + 2 * chart_margin_y)
|
||||
.with_onmouseover("c()")
|
||||
);
|
||||
|
||||
let mut group = Group::new()
|
||||
.with_transform(
|
||||
Transform::new().translate(chart_margin_x, chart_height + chart_margin_y)
|
||||
)
|
||||
.with_class("ui");
|
||||
|
||||
// build bubbles
|
||||
let mut bubbles = Group::new().with_class("b").with_transform(
|
||||
Transform::new()
|
||||
.translate(axis_step_dist / 2, axis_step_dist / -2)
|
||||
.scale(1.0, -1.0)
|
||||
);
|
||||
for (i, percentage) in percentages.iter().enumerate() {
|
||||
let xvalue = xaxis.values[i];
|
||||
let yvalue = yaxis.values[i];
|
||||
|
||||
let xpos =
|
||||
(xvalue - xaxis.min()) as f32 / xaxis_step as f32 * axis_step_dist as f32;
|
||||
let ypos =
|
||||
(yvalue - yaxis.min()) as f32 / yaxis_step as f32 * axis_step_dist as f32;
|
||||
// we want the area to grow proportional to the percentage, so the
|
||||
// radius grows proportional to the square root of the percentage
|
||||
let radius = bubble_min_r
|
||||
+ ((bubble_max_r - bubble_min_r)
|
||||
* ((percentage - min_percentage)
|
||||
/ (max_percentage - min_percentage))
|
||||
.sqrt());
|
||||
bubbles.push(
|
||||
Circle::new()
|
||||
.with_cx(xpos)
|
||||
.with_cy(ypos)
|
||||
.with_r(radius)
|
||||
.with_onmouseover(format!(
|
||||
"s(evt,\"{:.2}\",{},{})",
|
||||
percentage, xvalue, yvalue
|
||||
))
|
||||
);
|
||||
}
|
||||
group.push(bubbles);
|
||||
|
||||
// build y axis line
|
||||
let mut axis_line0 = Data::new();
|
||||
let mut axis_line1 = Data::new();
|
||||
axis_line0.move_to(0, -chart_height).vert_line_to(0);
|
||||
axis_line1
|
||||
.move_to(chart_width, -chart_height)
|
||||
.vert_line_to(0);
|
||||
for i in 0 .. yaxis_step_count {
|
||||
let (offset, step) = match i {
|
||||
0 => (-3, -axis_step_dist / 2),
|
||||
_ => (-6, -axis_step_dist)
|
||||
};
|
||||
axis_line0.move_by(offset, step).horiz_line_by(6);
|
||||
axis_line1.move_by(offset, step).horiz_line_by(6);
|
||||
}
|
||||
|
||||
// build x axis line
|
||||
axis_line0.move_to(chart_width, 0).horiz_line_to(0);
|
||||
axis_line1
|
||||
.move_to(chart_width, -chart_height)
|
||||
.horiz_line_to(0);
|
||||
for i in 0 .. xaxis_step_count {
|
||||
let (offset, step) = match i {
|
||||
0 => (-3, axis_step_dist / 2),
|
||||
_ => (-6, axis_step_dist)
|
||||
};
|
||||
axis_line0.move_by(step, offset).vert_line_by(6);
|
||||
axis_line1.move_by(step, offset).vert_line_by(6);
|
||||
}
|
||||
group.push(
|
||||
Group::new()
|
||||
.with_stroke_linejoin("arcs")
|
||||
.append(Path::new().with_d(axis_line0))
|
||||
.append(Path::new().with_d(axis_line1))
|
||||
);
|
||||
|
||||
// build y axis descriptions
|
||||
let mut yaxis_text0 = Text::new()
|
||||
.with_text_anchor("end")
|
||||
.with_dominant_baseline("middle");
|
||||
let mut yaxis_text1 = Text::new().with_dominant_baseline("middle");
|
||||
for i in 0 .. yaxis_step_count {
|
||||
let text = format!("{}", yaxis.min() + i as int * yaxis_step,);
|
||||
yaxis_text0.push(
|
||||
TSpan::new()
|
||||
.with_x(-5)
|
||||
.with_y(i as i32 * -axis_step_dist - axis_step_dist / 2)
|
||||
.append(text.clone())
|
||||
);
|
||||
yaxis_text1.push(
|
||||
TSpan::new()
|
||||
.with_x(chart_width + 5)
|
||||
.with_y(i as i32 * -axis_step_dist - axis_step_dist / 2)
|
||||
.append(text)
|
||||
);
|
||||
}
|
||||
group.push(yaxis_text0);
|
||||
group.push(yaxis_text1);
|
||||
|
||||
// build x axis descriptions
|
||||
let mut xaxis_text0 = Text::new().with_text_anchor("middle");
|
||||
let mut xaxis_text1 = Text::new()
|
||||
.with_text_anchor("middle")
|
||||
.with_dominant_baseline("hanging");
|
||||
for i in 0 .. xaxis_step_count {
|
||||
let text = format!("{}", xaxis.min() + i as int * xaxis_step,);
|
||||
xaxis_text0.push(
|
||||
TSpan::new()
|
||||
.with_x(i as i32 * axis_step_dist + axis_step_dist / 2)
|
||||
.with_y(-chart_height - 5)
|
||||
.append(text.clone())
|
||||
);
|
||||
xaxis_text1.push(
|
||||
TSpan::new()
|
||||
.with_x(i as i32 * axis_step_dist + axis_step_dist / 2)
|
||||
.with_y(5)
|
||||
.append(text)
|
||||
);
|
||||
}
|
||||
group.push(xaxis_text0);
|
||||
group.push(xaxis_text1);
|
||||
|
||||
// build y axis legend
|
||||
group.push(
|
||||
Group::new()
|
||||
.with_transform(
|
||||
Transform::new()
|
||||
.translate(15 - chart_margin_x, chart_height / -2)
|
||||
.rotate(270.0)
|
||||
)
|
||||
.append(
|
||||
Text::new()
|
||||
.with_text_anchor("middle")
|
||||
.with_dominant_baseline("middle")
|
||||
.append(yaxis_name)
|
||||
)
|
||||
);
|
||||
group.push(
|
||||
Group::new()
|
||||
.with_transform(
|
||||
Transform::new()
|
||||
.translate(chart_width + chart_margin_x - 15, chart_height / -2)
|
||||
.rotate(270.0)
|
||||
)
|
||||
.append(
|
||||
Text::new()
|
||||
.with_text_anchor("middle")
|
||||
.with_dominant_baseline("middle")
|
||||
.append(yaxis_name)
|
||||
)
|
||||
);
|
||||
|
||||
// build x axis legend
|
||||
group.push(
|
||||
Text::new()
|
||||
.with_text_anchor("middle")
|
||||
.with_dominant_baseline("middle")
|
||||
.append(
|
||||
TSpan::new()
|
||||
.with_x(chart_width / 2)
|
||||
.with_y(chart_margin_y - 15)
|
||||
.append(xaxis_name)
|
||||
)
|
||||
.append(
|
||||
TSpan::new()
|
||||
.with_x(chart_width / 2)
|
||||
.with_y(15 - chart_margin_y - chart_height)
|
||||
.append(xaxis_name)
|
||||
)
|
||||
);
|
||||
|
||||
svg.push(group);
|
||||
|
||||
// build tooltip
|
||||
let mut tt = Group::new()
|
||||
.with_id("tt")
|
||||
.with_transform(Transform::new().scale(0.0, 0.0));
|
||||
{
|
||||
let tip_straight_h = 5;
|
||||
let tip_curve_h = 2;
|
||||
let width = 80;
|
||||
let height = 50;
|
||||
let border_r = 5;
|
||||
let bottom_hline = width / 2 - tip_straight_h - 2 * tip_curve_h;
|
||||
let mut data = Data::new();
|
||||
data.move_to(0, 0)
|
||||
.line_by(tip_straight_h, -tip_straight_h)
|
||||
.quad_by(tip_curve_h, -tip_curve_h, 2 * tip_curve_h, -tip_curve_h)
|
||||
.horiz_line_by(bottom_hline)
|
||||
.arc_by(border_r, border_r, 0, false, false, border_r, -border_r)
|
||||
.vert_line_by(-height)
|
||||
.arc_by(border_r, border_r, 0, false, false, -border_r, -border_r)
|
||||
.horiz_line_by(-width)
|
||||
.arc_by(border_r, border_r, 0, false, false, -border_r, border_r)
|
||||
.vert_line_by(height)
|
||||
.arc_by(border_r, border_r, 0, false, false, border_r, border_r)
|
||||
.horiz_line_by(bottom_hline)
|
||||
.quad_by(tip_curve_h, 0, 2 * tip_curve_h, tip_curve_h)
|
||||
.close();
|
||||
tt.push(Path::new().with_d(data));
|
||||
tt.push(
|
||||
Text::new()
|
||||
.with_dominant_baseline("hanging")
|
||||
.with_transform(Transform::new().translate(
|
||||
width / -2,
|
||||
-height - tip_straight_h - tip_curve_h - border_r
|
||||
))
|
||||
.append(TSpan::new().with_id("ttp"))
|
||||
.append(TSpan::new().with_id("ttx").with_x(0).with_dy("1.2em"))
|
||||
.append(TSpan::new().with_id("tty").with_x(0).with_dy("1.2em"))
|
||||
);
|
||||
}
|
||||
svg.push(tt);
|
||||
|
||||
let svg = if self.pretty {
|
||||
svg.to_string_pretty()
|
||||
} else {
|
||||
svg.to_string()
|
||||
};
|
||||
writeln!(out, "{}", svg)
|
||||
}
|
||||
}
|
46
stdlib/src/output/list.rs
Normal file
46
stdlib/src/output/list.rs
Normal file
|
@ -0,0 +1,46 @@
|
|||
use super::{OutputList, OutputPrinter};
|
||||
use crate::int;
|
||||
use std::io::{self, Write};
|
||||
|
||||
/// Default output printer.
|
||||
pub struct ListPrinter;
|
||||
|
||||
impl OutputPrinter for ListPrinter {
|
||||
fn print<I, E, W>(self, list: OutputList<I>, mut out: W) -> io::Result<()>
|
||||
where
|
||||
I: IntoIterator<Item = (E, usize)>,
|
||||
E: IntoIterator<Item = (&'static str, int)>,
|
||||
W: Write
|
||||
{
|
||||
writeln!(out, "Output Variable Analysis:")?;
|
||||
for (key, stats) in list.stats() {
|
||||
writeln!(out, " {key}: {stats:?}")?;
|
||||
}
|
||||
|
||||
writeln!(out, "Samples:")?;
|
||||
let mut qty_buf = itoa::Buffer::new();
|
||||
let len_str = qty_buf.format(list.n);
|
||||
let qty_spacing = len_str.len();
|
||||
for (entry, qty) in list.samples {
|
||||
let qty_str = qty_buf.format(qty);
|
||||
let qty_str_len = qty_str.len();
|
||||
write!(out, " ")?;
|
||||
for _ in 0 .. qty_spacing - qty_str_len {
|
||||
write!(out, " ")?;
|
||||
}
|
||||
write!(
|
||||
out,
|
||||
"{qty_str} ({:>6.2}%): {{ ",
|
||||
qty as f32 / list.n as f32 * 100.0
|
||||
)?;
|
||||
for (i, (key, value)) in entry.into_iter().enumerate() {
|
||||
if i > 0 {
|
||||
write!(out, ", ")?;
|
||||
}
|
||||
write!(out, "{key}: {value}")?;
|
||||
}
|
||||
writeln!(out, " }}")?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
322
stdlib/src/output/mod.rs
Normal file
322
stdlib/src/output/mod.rs
Normal file
|
@ -0,0 +1,322 @@
|
|||
#![allow(clippy::new_without_default)]
|
||||
|
||||
use crate::int;
|
||||
use indexmap::IndexMap;
|
||||
use std::{
|
||||
error::Error,
|
||||
fmt::{self, Display, Formatter},
|
||||
fs::File,
|
||||
io::{self, stdout, Write},
|
||||
ops::{Deref, DerefMut},
|
||||
path::PathBuf,
|
||||
str::FromStr
|
||||
};
|
||||
use svgwriter::{tags::TagWithGlobalEventAttributes, Graphic, Value};
|
||||
|
||||
mod bar;
|
||||
mod bubble;
|
||||
mod list;
|
||||
|
||||
pub use bar::BarChartPrinter;
|
||||
pub use bubble::BubbleChartPrinter;
|
||||
pub use list::ListPrinter;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct NoSuchVariant(String);
|
||||
|
||||
impl Display for NoSuchVariant {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
|
||||
write!(f, "No such variant: {}", self.0)
|
||||
}
|
||||
}
|
||||
|
||||
impl Error for NoSuchVariant {}
|
||||
|
||||
/// Used for argument parsing
|
||||
#[derive(Clone, Copy, Debug, Default)]
|
||||
pub enum OutputPrinterAny {
|
||||
#[default]
|
||||
List,
|
||||
Bar
|
||||
}
|
||||
|
||||
impl FromStr for OutputPrinterAny {
|
||||
type Err = NoSuchVariant;
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
match s {
|
||||
"list" => Ok(Self::List),
|
||||
"bar" => Ok(Self::Bar),
|
||||
_ => Err(NoSuchVariant(s.to_owned()))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Used for argument parsing
|
||||
#[derive(Clone, Copy, Debug, Default)]
|
||||
pub enum OutputPrinterTwoVars {
|
||||
#[default]
|
||||
List,
|
||||
Bar,
|
||||
Bubble
|
||||
}
|
||||
|
||||
impl FromStr for OutputPrinterTwoVars {
|
||||
type Err = NoSuchVariant;
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
match s {
|
||||
"list" => Ok(Self::List),
|
||||
"bar" => Ok(Self::Bar),
|
||||
"bubble" => Ok(Self::Bubble),
|
||||
_ => Err(NoSuchVariant(s.to_owned()))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Used for output printing.
|
||||
pub trait OutputPrinter {
|
||||
fn print<I, E, W>(self, list: OutputList<I>, out: W) -> io::Result<()>
|
||||
where
|
||||
I: IntoIterator<Item = (E, usize)>,
|
||||
E: IntoIterator<Item = (&'static str, int)>,
|
||||
W: Write;
|
||||
}
|
||||
|
||||
/// Used for output printing.
|
||||
pub struct OutputList<I> {
|
||||
/// An iterator over all samples. Must yield pairs of (values, qty),
|
||||
/// where values is an iterator over (variable, value).
|
||||
pub samples: I,
|
||||
/// The total quantity of all samples.
|
||||
pub n: usize,
|
||||
/// The minimum of each output variable.
|
||||
min: IndexMap<&'static str, int>,
|
||||
/// The maximum of each output variable.
|
||||
max: IndexMap<&'static str, int>,
|
||||
/// The mean of each output variable.
|
||||
mean: IndexMap<&'static str, f64>,
|
||||
/// The variance of each output variable.
|
||||
variance: IndexMap<&'static str, f64>
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Stats {
|
||||
pub min: int,
|
||||
pub max: int,
|
||||
pub mean: f64,
|
||||
pub variance: f64,
|
||||
pub standard_deviation: f64
|
||||
}
|
||||
|
||||
impl<I, E> OutputList<I>
|
||||
where
|
||||
I: IntoIterator<Item = (E, usize)>,
|
||||
for<'a> &'a I: IntoIterator<Item = (&'a E, &'a usize)>,
|
||||
E: IntoIterator<Item = (&'static str, int)>,
|
||||
for<'a> &'a E: IntoIterator<Item = (&'static str, int)>
|
||||
{
|
||||
pub fn new(samples: I, n: usize) -> Self {
|
||||
let mut min = IndexMap::new();
|
||||
let mut max = IndexMap::new();
|
||||
let mut mean = IndexMap::new();
|
||||
let mut qty = 0;
|
||||
for (sample, sample_qty) in &samples {
|
||||
let new_qty = (qty + sample_qty) as f64;
|
||||
for (var, value) in sample {
|
||||
if min.get(var).map(|v| *v > value).unwrap_or(true) {
|
||||
min.insert(var, value);
|
||||
}
|
||||
if max.get(var).map(|v| *v < value).unwrap_or(true) {
|
||||
max.insert(var, value);
|
||||
}
|
||||
mean.insert(
|
||||
var,
|
||||
mean.get(var).copied().unwrap_or(0.0) * (qty as f64 / new_qty)
|
||||
+ value as f64 * (*sample_qty as f64 / new_qty)
|
||||
);
|
||||
}
|
||||
qty += sample_qty;
|
||||
}
|
||||
debug_assert_eq!(qty, n);
|
||||
|
||||
let mut variance = IndexMap::new();
|
||||
let mut qty;
|
||||
let mut samples_iter = (&samples).into_iter();
|
||||
let (first_sample, first_sample_qty) =
|
||||
samples_iter.next().expect("I need at least one sample");
|
||||
if *first_sample_qty > 1 {
|
||||
qty = *first_sample_qty;
|
||||
for (var, value) in first_sample {
|
||||
variance.insert(
|
||||
var,
|
||||
(value as f64 - mean[var]).powi(2) * (qty as f64 / (qty - 1) as f64)
|
||||
);
|
||||
}
|
||||
} else {
|
||||
let (second_sample, second_sample_qty) =
|
||||
samples_iter.next().expect("I need at least two samples");
|
||||
let first_sample: IndexMap<&'static str, int> =
|
||||
first_sample.into_iter().collect();
|
||||
qty = first_sample_qty + second_sample_qty;
|
||||
for (var, second_value) in second_sample {
|
||||
let first_value = first_sample[var];
|
||||
variance.insert(
|
||||
var,
|
||||
(first_value as f64 - mean[var]).powi(2)
|
||||
* (*first_sample_qty as f64 / (qty - 1) as f64)
|
||||
+ (second_value as f64 - mean[var]).powi(2)
|
||||
* (*second_sample_qty as f64 / (qty - 1) as f64)
|
||||
);
|
||||
}
|
||||
}
|
||||
for (sample, sample_qty) in samples_iter {
|
||||
let new_qty = (qty + sample_qty) as f64;
|
||||
for (var, value) in sample {
|
||||
variance.insert(
|
||||
var,
|
||||
variance[var] * ((qty - 1) as f64 / (new_qty - 1.0))
|
||||
+ (value as f64 - mean[var]).powi(2)
|
||||
* (*sample_qty as f64 / (new_qty - 1.0))
|
||||
);
|
||||
}
|
||||
qty += sample_qty;
|
||||
}
|
||||
debug_assert_eq!(qty, n);
|
||||
|
||||
Self {
|
||||
samples,
|
||||
n,
|
||||
min,
|
||||
max,
|
||||
mean,
|
||||
variance
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<I> OutputList<I> {
|
||||
pub fn min(&self, var: &'static str) -> int {
|
||||
self.min.get(var).copied().unwrap_or(0)
|
||||
}
|
||||
|
||||
pub fn max(&self, var: &'static str) -> int {
|
||||
self.max.get(var).copied().unwrap_or(0)
|
||||
}
|
||||
|
||||
pub fn mean(&self, var: &'static str) -> f64 {
|
||||
self.mean.get(var).copied().unwrap_or(0.0)
|
||||
}
|
||||
|
||||
pub fn variance(&self, var: &'static str) -> f64 {
|
||||
self.variance.get(var).copied().unwrap_or(f64::INFINITY)
|
||||
}
|
||||
|
||||
pub fn stats(&self) -> impl Iterator<Item = (&'static str, Stats)> + '_ {
|
||||
self.mean.iter().map(|(key, mean)| {
|
||||
let variance = self.variance(key);
|
||||
(*key, Stats {
|
||||
min: self.min(key),
|
||||
max: self.max(key),
|
||||
mean: *mean,
|
||||
variance,
|
||||
standard_deviation: variance.sqrt()
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// Used for output printing.
|
||||
pub trait OutputTarget {
|
||||
fn print<I, E>(self, list: OutputList<I>)
|
||||
where
|
||||
I: IntoIterator<Item = (E, usize)>,
|
||||
E: IntoIterator<Item = (&'static str, int)>;
|
||||
}
|
||||
|
||||
fn print_with<I, E, P>(list: OutputList<I>, printer: P, path: Option<PathBuf>)
|
||||
where
|
||||
I: IntoIterator<Item = (E, usize)>,
|
||||
E: IntoIterator<Item = (&'static str, int)>,
|
||||
P: OutputPrinter
|
||||
{
|
||||
match path {
|
||||
Some(path) => {
|
||||
let file = File::create(&path).expect("Failed to create output file");
|
||||
printer.print(list, file).expect("Failed to print output");
|
||||
eprintln!("Wrote output to {}", path.display());
|
||||
},
|
||||
None => {
|
||||
printer
|
||||
.print(list, stdout())
|
||||
.expect("Failed to print output");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl OutputTarget for (OutputPrinterAny, Option<PathBuf>) {
|
||||
fn print<I, E>(self, list: OutputList<I>)
|
||||
where
|
||||
I: IntoIterator<Item = (E, usize)>,
|
||||
E: IntoIterator<Item = (&'static str, int)>
|
||||
{
|
||||
match self.0 {
|
||||
OutputPrinterAny::List => print_with(list, ListPrinter, self.1),
|
||||
OutputPrinterAny::Bar => {
|
||||
print_with(list, BarChartPrinter::new(self.1.is_none()), self.1)
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl OutputTarget for (OutputPrinterTwoVars, Option<PathBuf>) {
|
||||
fn print<I, E>(self, list: OutputList<I>)
|
||||
where
|
||||
I: IntoIterator<Item = (E, usize)>,
|
||||
E: IntoIterator<Item = (&'static str, int)>
|
||||
{
|
||||
match self.0 {
|
||||
OutputPrinterTwoVars::List => print_with(list, ListPrinter, self.1),
|
||||
OutputPrinterTwoVars::Bar => {
|
||||
print_with(list, BarChartPrinter::new(self.1.is_none()), self.1)
|
||||
},
|
||||
OutputPrinterTwoVars::Bubble => {
|
||||
print_with(list, BubbleChartPrinter::new(self.1.is_none()), self.1)
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Helper for creating svgs
|
||||
struct Svg(Graphic);
|
||||
|
||||
impl Deref for Svg {
|
||||
type Target = Graphic;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl DerefMut for Svg {
|
||||
fn deref_mut(&mut self) -> &mut Self::Target {
|
||||
&mut self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl Svg {
|
||||
fn new(width: i32, height: i32) -> Self {
|
||||
let mut svg = Graphic::new();
|
||||
svg.set_view_box(format!("0 0 {width} {height}"));
|
||||
svg.set_height("20cm");
|
||||
Self(svg)
|
||||
}
|
||||
|
||||
fn with_onload<V>(mut self, onload: V) -> Self
|
||||
where
|
||||
V: Value + 'static
|
||||
{
|
||||
self.0.set_onload(onload);
|
||||
self
|
||||
}
|
||||
}
|
Reference in a new issue