Browse Source

Add log scale option for histogram

log-scale
JuanLeon Lahoz 3 years ago
parent
commit
d79b98e4d2
  1. 7
      CHANGELOG.md
  2. 10
      README.md
  3. 11
      src/app.rs
  4. 1
      src/main.rs
  5. 251
      src/plot/histogram.rs

7
CHANGELOG.md

@ -1,3 +1,10 @@
0.5.8
=====
Features:
* Support logarithmic scale in histograms via `--log-scale` flag.
0.5.7 0.5.7
===== =====

10
README.md

@ -86,6 +86,8 @@ each ∎ represents a count of 228
[0.044 .. 0.049] [ 183] [0.044 .. 0.049] [ 183]
``` ```
Command supports a `--log-scale` flag to use a logarithmic scale.
#### Time Histogram #### Time Histogram
This chart is generated using `strace -tt ls -lR * 2>&1 | lowcharts timehist --intervals 10`: This chart is generated using `strace -tt ls -lR * 2>&1 | lowcharts timehist --intervals 10`:
@ -191,11 +193,13 @@ lowcharts = "*"
Example: Example:
```rust ```rust
// use lowcharts::plot; use lowcharts::plot;
let vec = &[-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];
let vec = &[-1.0, -1.1, 2.0, 2.0, 2.1, -0.9, 11.0, 11.2, 1.9, 1.99];
// Plot a histogram of the above vector, with 4 buckets and a precision // Plot a histogram of the above vector, with 4 buckets and a precision
// choosen by library // choosen by library
let histogram = plot::Histogram::new(vec, 4, None); let options = plot::HistogramOptions { intervals: 4, ..Default::default() };
let histogram = plot::Histogram::new(vec, options);
print!("{}", histogram); print!("{}", histogram);
``` ```

11
src/app.rs

@ -105,12 +105,21 @@ fn add_precision(cmd: Command) -> Command {
) )
} }
fn add_log_scale(cmd: Command) -> Command {
cmd.arg(
Arg::new("log-scale")
.long("log-scale")
.help("Use a logarithmic scale in buckets")
.takes_value(false),
)
}
pub fn get_app() -> Command<'static> { pub fn get_app() -> Command<'static> {
let mut hist = Command::new("hist") let mut hist = Command::new("hist")
.version(clap::crate_version!()) .version(clap::crate_version!())
.about("Plot an histogram from input values"); .about("Plot an histogram from input values");
hist = add_input(add_regex(add_width(add_min_max(add_precision( hist = add_input(add_regex(add_width(add_min_max(add_precision(
add_intervals(hist), add_intervals(add_log_scale(hist)),
))))); )))));
let mut plot = Command::new("plot") let mut plot = Command::new("plot")

1
src/main.rs

@ -114,6 +114,7 @@ fn histogram(matches: &ArgMatches) -> i32 {
if precision_arg > 0 { if precision_arg > 0 {
options.precision = Some(precision_arg as usize); options.precision = Some(precision_arg as usize);
}; };
options.log_scale = matches.is_present("log-scale");
options.intervals = matches.value_of_t("intervals").unwrap(); options.intervals = matches.value_of_t("intervals").unwrap();
let width = matches.value_of_t("width").unwrap(); let width = matches.value_of_t("width").unwrap();
let histogram = plot::Histogram::new(&vec, options); let histogram = plot::Histogram::new(&vec, options);

251
src/plot/histogram.rs

@ -26,73 +26,63 @@ impl Bucket {
/// A struct representing the options to build an histogram. /// A struct representing the options to build an histogram.
pub struct Histogram { pub struct Histogram {
vec: Vec<Bucket>, vec: Vec<Bucket>,
max: f64,
step: f64, step: f64,
// Maximum of all bucket counts
top: usize, top: usize,
last: usize, last: usize,
stats: Stats, stats: Stats,
log_scale: bool,
precision: Option<usize>, // If None, then human friendly display will be used precision: Option<usize>, // If None, then human friendly display will be used
} }
/// A struct holding data to plot a Histogram of numerical data. /// A struct holding data to plot a Histogram of numerical data.
#[derive(Default)]
pub struct HistogramOptions { pub struct HistogramOptions {
/// Maximum number of buckets to use /// `intervals` is the number of histogram buckets to display (capped to the
/// length of input data).
pub intervals: usize, pub intervals: usize,
/// If true, logarithmic scale will be used for buckets /// If true, logarithmic scale will be used for buckets
pub log_scale: bool, pub log_scale: bool,
/// If None, then human friendly display will be used /// `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 precision: Option<usize>, pub precision: Option<usize>,
} }
impl Default for HistogramOptions {
fn default() -> Self {
Self {
intervals: 10,
log_scale: false,
precision: None,
}
}
}
impl Histogram { impl Histogram {
/// Creates a Histogram from a vector of numerical data. /// Creates a Histogram from a vector of numerical data.
/// ///
/// `intervals` is the number of histogram buckets to display (capped to the /// `options` is a `HistogramOptions` struct with the preferences to create
/// length of input data). /// histogram.
///
/// `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], mut options: HistogramOptions) -> Self { pub fn new(vec: &[f64], mut options: HistogramOptions) -> Self {
options.intervals = options.intervals.min(vec.len()); let mut stats = Stats::new(vec, options.precision);
let stats = Stats::new(vec, options.precision); if options.log_scale {
let size = options.intervals.min(vec.len()); stats.min = 0.0; // We will silently discard negative values
let step = (stats.max - stats.min) / size as f64; }
let mut histogram = Self::new_with_stats(step, stats, &options); options.intervals = options.intervals.clamp(1, vec.len());
let mut histogram = Self::new_with_stats(stats, &options);
histogram.load(vec); histogram.load(vec);
histogram histogram
} }
/// Creates a Histogram with no input data. /// Creates a Histogram with no input data.
/// ///
///
/// Parameters are similar to those on the `new` method, but a parameter /// 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 /// named `stats` is needed to decide how future data (to be injected with
/// the load method) will be accommodated. /// the load method) will be accommodated.
pub fn new_with_stats(step: f64, stats: Stats, options: &HistogramOptions) -> Self { pub fn new_with_stats(stats: Stats, options: &HistogramOptions) -> Self {
let mut vec = Vec::<Bucket>::with_capacity(options.intervals); let step = if options.log_scale {
let mut lower = stats.min; f64::NAN
for _ in 0..options.intervals { } else {
vec.push(Bucket::new(lower..lower + step)); (stats.max - stats.min) / options.intervals as f64
lower += step; };
}
Self { Self {
vec, vec: Self::build_buckets(stats.min..stats.max, options),
max: step.mul_add(options.intervals as f64, stats.min),
step, step,
top: 0, top: 0,
last: options.intervals - 1, last: options.intervals - 1,
stats, stats,
log_scale: options.log_scale,
precision: options.precision, precision: options.precision,
} }
} }
@ -113,12 +103,43 @@ impl Histogram {
} }
fn find_slot(&self, n: f64) -> Option<usize> { fn find_slot(&self, n: f64) -> Option<usize> {
if n < self.stats.min || n > self.max { if n < self.stats.min || n > self.stats.max {
None return None;
}
if self.log_scale {
let mut bucket = None;
for i in 0..self.vec.len() {
if self.vec[i].range.end >= n {
bucket = Some(i);
break;
}
}
bucket
} else { } else {
Some((((n - self.stats.min) / self.step) as usize).min(self.last)) Some((((n - self.stats.min) / self.step) as usize).min(self.last))
} }
} }
fn build_buckets(range: Range<f64>, options: &HistogramOptions) -> Vec<Bucket> {
let mut vec = Vec::<Bucket>::with_capacity(options.intervals);
if options.log_scale {
let first_bucket_size = range.end / (2_f64.powi(options.intervals as i32) - 1.0);
let mut lower = 0.0;
for i in 0..options.intervals {
let upper = lower + 2_f64.powi(i as i32) * first_bucket_size;
vec.push(Bucket::new(lower..upper));
lower = upper;
}
} else {
let step = (range.end - range.start) / options.intervals as f64;
let mut lower = range.start;
for _ in 0..options.intervals {
vec.push(Bucket::new(lower..lower + step));
lower += step;
}
}
vec
}
} }
impl fmt::Display for Histogram { impl fmt::Display for Histogram {
@ -180,7 +201,7 @@ impl HistWriter {
self.formatter self.formatter
.format(hist.stats.min) .format(hist.stats.min)
.len() .len()
.max(self.formatter.format(hist.max).len()) .max(self.formatter.format(hist.stats.max).len())
} }
fn get_max_bar_len(&self, fixed_width: usize) -> usize { fn get_max_bar_len(&self, fixed_width: usize) -> usize {
@ -196,6 +217,7 @@ impl HistWriter {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
use float_eq::assert_float_eq;
use yansi::Paint; use yansi::Paint;
#[test] #[test]
@ -205,18 +227,18 @@ mod tests {
intervals: 8, intervals: 8,
..Default::default() ..Default::default()
}; };
let mut hist = Histogram::new_with_stats(2.5, stats, &options); let mut hist = Histogram::new_with_stats(stats, &options);
hist.load(&[ 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, -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); assert_eq!(hist.top, 5);
let bucket = &hist.vec[0]; let bucket = &hist.vec[0];
assert_eq!(bucket.range, -2.0..0.5); assert_eq!(bucket.range, -2.0..0.0);
assert_eq!(bucket.count, 3); assert_eq!(bucket.count, 3);
let bucket = &hist.vec[1]; let bucket = &hist.vec[1];
assert_eq!(bucket.count, 8); assert_eq!(bucket.count, 5);
assert_eq!(bucket.range, 0.5..3.0); assert_eq!(bucket.range, 0.0..2.0);
} }
#[test] #[test]
@ -225,7 +247,7 @@ mod tests {
intervals: 6, intervals: 6,
..Default::default() ..Default::default()
}; };
let mut hist = Histogram::new_with_stats(1.0, Stats::new(&[-2.0, 4.0], None), &options); let mut hist = Histogram::new_with_stats(Stats::new(&[-2.0, 4.0], None), &options);
hist.load(&[-1.0, 2.0, -1.0, 2.0, 10.0, 10.0, 10.0, -10.0]); hist.load(&[-1.0, 2.0, -1.0, 2.0, 10.0, 10.0, 10.0, -10.0]);
assert_eq!(hist.top, 2); assert_eq!(hist.top, 2);
} }
@ -236,17 +258,19 @@ mod tests {
let options = HistogramOptions { let options = HistogramOptions {
intervals: 8, intervals: 8,
precision: Some(3), precision: Some(3),
log_scale: false, ..Default::default()
}; };
let mut hist = Histogram::new_with_stats(2.5, stats, &options); let mut hist = Histogram::new_with_stats(stats, &options);
hist.load(&[ 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, -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(); Paint::disable();
let display = format!("{hist}"); let display = format!("{hist}");
assert!(display.contains("[-2.000 .. 0.500] [3] ∎∎∎\n")); assert!(display.contains("[-2.000 .. 0.000] [3] ∎∎∎\n"));
assert!(display.contains("[ 0.500 .. 3.000] [8] ∎∎∎∎∎∎∎∎\n")); assert!(display.contains("[ 0.000 .. 2.000] [5] ∎∎∎∎∎\n"));
assert!(display.contains("[10.500 .. 13.000] [2] ∎∎\n")); assert!(display.contains("[ 2.000 .. 4.000] [3] ∎∎∎\n"));
assert!(display.contains("[ 6.000 .. 8.000] [0] \n"));
assert!(display.contains("[10.000 .. 12.000] [2] ∎∎\n"));
} }
#[test] #[test]
@ -254,15 +278,15 @@ mod tests {
let options = HistogramOptions { let options = HistogramOptions {
intervals: 8, intervals: 8,
precision: Some(3), precision: Some(3),
log_scale: false, ..Default::default()
}; };
let mut hist = Histogram::new_with_stats(2.5, Stats::new(&[-2.0, 14.0], None), &options); let mut hist = Histogram::new_with_stats(Stats::new(&[-2.0, 14.0], None), &options);
hist.load(&[ 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, -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(); Paint::disable();
let display = format!("{hist:2}"); let display = format!("{hist:2}");
assert!(display.contains("[-2.000 .. 0.500] [3] ∎∎∎\n")); assert!(display.contains("[-2.000 .. 0.000] [3] ∎∎∎\n"));
} }
#[test] #[test]
@ -277,7 +301,13 @@ mod tests {
500000.0, 500000.0,
500000.0, 500000.0,
]; ];
let hist = Histogram::new(vector, HistogramOptions::default()); let hist = Histogram::new(
vector,
HistogramOptions {
intervals: 10,
..Default::default()
},
);
Paint::disable(); Paint::disable();
let display = format!("{hist}"); let display = format!("{hist}");
assert!(display.contains("[-12.0 M .. -10.4 M] [4] ∎∎∎∎\n")); assert!(display.contains("[-12.0 M .. -10.4 M] [4] ∎∎∎∎\n"));
@ -286,4 +316,121 @@ mod tests {
assert!(display.contains("Samples = 8; Min = -12.0 M; Max = 0.5 M")); assert!(display.contains("Samples = 8; Min = -12.0 M; Max = 0.5 M"));
assert!(display.contains("Average = -6.1 M;")); assert!(display.contains("Average = -6.1 M;"));
} }
#[test]
fn display_test_log_scale() {
let hist = Histogram::new(
&[0.4, 0.4, 0.4, 0.4, 255.0, 0.2, 1.2, 128.0, 126.0, -7.0],
HistogramOptions {
intervals: 8,
log_scale: true,
..Default::default()
},
);
Paint::disable();
let display = format!("{hist}");
assert!(display.contains("[ 0.00 .. 1.00] [5] ∎∎∎∎∎\n"));
assert!(display.contains("[ 1.00 .. 3.00] [1] ∎\n"));
assert!(display.contains("[ 3.00 .. 7.00] [0]"));
assert!(display.contains("[ 7.00 .. 15.00] [0]"));
assert!(display.contains("[ 15.00 .. 31.00] [0]"));
assert!(display.contains("[ 31.00 .. 63.00] [0]"));
assert!(display.contains("[ 63.00 .. 127.00] [1] ∎\n"));
assert!(display.contains("[127.00 .. 255.00] [2] ∎∎\n"));
}
#[test]
fn build_buckets_log_scale() {
let options = HistogramOptions {
intervals: 8,
log_scale: true,
..Default::default()
};
let buckets = Histogram::build_buckets(0.0..2.0_f64.powi(8) - 1.0, &options);
assert!(buckets.len() == 8);
assert!(buckets[0].range == (0.0..1.0));
assert!(buckets[1].range == (1.0..3.0));
assert!(buckets[2].range == (3.0..7.0));
assert!(buckets[3].range == (7.0..15.0));
assert!(buckets[4].range == (15.0..31.0));
assert!(buckets[5].range == (31.0..63.0));
assert!(buckets[6].range == (63.0..127.0));
assert!(buckets[7].range == (127.0..255.0));
}
#[test]
fn build_buckets_log_scale_with_math() {
let options = HistogramOptions {
intervals: 10,
log_scale: true,
..Default::default()
};
let buckets = Histogram::build_buckets(0.0..10000.0, &options);
assert!(buckets.len() == 10);
for i in 0..9 {
assert_float_eq!(
2.0 * (buckets[i].range.end - buckets[i].range.start),
buckets[i + 1].range.end - buckets[i + 1].range.start,
rmax <= 2.0 * f64::EPSILON
);
}
assert_float_eq!(
buckets[9].range.end - buckets[0].range.start,
10000.0,
rmax <= 2.0 * f64::EPSILON
);
}
#[test]
fn build_buckets_no_log_scale() {
let options = HistogramOptions {
intervals: 7,
..Default::default()
};
let buckets = Histogram::build_buckets(0.0..700.0, &options);
assert!(buckets.len() == 7);
for i in 0..6 {
let min = (i * 100) as f64;
let max = ((i + 1) * 100) as f64;
assert!(buckets[i].range == (min..max));
}
}
#[test]
fn find_slot_linear() {
let options = HistogramOptions {
intervals: 8,
..Default::default()
};
let hist = Histogram::new_with_stats(Stats::new(&[-12.0, 4.0], None), &options);
assert!(hist.find_slot(-13.0) == None);
assert!(hist.find_slot(13.0) == None);
assert!(hist.find_slot(-12.0) == Some(0));
assert!(hist.find_slot(-11.0) == Some(0));
assert!(hist.find_slot(-9.0) == Some(1));
assert!(hist.find_slot(4.0) == Some(7));
assert!(hist.find_slot(1.1) == Some(6));
}
#[test]
fn find_slot_logarithmic() {
let hist = Histogram::new(
// More than 8 values to avoid interval truncation
&[255.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, -2000.0],
HistogramOptions {
intervals: 8,
log_scale: true,
..Default::default()
},
);
assert!(hist.find_slot(-1.0) == None);
assert!(hist.find_slot(0.0) == Some(0));
assert!(hist.find_slot(0.5) == Some(0));
assert!(hist.find_slot(1.5) == Some(1));
assert!(hist.find_slot(8.75) == Some(3));
assert!(hist.find_slot(33.1) == Some(5));
assert!(hist.find_slot(127.1) == Some(7));
assert!(hist.find_slot(247.1) == Some(7));
assert!(hist.find_slot(1000.0) == None);
}
} }

Loading…
Cancel
Save