Browse Source

Allow using lowcharts as a library

pull/2/head
JuanLeon Lahoz 4 years ago
parent
commit
2cbdba8497
  1. 9
      Cargo.toml
  2. 3
      Makefile
  3. 28
      README.md
  4. 3
      src/lib.rs
  5. 21
      src/main.rs
  6. 52
      src/plot/histogram.rs
  7. 5
      src/plot/matchbar.rs
  8. 20
      src/plot/splittimehist.rs
  9. 9
      src/plot/terms.rs
  10. 25
      src/plot/timehist.rs
  11. 41
      src/plot/xy.rs
  12. 18
      src/stats/mod.rs

9
Cargo.toml

@ -12,6 +12,15 @@ keywords = ["regex", "grep", "data", "troubleshooting", "graph", "text", "consol
categories = ["command-line-utilities", "text-processing"]
license = "MIT"
[lib]
name = "lowcharts"
path = "src/lib.rs"
[[bin]]
name = "lowcharts"
path = "src/main.rs"
[dependencies]
clap = { version = "^3", features = ["cargo"] }
yansi = "^0"

3
Makefile

@ -13,3 +13,6 @@ test:
# assert_cmd, as it does not follow forks
coverage:
cargo tarpaulin -o Html --ignore-tests -- --test-threads 1
doc:
cargo doc -p lowcharts

28
README.md

@ -149,6 +149,34 @@ of a metric over time, but not the speed of that evolution.
There is regex support for this type of plots.
### Using it as a library
`lowcharts` can be used as a library by any code that needs to display text
based charts.
```toml
[dependencies]
lowcharts = "*"
```
Example:
```rust
// 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];
// Plot a histogram of the above vector, with 4 buckets and a precision
// choosen by library
let histogram = plot::Histogram::new(vec, 4, None);
print!("{}", histogram);
```
You can disable coloring by doing:
```rust
// use yansi::Paint;
Paint::disable();
```
### Installing
#### Via release

3
src/lib.rs

@ -0,0 +1,3 @@
mod format;
pub mod plot;
pub mod stats;

21
src/main.rs

@ -115,18 +115,9 @@ fn histogram(matches: &ArgMatches) -> i32 {
} else {
Some(precision_arg as usize)
};
let stats = stats::Stats::new(&vec, precision);
let width = matches.value_of_t("width").unwrap();
let mut intervals: usize = matches.value_of_t("intervals").unwrap();
intervals = intervals.min(vec.len());
let mut histogram = plot::Histogram::new(
intervals,
(stats.max - stats.min) / intervals as f64,
stats,
precision,
);
histogram.load(&vec);
let intervals: usize = matches.value_of_t("intervals").unwrap();
let histogram = plot::Histogram::new(&vec, intervals, precision);
print!("{:width$}", histogram, width = width);
0
}
@ -147,13 +138,12 @@ fn plot(matches: &ArgMatches) -> i32 {
} else {
Some(precision_arg as usize)
};
let mut plot = plot::XyPlot::new(
let plot = plot::XyPlot::new(
&vec,
matches.value_of_t("width").unwrap(),
matches.value_of_t("height").unwrap(),
stats::Stats::new(&vec, precision),
precision,
);
plot.load(&vec);
print!("{}", plot);
0
}
@ -235,8 +225,7 @@ fn timehist(matches: &ArgMatches) -> i32 {
let reader = builder.build().unwrap();
let vec = reader.read(matches.value_of("input").unwrap());
if assert_data(&vec, 2) {
let mut timehist = plot::TimeHistogram::new(matches.value_of_t("intervals").unwrap(), &vec);
timehist.load(&vec);
let timehist = plot::TimeHistogram::new(matches.value_of_t("intervals").unwrap(), &vec);
print!("{:width$}", timehist, width = width);
};
0

52
src/plot/histogram.rs

@ -7,6 +7,7 @@ use crate::format::F64Formatter;
use crate::stats::Stats;
#[derive(Debug)]
/// A struct that represents a bucket of an histogram.
struct Bucket {
range: Range<f64>,
count: usize,
@ -23,6 +24,7 @@ impl Bucket {
}
#[derive(Debug)]
/// A struct holding data to plot a Histogram of numerical data.
pub struct Histogram {
vec: Vec<Bucket>,
max: f64,
@ -34,7 +36,35 @@ pub struct Histogram {
}
impl Histogram {
pub fn new(size: usize, step: f64, stats: Stats, precision: Option<usize>) -> 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 {
@ -52,12 +82,14 @@ impl Histogram {
}
}
/// 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();
@ -158,7 +190,7 @@ mod tests {
#[test]
fn test_buckets() {
let stats = Stats::new(&[-2.0, 14.0], None);
let mut hist = Histogram::new(8, 2.5, stats, 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,
]);
@ -174,7 +206,7 @@ mod tests {
#[test]
fn test_buckets_bad_stats() {
let mut hist = Histogram::new(6, 1.0, Stats::new(&[-2.0, 4.0], None), None);
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);
}
@ -182,7 +214,7 @@ mod tests {
#[test]
fn display_test() {
let stats = Stats::new(&[-2.0, 14.0], None);
let mut hist = Histogram::new(8, 2.5, stats, Some(3));
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,
]);
@ -195,7 +227,7 @@ mod tests {
#[test]
fn display_test_bad_width() {
let mut hist = Histogram::new(8, 2.5, Stats::new(&[-2.0, 14.0], None), Some(3));
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,
]);
@ -216,15 +248,7 @@ mod tests {
500000.0,
500000.0,
];
let intervals = vector.len();
let stats = Stats::new(vector, None);
let mut hist = Histogram::new(
intervals,
(stats.max - stats.min) / intervals as f64,
stats,
None,
);
hist.load(vector);
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"));

5
src/plot/matchbar.rs

@ -3,6 +3,8 @@ use std::fmt;
use yansi::Color::{Blue, Green, Red};
#[derive(Debug)]
/// A struct that represents a single match bar of a match bar histogram (a
/// bucket for a term/label).
pub struct MatchBarRow {
pub label: String,
pub count: usize,
@ -24,6 +26,8 @@ impl MatchBarRow {
}
#[derive(Debug)]
/// A struct holding data to plot a MatchBar: a histogram of the number of
/// occurrences of a set of strings in some input dara.
pub struct MatchBar {
pub vec: Vec<MatchBarRow>,
top_values: usize,
@ -31,6 +35,7 @@ pub struct MatchBar {
}
impl MatchBar {
/// Creates a Histogram from a vector of MatchBarRow elements.
pub fn new(vec: Vec<MatchBarRow>) -> MatchBar {
let mut top_lenght: usize = 0;
let mut top_values: usize = 0;

20
src/plot/splittimehist.rs

@ -31,6 +31,8 @@ impl TimeBucket {
}
#[derive(Debug)]
/// A struct holding data to plot a split time histogram, where the display
/// shows the frequency of selected terms over time.
pub struct SplitTimeHistogram {
vec: Vec<TimeBucket>,
strings: Vec<String>,
@ -42,6 +44,13 @@ pub struct SplitTimeHistogram {
}
impl SplitTimeHistogram {
/// Creates a SplitTimeHistogram from a vector of `strings` (the terms whose
/// frequency we want to display) and a vector of timestamps where the terms
/// appear.
///
/// `size` is the number of time slots in the histogram. Parameter 'ts' is
/// a slice of tuples of DateTime (the timestamp of a term occurrence) and
/// the index of the term in the `strings` parameter.
pub fn new(
size: usize,
strings: Vec<String>,
@ -68,13 +77,18 @@ impl SplitTimeHistogram {
sth
}
fn load(&mut self, vec: &[(DateTime<FixedOffset>, usize)]) {
/// Add to the `SplitTimeHistogram` data the values of a slice of tuples of
/// DateTime (the timestamp of a term occurrence) and the index of the term
/// in the in the list of common terms.
pub fn load(&mut self, vec: &[(DateTime<FixedOffset>, usize)]) {
for x in vec {
self.add(x.0, x.1);
}
}
fn add(&mut self, ts: DateTime<FixedOffset>, index: usize) {
/// Add to the `SplitTimeHistogram` data another data point (a timestamp and
/// index of the term in the list of common terms).
pub fn add(&mut self, ts: DateTime<FixedOffset>, index: usize) {
if let Some(slot) = self.find_slot(ts) {
self.vec[slot].inc(index);
}
@ -89,7 +103,7 @@ impl SplitTimeHistogram {
}
}
// Clippy gets badly confused necause self.strings and COLORS may have
// Clippy gets badly confused because self.strings and COLORS may have
// different lengths
#[allow(clippy::needless_range_loop)]
fn fmt_row(

9
src/plot/terms.rs

@ -4,12 +4,20 @@ use std::fmt;
use yansi::Color::{Blue, Green, Red};
#[derive(Debug)]
/// A struct holding data to plot a Histogram of the most frequent terms in an
/// arbitrary input.
///
/// The struct is create empty and it will fill its data by calling its
/// `observe` method.
pub struct CommonTerms {
pub terms: HashMap<String, usize>,
lines: usize,
}
impl CommonTerms {
/// Create and empty `CommonTerms`.
///
/// `lines` is the number of lines to be displayed.
pub fn new(lines: usize) -> CommonTerms {
CommonTerms {
terms: HashMap::new(),
@ -17,6 +25,7 @@ impl CommonTerms {
}
}
/// Observe a new "term".
pub fn observe(&mut self, term: String) {
*self.terms.entry(term).or_insert(0) += 1
}

25
src/plot/timehist.rs

@ -22,6 +22,7 @@ impl TimeBucket {
}
#[derive(Debug)]
/// A struct holding data to plot a TimeHistogram of timestamp data.
pub struct TimeHistogram {
vec: Vec<TimeBucket>,
min: DateTime<FixedOffset>,
@ -33,6 +34,9 @@ pub struct TimeHistogram {
}
impl TimeHistogram {
/// Creates a Histogram from a vector of DateTime elements.
///
/// `size` is the number of histogram buckets to display.
pub fn new(size: usize, ts: &[DateTime<FixedOffset>]) -> TimeHistogram {
let mut vec = Vec::<TimeBucket>::with_capacity(size);
let min = *ts.iter().min().unwrap();
@ -42,7 +46,7 @@ impl TimeHistogram {
for i in 0..size {
vec.push(TimeBucket::new(min + (inc * i as i32)));
}
TimeHistogram {
let mut timehist = TimeHistogram {
vec,
min,
max,
@ -50,15 +54,23 @@ impl TimeHistogram {
top: 0,
last: size - 1,
nanos: (max - min).num_microseconds().unwrap() as u64,
}
};
timehist.load(ts);
timehist
}
/// Add to the `TimeHistogram` data the values of a slice of DateTime
/// elements. Elements not in the initial range (the one passed to `new`)
/// will be silently discarded.
pub fn load(&mut self, vec: &[DateTime<FixedOffset>]) {
for x in vec {
self.add(*x);
}
}
/// Add to the `TimeHistogram` another DateTime element. If element is not
/// in the initial range (the one passed to `new`), it will be silently
/// discarded.
pub fn add(&mut self, ts: DateTime<FixedOffset>) {
if let Some(slot) = self.find_slot(ts) {
self.vec[slot].inc();
@ -129,8 +141,7 @@ mod tests {
vec.push(DateTime::parse_from_rfc3339("2022-04-15T04:25:00+00:00").unwrap());
vec.push(DateTime::parse_from_rfc3339("2022-04-15T04:25:00+00:00").unwrap());
vec.push(DateTime::parse_from_rfc3339("2023-04-15T04:25:00+00:00").unwrap());
let mut th = TimeHistogram::new(3, &vec);
th.load(&vec);
let th = TimeHistogram::new(3, &vec);
let display = format!("{}", th);
assert!(display.contains("Matches: 5"));
assert!(display.contains("represents a count of 1"));
@ -146,8 +157,7 @@ mod tests {
vec.push(DateTime::parse_from_rfc3339("2022-04-15T04:25:00.001+00:00").unwrap());
vec.push(DateTime::parse_from_rfc3339("2022-04-15T04:25:00.002+00:00").unwrap());
vec.push(DateTime::parse_from_rfc3339("2022-04-15T04:25:00.006+00:00").unwrap());
let mut th = TimeHistogram::new(4, &vec);
th.load(&vec);
let th = TimeHistogram::new(4, &vec);
let display = format!("{}", th);
assert!(display.contains("Matches: 3"));
assert!(display.contains("represents a count of 1"));
@ -163,8 +173,7 @@ mod tests {
let mut vec = Vec::<DateTime<FixedOffset>>::new();
vec.push(DateTime::parse_from_rfc3339("2022-04-15T04:25:00.001+00:00").unwrap());
vec.push(DateTime::parse_from_rfc3339("2022-04-15T04:25:00.001+00:00").unwrap());
let mut th = TimeHistogram::new(4, &vec);
th.load(&vec);
let th = TimeHistogram::new(4, &vec);
let display = format!("{}", th);
assert!(display.contains("Matches: 2"));
assert!(display.contains("represents a count of 1"));

41
src/plot/xy.rs

@ -7,6 +7,7 @@ use crate::format::F64Formatter;
use crate::stats::Stats;
#[derive(Debug)]
/// A struct holding data to plot a XY graph.
pub struct XyPlot {
x_axis: Vec<f64>,
y_axis: Vec<f64>,
@ -17,7 +18,36 @@ pub struct XyPlot {
}
impl XyPlot {
pub fn new(width: usize, height: usize, stats: Stats, precision: Option<usize>) -> XyPlot {
/// Creates a XyPlot from a vector of numerical data.
///
/// `width` is the number of "columns" to display (capped to the length of
/// input data). The data in every column is the average of the y-values
/// that would be aggregated into the x-value of the column (every column
/// has a width of a character).
///
/// `height` is the number of "rows" to display (every row has a height of a
/// character).
///
/// `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], width: usize, height: usize, precision: Option<usize>) -> XyPlot {
let mut plot = XyPlot::new_with_stats(width, height, Stats::new(vec, precision), precision);
plot.load(vec);
plot
}
/// Creates a XyPlot 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(
width: usize,
height: usize,
stats: Stats,
precision: Option<usize>,
) -> XyPlot {
XyPlot {
x_axis: Vec::with_capacity(width),
y_axis: Vec::with_capacity(height),
@ -28,6 +58,7 @@ impl XyPlot {
}
}
/// Add to the `XyPlot` data the values of a slice of numerical data.
pub fn load(&mut self, vec: &[f64]) {
self.width = self.width.min(vec.len());
let num_chunks = vec.len() / self.width;
@ -103,7 +134,7 @@ mod tests {
#[test]
fn basic_test() {
let stats = Stats::new(&[-1.0, 4.0], None);
let mut plot = XyPlot::new(3, 5, stats, Some(3));
let mut plot = XyPlot::new_with_stats(3, 5, stats, Some(3));
plot.load(&[-1.0, 0.0, 1.0, 2.0, 3.0, 4.0, -1.0]);
assert_float_eq!(plot.x_axis[0], -0.5, rmax <= f64::EPSILON);
assert_float_eq!(plot.x_axis[1], 1.5, rmax <= f64::EPSILON);
@ -117,7 +148,7 @@ mod tests {
#[test]
fn display_test() {
let stats = Stats::new(&[-1.0, 4.0], None);
let mut plot = XyPlot::new(3, 5, stats, Some(3));
let mut plot = XyPlot::new_with_stats(3, 5, stats, Some(3));
plot.load(&[-1.0, 0.0, 1.0, 2.0, 3.0, 4.0, -1.0]);
Paint::disable();
let display = format!("{}", plot);
@ -130,9 +161,7 @@ mod tests {
#[test]
fn display_test_human_units() {
let vector = &[1000000.0, -1000000.0, -2000000.0, -4000000.0];
let stats = Stats::new(vector, None);
let mut plot = XyPlot::new(3, 5, stats, None);
plot.load(vector);
let plot = XyPlot::new(vector, 3, 5, None);
Paint::disable();
let display = format!("{}", plot);
assert!(display.contains("[ 0 K] ● "));

18
src/stats/mod.rs

@ -5,18 +5,30 @@ use yansi::Color::Blue;
use crate::format::F64Formatter;
#[derive(Debug)]
/// A struct holding statistical data regarding a unsorted set of numerical
/// values.
pub struct Stats {
/// Minimum of the input values.
pub min: f64,
/// Maximum of the input values.
pub max: f64,
/// Average of the input values.
pub avg: f64,
/// Standard deviation of the input values.
pub std: f64,
/// Variance of the input values.
pub var: f64,
pub sum: f64,
/// Number of samples of the input values.
pub samples: usize,
pub precision: Option<usize>, // If None, then human friendly display will be used
precision: Option<usize>, // If None, then human friendly display will be used
}
impl Stats {
/// Creates a Stats struct from a vector of numerical 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], precision: Option<usize>) -> Stats {
let mut max = vec[0];
let mut min = max;
@ -36,7 +48,6 @@ impl Stats {
avg,
std,
var,
sum,
samples: vec.len(),
precision,
}
@ -76,7 +87,6 @@ mod tests {
fn basic_test() {
let stats = Stats::new(&[1.1, 3.3, 2.2], Some(3));
assert_eq!(3_usize, stats.samples);
assert_float_eq!(stats.sum, 6.6, rmax <= f64::EPSILON);
assert_float_eq!(stats.avg, 2.2, rmax <= f64::EPSILON);
assert_float_eq!(stats.min, 1.1, rmax <= f64::EPSILON);
assert_float_eq!(stats.max, 3.3, rmax <= f64::EPSILON);

Loading…
Cancel
Save