You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
256 lines
7.7 KiB
256 lines
7.7 KiB
use std::fmt; |
|
use std::ops::Range; |
|
|
|
use yansi::Color::Blue; |
|
|
|
use crate::format::{F64Formatter, HorizontalScale}; |
|
use crate::stats::Stats; |
|
|
|
#[derive(Debug)] |
|
/// A struct that represents a bucket of an histogram. |
|
struct Bucket { |
|
range: Range<f64>, |
|
count: usize, |
|
} |
|
|
|
impl Bucket { |
|
fn new(range: Range<f64>) -> Bucket { |
|
Bucket { range, count: 0 } |
|
} |
|
|
|
fn inc(&mut self) { |
|
self.count += 1; |
|
} |
|
} |
|
|
|
#[derive(Debug)] |
|
/// A struct holding data to plot a Histogram of numerical data. |
|
pub struct Histogram { |
|
vec: Vec<Bucket>, |
|
max: f64, |
|
step: f64, |
|
top: usize, |
|
last: usize, |
|
stats: Stats, |
|
precision: Option<usize>, // If None, then human friendly display will be used |
|
} |
|
|
|
impl Histogram { |
|
/// Creates a Histogram from a vector of numerical data. |
|
/// |
|
/// `intervals` is the number of histogram buckets to display (capped to the |
|
/// length of input data). |
|
/// |
|
/// `precision` is an Option with the number of decimals to display. If |
|
/// "None" is used, human units will be used, with an heuristic based on the |
|
/// input data for deciding the units and the decimal places. |
|
pub fn new(vec: &[f64], intervals: usize, precision: Option<usize>) -> Histogram { |
|
let stats = Stats::new(vec, precision); |
|
let size = intervals.min(vec.len()); |
|
let step = (stats.max - stats.min) / size as f64; |
|
let mut histogram = Histogram::new_with_stats(size, step, stats, precision); |
|
histogram.load(vec); |
|
histogram |
|
} |
|
|
|
/// Creates a Histogram with no input data. |
|
/// |
|
/// |
|
/// Parameters are similar to those on the `new` method, but a parameter |
|
/// named `stats` is needed to decide how future data (to be injected with |
|
/// the load method) will be accommodated. |
|
pub fn new_with_stats( |
|
size: usize, |
|
step: f64, |
|
stats: Stats, |
|
precision: Option<usize>, |
|
) -> Histogram { |
|
let mut vec = Vec::<Bucket>::with_capacity(size); |
|
let mut lower = stats.min; |
|
for _ in 0..size { |
|
vec.push(Bucket::new(lower..lower + step)); |
|
lower += step; |
|
} |
|
Histogram { |
|
vec, |
|
max: stats.min + (step * size as f64), |
|
step, |
|
top: 0, |
|
last: size - 1, |
|
stats, |
|
precision, |
|
} |
|
} |
|
|
|
/// Add to the `Histogram` data the values of a slice of numerical data. |
|
pub fn load(&mut self, vec: &[f64]) { |
|
for x in vec { |
|
self.add(*x); |
|
} |
|
} |
|
|
|
/// Add to the `Histogram` a single piece of numerical data. |
|
pub fn add(&mut self, n: f64) { |
|
if let Some(slot) = self.find_slot(n) { |
|
self.vec[slot].inc(); |
|
self.top = self.top.max(self.vec[slot].count); |
|
} |
|
} |
|
|
|
fn find_slot(&self, n: f64) -> Option<usize> { |
|
if n < self.stats.min || n > self.max { |
|
None |
|
} else { |
|
Some((((n - self.stats.min) / self.step) as usize).min(self.last)) |
|
} |
|
} |
|
} |
|
|
|
impl fmt::Display for Histogram { |
|
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { |
|
write!(f, "{}", self.stats)?; |
|
let formatter = match self.precision { |
|
None => F64Formatter::new_with_range(self.stats.min..self.stats.max), |
|
Some(n) => F64Formatter::new(n), |
|
}; |
|
let writer = HistWriter { |
|
width: f.width().unwrap_or(110), |
|
formatter, |
|
}; |
|
writer.write(f, self) |
|
} |
|
} |
|
|
|
struct HistWriter { |
|
width: usize, |
|
formatter: F64Formatter, |
|
} |
|
|
|
impl HistWriter { |
|
pub fn write(&self, f: &mut fmt::Formatter, hist: &Histogram) -> fmt::Result { |
|
let width_range = self.get_width(hist); |
|
let width_count = ((hist.top as f64).log10().ceil() as usize).max(1); |
|
let horizontal_scale = |
|
HorizontalScale::new(hist.top / self.get_max_bar_len(width_range + width_count)); |
|
writeln!(f, "{}", horizontal_scale)?; |
|
for x in hist.vec.iter() { |
|
self.write_bucket(f, x, &horizontal_scale, width_range, width_count)?; |
|
} |
|
Ok(()) |
|
} |
|
|
|
fn write_bucket( |
|
&self, |
|
f: &mut fmt::Formatter, |
|
bucket: &Bucket, |
|
horizontal_scale: &HorizontalScale, |
|
width: usize, |
|
width_count: usize, |
|
) -> fmt::Result { |
|
writeln!( |
|
f, |
|
"[{range}] [{count}] {bar}", |
|
range = Blue.paint(format!( |
|
"{:>width$} .. {:>width$}", |
|
self.formatter.format(bucket.range.start), |
|
self.formatter.format(bucket.range.end), |
|
width = width, |
|
)), |
|
count = horizontal_scale.get_count(bucket.count, width_count), |
|
bar = horizontal_scale.get_bar(bucket.count) |
|
) |
|
} |
|
|
|
fn get_width(&self, hist: &Histogram) -> usize { |
|
self.formatter |
|
.format(hist.stats.min) |
|
.len() |
|
.max(self.formatter.format(hist.max).len()) |
|
} |
|
|
|
fn get_max_bar_len(&self, fixed_width: usize) -> usize { |
|
const EXTRA_CHARS: usize = 10; |
|
if self.width < fixed_width + EXTRA_CHARS { |
|
75 |
|
} else { |
|
self.width - fixed_width - EXTRA_CHARS |
|
} |
|
} |
|
} |
|
|
|
#[cfg(test)] |
|
mod tests { |
|
use super::*; |
|
use yansi::Paint; |
|
|
|
#[test] |
|
fn test_buckets() { |
|
let stats = Stats::new(&[-2.0, 14.0], None); |
|
let mut hist = Histogram::new_with_stats(8, 2.5, stats, None); |
|
hist.load(&[ |
|
-1.0, -1.1, 2.0, 2.0, 2.1, -0.9, 11.0, 11.2, 1.9, 1.99, 1.98, 1.97, 1.96, |
|
]); |
|
|
|
assert_eq!(hist.top, 8); |
|
let bucket = &hist.vec[0]; |
|
assert_eq!(bucket.range, -2.0..0.5); |
|
assert_eq!(bucket.count, 3); |
|
let bucket = &hist.vec[1]; |
|
assert_eq!(bucket.count, 8); |
|
assert_eq!(bucket.range, 0.5..3.0); |
|
} |
|
|
|
#[test] |
|
fn test_buckets_bad_stats() { |
|
let mut hist = Histogram::new_with_stats(6, 1.0, Stats::new(&[-2.0, 4.0], None), None); |
|
hist.load(&[-1.0, 2.0, -1.0, 2.0, 10.0, 10.0, 10.0, -10.0]); |
|
assert_eq!(hist.top, 2); |
|
} |
|
|
|
#[test] |
|
fn display_test() { |
|
let stats = Stats::new(&[-2.0, 14.0], None); |
|
let mut hist = Histogram::new_with_stats(8, 2.5, stats, Some(3)); |
|
hist.load(&[ |
|
-1.0, -1.1, 2.0, 2.0, 2.1, -0.9, 11.0, 11.2, 1.9, 1.99, 1.98, 1.97, 1.96, |
|
]); |
|
Paint::disable(); |
|
let display = format!("{}", hist); |
|
assert!(display.contains("[-2.000 .. 0.500] [3] ∎∎∎\n")); |
|
assert!(display.contains("[ 0.500 .. 3.000] [8] ∎∎∎∎∎∎∎∎\n")); |
|
assert!(display.contains("[10.500 .. 13.000] [2] ∎∎\n")); |
|
} |
|
|
|
#[test] |
|
fn display_test_bad_width() { |
|
let mut hist = Histogram::new_with_stats(8, 2.5, Stats::new(&[-2.0, 14.0], None), Some(3)); |
|
hist.load(&[ |
|
-1.0, -1.1, 2.0, 2.0, 2.1, -0.9, 11.0, 11.2, 1.9, 1.99, 1.98, 1.97, 1.96, |
|
]); |
|
Paint::disable(); |
|
let display = format!("{:2}", hist); |
|
assert!(display.contains("[-2.000 .. 0.500] [3] ∎∎∎\n")); |
|
} |
|
|
|
#[test] |
|
fn display_test_human_units() { |
|
let vector = &[ |
|
-1.0, |
|
-12000000.0, |
|
-12000001.0, |
|
-12000002.0, |
|
-12000003.0, |
|
-2000000.0, |
|
500000.0, |
|
500000.0, |
|
]; |
|
let hist = Histogram::new(vector, vector.len(), None); |
|
Paint::disable(); |
|
let display = format!("{}", hist); |
|
assert!(display.contains("[-12.0 M .. -10.4 M] [4] ∎∎∎∎\n")); |
|
assert!(display.contains("[ -2.6 M .. -1.1 M] [1] ∎\n")); |
|
assert!(display.contains("[ -1.1 M .. 0.5 M] [3] ∎∎∎\n")); |
|
assert!(display.contains("Samples = 8; Min = -12.0 M; Max = 0.5 M")); |
|
assert!(display.contains("Average = -6.1 M;")); |
|
} |
|
}
|
|
|