diff options
author | jakobst1n <jakob.stendahl@outlook.com> | 2024-06-09 15:51:10 +0200 |
---|---|---|
committer | jakobst1n <jakob.stendahl@outlook.com> | 2024-06-09 15:51:10 +0200 |
commit | 0197c7069d9c814650cea8caf14731a39b8eca89 (patch) | |
tree | 51e16e5374c5f4ec1e35a775c9dd6541c8f3e062 | |
parent | 4e6a860b275abda39ade147ee7cdc48a3520212a (diff) | |
download | textgraph-0197c7069d9c814650cea8caf14731a39b8eca89.tar.gz textgraph-0197c7069d9c814650cea8caf14731a39b8eca89.zip |
Add some documentation
-rw-r--r-- | Cargo.toml | 3 | ||||
-rw-r--r-- | src/graph.rs | 51 | ||||
-rw-r--r-- | src/graph_canvas.rs | 50 | ||||
-rw-r--r-- | src/main.rs | 71 | ||||
-rw-r--r-- | src/parseopts.rs | 197 |
5 files changed, 287 insertions, 85 deletions
@@ -3,6 +3,5 @@ name = "textgraph" version = "0.1.0" edition = "2021" -# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html - [dependencies] + diff --git a/src/graph.rs b/src/graph.rs index 5ff7f6c..fd616ea 100644 --- a/src/graph.rs +++ b/src/graph.rs @@ -15,9 +15,12 @@ pub struct GraphOptions { pub axis: bool, } -/** - * Simply downsample, not the most correct way, but will likely not be too bad. - */ +/// Simply downsample, not the most correct way, but will likely not be too bad. +/// +/// # Arguments +/// +/// * `y_values` - The y values that should be downsampled +/// * `column_count` - Desired resolution of the output pub fn downsample(y_values: &[f64], column_count: usize) -> Vec<f64> { let factor = y_values.len() as f64 / column_count as f64; (0..column_count) @@ -25,9 +28,13 @@ pub fn downsample(y_values: &[f64], column_count: usize) -> Vec<f64> { .collect() } -/** - * A better way to downsize, heavier and more complex, but should be used when sample speed is uneven. - */ +/// A better way to downsize, heavier and more complex, but should be used when sample speed is uneven. +/// +/// # Arguments +/// +/// * `y_values` - The y values that should be downsampled +/// * `x_values` - X values, needed to interpolate while keeping sample distance +/// * `column_count` - Desired resolution of the output pub fn interpolate(y_values: &[f64], x_values: &[f64], column_count: usize) -> Vec<f64> { let min_x = x_values.iter().cloned().fold(f64::INFINITY, f64::min); let max_x = x_values.iter().cloned().fold(f64::NEG_INFINITY, f64::max); @@ -51,9 +58,12 @@ pub fn interpolate(y_values: &[f64], x_values: &[f64], column_count: usize) -> V interpolated_data } -/** - * Scale a value to a new scale, useful for y values which needs to be scaled to fit within a size - */ +/// Scale a value to a new scale, useful for y values which needs to be scaled to fit within a size +/// +/// # Arguments +/// +/// * `values` - The values to scale to a new height +/// * `row_count` - The desired range of the new values (0 -> row_count) fn scale(values: &[f64], row_count: usize) -> Vec<usize> { let min_value = values.iter().cloned().fold(f64::INFINITY, f64::min); let max_value = values.iter().cloned().fold(f64::NEG_INFINITY, f64::max); @@ -64,6 +74,15 @@ fn scale(values: &[f64], row_count: usize) -> Vec<usize> { .collect() } +/// Prepare the values of a graph before graphing +/// by applying scaling and interpolation/downscaling +/// +/// # Arguments +/// +/// * `x_values` - Values of the x-axis, needed for interpolation +/// * `y_values` - Graph values +/// * `graph` - The graph object, needed for knowing the information about width and height +/// * `options` - GraphOptions, used for forced interpolation pub fn prepare( y_values: &[f64], x_values: &[f64], @@ -85,6 +104,13 @@ pub fn prepare( scaled_data } +/// Draw a graph using * for the pixels of the graph +/// +/// # Arguments +/// +/// * `x_values` - Values of the x-axis +/// * `y_values` - Graph values +/// * `options` - GraphOptions, used for forced interpolation pub fn star(y_values: &[f64], x_values: &[f64], options: &GraphOptions) -> String { let mut graph = GraphCanvas::new(options.width as usize, options.height as usize); if options.axis { @@ -107,6 +133,13 @@ pub fn star(y_values: &[f64], x_values: &[f64], options: &GraphOptions) -> Strin graph.to_string() } +/// Draw a graph using somewhat pretty ascii characters for pixels of the graph +/// +/// # Arguments +/// +/// * `x_values` - Values of the x-axis +/// * `y_values` - Graph values +/// * `options` - GraphOptions, used for forced interpolation pub fn ascii(y_values: &[f64], x_values: &[f64], options: &GraphOptions) -> String { let mut graph = GraphCanvas::new_default( GraphPixel::Blank, diff --git a/src/graph_canvas.rs b/src/graph_canvas.rs index be5c4aa..884f597 100644 --- a/src/graph_canvas.rs +++ b/src/graph_canvas.rs @@ -29,21 +29,43 @@ impl std::fmt::Display for GraphPixel { } } +/// Temporary variables used while building a graph pub struct GraphCanvas<T> { + /// A array of pixels, this will ultimately be turned to a string, is initialized to width * height elements: Vec<T>, + /// Width of canvas width: usize, + /// Height of canvas height: usize, + /// Width of the area of the canvas left for the actual graph draw_width: usize, + /// Height of the area of the canvas left for the actual graph draw_height: usize, + /// x-offset for where the graph draw area begins col_offset: usize, + /// y-offset for where the graph draw area begins row_offset: usize, } impl<T: Clone + Default + std::fmt::Display> GraphCanvas<T> { + + /// Create a new canvas with desired width and height + /// + /// # Arguments + /// + /// * `width` - Width of the output canvas + /// * `height` - Height of the output canvas pub fn new(width: usize, height: usize) -> Self { GraphCanvas::new_default(T::default(), width, height) } + /// Create a new canvas with desired width, height, and default canvas pixel + /// + /// # Arguments + /// + /// * `default` - Pixel to use for the "background" of the canvas + /// * `width` - Width of the output canvas + /// * `height` - Height of the output canvas pub fn new_default(default: T, width: usize, height: usize) -> Self { GraphCanvas { elements: vec![default; width * height], @@ -56,6 +78,7 @@ impl<T: Clone + Default + std::fmt::Display> GraphCanvas<T> { } } + /// Turn canvas into a string pub fn to_string(&self) -> String { let mut out = String::with_capacity(self.height * (self.width + 1)); for (i, px) in self.elements.iter().enumerate() { @@ -67,6 +90,16 @@ impl<T: Clone + Default + std::fmt::Display> GraphCanvas<T> { out } + /// Add axis to the canvas and move graph drawing area inside axis + /// + /// # Arguments + /// + /// * `c1` - Horizontal axis lines + /// * `c2` - Vertical axis lines + /// * `c4` - Bottom left axis pixel + /// * `c5` - Top left axis pixel + /// * `c6` - Bottom right axis pixel + /// * `c7` - Top right axis pixel pub fn axis(&mut self, c1: T, c2: T, c3: T, c4: T, c5: T, c6: T) { if self.height < 2 || self.width < 2 { return; @@ -93,27 +126,44 @@ impl<T: Clone + Default + std::fmt::Display> GraphCanvas<T> { self.row_offset = 1; } + /// Width of drawable area of graph pub fn width(&self) -> usize { self.draw_width } + /// Total width of graph canvas pub fn full_width(&self) -> usize { self.width } + /// Height of drawable area of graph pub fn height(&self) -> usize { self.draw_height } + /// Total height of graph canvas pub fn full_height(&self) -> usize { self.height } + /// Set a pixel at a absolute position in the canvas + /// + /// # Argument + /// + /// * `x` - X-position of pixel + /// * `y` - Y-position of pixel + /// * `px` - The pixel to set pub fn set(&mut self, x: usize, y: usize, px: T) { let pos = y * self.width + x; self.elements[pos] = px; } + /// Get the absolite position of a character from a coordinate drawable part of the canvas + /// + /// # Argument + /// + /// * `x` - Relative X-position of pixel + /// * `y` - Relative Y-position of pixel fn element_position(&self, row: usize, col: usize) -> usize { (row + self.row_offset) * self.width + (col + self.col_offset) } diff --git a/src/main.rs b/src/main.rs index e9798c5..49650e1 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,19 +1,62 @@ use textgraph::graph; -use textgraph::parseopts::parseopts; +use textgraph::parseopts::{parseopts, Opts}; +use std::io::{self, BufRead, Write}; +use std::str::FromStr; -fn main() { - let opts = parseopts(); +/// Will graph what comes in through stdin, +/// For each new line, the graph will be re-drawn. +/// +/// # Arguments +/// +/// * `opts` - textgraph::parseopts::Opts +fn filter(opts: Opts) { + print!("\x1b[?1049h"); + + let mut x_values: Vec<f64> = Vec::new(); + let mut y_values: Vec<f64> = Vec::new(); + let mut i = 0.0; + + let stdin = io::stdin(); + for line in stdin.lock().lines() { + i += 1.0; + let line = line.expect("Could not read..."); + + let y = f64::from_str(line.as_str()).expect("TG7 invalid number"); + y_values.push(y); + x_values.push(i); + + let graph_options: textgraph::graph::GraphOptions = (&opts).into(); + let g = match opts.graph_type { + textgraph::parseopts::GraphType::Ascii => { + graph::ascii(&y_values, &x_values, &graph_options) + } + textgraph::parseopts::GraphType::Star => graph::star(&y_values, &x_values, &graph_options), + }; + print!("\x1B[2J\x1B[H"); + println!("{}", g); + } + + print!("\x1B[?1049l"); + io::stdout().flush().unwrap(); +} + +/// Will graph the contents of a file +/// This assumes opts.in_file is Some, or it will panic! +/// Currently this only supports a single column, with no x-values +/// +/// # Arguments +/// +/// * `opts` - textgraph::parseopts::Opts +fn graph_file(opts: Opts) { + let raw_y_values = std::fs::read_to_string(opts.in_file.clone().unwrap()).expect("TG6"); let mut y_values: Vec<f64> = Vec::new(); let mut x_values: Vec<f64> = Vec::new(); - for i in 0..600 { - y_values.push((i as f64 * std::f64::consts::PI / 120.0).sin()); + for (i, line) in raw_y_values.lines().enumerate() { + y_values.push(f64::from_str(line).expect("TG7")); x_values.push(i as f64); } - //let y_values: [f64; 6] = [1.0, 10.0, 40.0, 0.0, 30.0, 15.0]; - //let x_values: [f64; 6] = [1.0, 2.0, 3.0, 4.0, 5.0, 6.0]; - let graph_options: textgraph::graph::GraphOptions = (&opts).into(); let g = match opts.graph_type { textgraph::parseopts::GraphType::Ascii => { @@ -21,6 +64,16 @@ fn main() { } textgraph::parseopts::GraphType::Star => graph::star(&y_values, &x_values, &graph_options), }; - println!("{}", g); } + +/// Main entry point for the binary of textgraph +fn main() { + let opts = parseopts(); + + if opts.in_file.is_none() { + filter(opts); + } else { + graph_file(opts); + } +} diff --git a/src/parseopts.rs b/src/parseopts.rs index 7b7b2d1..ee05199 100644 --- a/src/parseopts.rs +++ b/src/parseopts.rs @@ -1,27 +1,39 @@ use crate::graph::GraphOptions; use std::str::FromStr; +/// Available options for how the graph should look pub enum GraphType { + /// Use only * symbols Star, + /// Use pretty characters from the ascii range Ascii, } +/// Struct containing command line options pub struct Opts { + /// Desired width of graph, if None, it should be automatically determined pub width: Option<u64>, + /// Desired height of graph, if None, it should be automatically determined pub height: Option<u64>, + /// Which type of graph it should be, ascii, star pub graph_type: GraphType, + /// Wether to always interpolate, even if not nesecarry pub interpolate: bool, + /// Enable axis on the resulting graph, makes it a bit prettier pub axis: bool, + /// Specify if it is used as a filter, and you only want to look at the last N samples pub last_n: Option<u64>, + /// Read from the specified file, instead of reading continously from stdin + pub in_file: Option<String>, } impl From<&Opts> for GraphOptions { + /// Convert from CLIOpts to GraphOptions, + /// This will do some magic, like find the terminal size if not specified, etc. fn from(opts: &Opts) -> Self { GraphOptions { width: opts.width.unwrap_or_else(|| { if let Ok((width, _)) = crate::term::get_terminal_size() { - // Here it would maybe be a good idea to keep the size of the graph if it is smaller than - // the specified value width as u64 } else { println!("Could not determine TTY columns, specify with -r"); @@ -30,8 +42,6 @@ impl From<&Opts> for GraphOptions { }), height: opts.height.unwrap_or_else(|| { if let Ok((_, height)) = crate::term::get_terminal_size() { - // Here it would maybe be a good idea to keep the size of the graph if it is smaller than - // the specified value height as u64 - 1 } else { println!("Could not determine TTY rows, specify with -h"); @@ -44,6 +54,9 @@ impl From<&Opts> for GraphOptions { } } +/// Simple convenience macro for printing usage of the program and exiting without a stacktrace. +/// For some reason, having this as a function didn't always make the compiler recognize that +/// the program exited. macro_rules! parseopts_panic { ($progname:expr) => { println!( @@ -54,6 +67,89 @@ macro_rules! parseopts_panic { }; } +/// Parse a single named option/argument, and update the Opts struct accordingly +/// +/// # Arguments +/// +/// * `opts` - The opts struct to modify +/// * `arg` - The name of the option/argument to read (without the -) +/// * `value` - Optionally the value of the option/argument. This function will panic if not +/// provided when it is required. +/// * `progname` - The first argument of the program, this is used for error messages. +pub fn parseopt(opts: &mut Opts, arg: &str, value: Option<String>, progname: &str) { + match arg { + "interpolate" => { + opts.interpolate = true; + } + "t" => { + let Some(graph_type) = value else { + println!("Missing value for {}", arg); + parseopts_panic!(progname); + }; + match graph_type.as_str() { + "star" => { + opts.graph_type = GraphType::Star; + } + "ascii" => { + opts.graph_type = GraphType::Ascii; + } + t => { + println!( + "Unknown type \"{}\", valid options are \"star\", \"ascii\".", + t + ); + parseopts_panic!(progname); + } + } + } + "h" | "height" => { + let Some(height) = value else { + println!("Missing value for {}", arg); + parseopts_panic!(progname); + }; + let Ok(height) = u64::from_str(&height) else { + println!("Cannot parse integer from \"{}\"", height); + parseopts_panic!(progname); + }; + opts.height = Some(height); + } + "l" | "last-n" => { + let Some(last_n) = value else { + println!("Missing value for {}", arg); + parseopts_panic!(progname); + }; + let Ok(last_n) = u64::from_str(&last_n) else { + println!("Cannot parse integer from \"{}\"", last_n); + parseopts_panic!(progname); + }; + opts.last_n = Some(last_n); + } + "a" | "axis" => { + opts.axis = true; + } + "w" | "width" => { + let Some(width) = value else { + println!("Missing value for {}", arg); + parseopts_panic!(progname); + }; + let Ok(width) = u64::from_str(&width) else { + println!("Cannot parse integer from \"{}\"", width); + parseopts_panic!(progname); + }; + opts.width = Some(width); + } + opt => { + println!("Unknown option \"{}\"", opt); + parseopts_panic!(progname); + } + } +} + +/// Parse command line options passed to binary +/// Very rudimentary argument parser, which allows for the most part the standard convention +/// of unix style command line arguments. +/// This function is specialised for the TextGraph program, +/// but is easily adaptable for other programs as well. pub fn parseopts() -> Opts { let mut opts = Opts { width: None, @@ -62,79 +158,50 @@ pub fn parseopts() -> Opts { interpolate: false, axis: false, last_n: None, + in_file: None, }; let mut it = std::env::args(); let progname = it.next().expect("TG1"); - while let Some(arg) = it.next() { - match arg.as_str() { - "--interpolate" => { - opts.interpolate = true; - } - "-t" => { - let Some(graph_type) = it.next() else { - println!("Missing value for {}", arg); - parseopts_panic!(progname); - }; - match graph_type.as_str() { - "star" => { - opts.graph_type = GraphType::Star; + while let Some(mut arg) = it.next() { + if arg.starts_with("--") { + arg.remove(0); + arg.remove(0); + + let arg_name; + let mut arg_value = None; + if arg.contains('=') { + let mut ita = arg.splitn(2, '='); + arg_name = ita.next().expect("TG4").to_string(); + arg_value = Some(ita.next().expect("TG5").to_string()); + } else { + arg_name = arg.clone(); + match arg_name.as_str() { + "widht" | "height" | "last-n" => { + arg_value = it.next(); } - "ascii" => { - opts.graph_type = GraphType::Ascii; + _ => () + } + } + parseopt(&mut opts, &arg_name, arg_value, &progname); + } else if arg.starts_with("-") { + arg.remove(0); + for arg_name in arg.chars() { + match arg_name { + 'h' | 't' | 'w' | 'l' => { + parseopt(&mut opts, &arg_name.to_string(), it.next(), &progname); } - t => { - println!( - "Unknown type \"{}\", valid options are \"star\", \"ascii_trailing\".", - t - ); - parseopts_panic!(progname); + _ => { + parseopt(&mut opts, &arg_name.to_string(), None, &progname); } } } - "-h" | "--height" => { - let Some(height) = it.next() else { - println!("Missing value for {}", arg); - parseopts_panic!(progname); - }; - let Ok(height) = u64::from_str(&height) else { - println!("Cannot parse integer from \"{}\"", height); - parseopts_panic!(progname); - }; - opts.height = Some(height); - } - "-l" | "--last-n" => { - let Some(last_n) = it.next() else { - println!("Missing value for {}", arg); - parseopts_panic!(progname); - }; - let Ok(last_n) = u64::from_str(&last_n) else { - println!("Cannot parse integer from \"{}\"", last_n); - parseopts_panic!(progname); - }; - opts.last_n = Some(last_n); - } - "-a" | "--axis" => { - opts.axis = true; - } - "-w" | "--width" => { - let Some(width) = it.next() else { - println!("Missing value for {}", arg); - parseopts_panic!(progname); - }; - let Ok(width) = u64::from_str(&width) else { - println!("Cannot parse integer from \"{}\"", width); - parseopts_panic!(progname); - }; - opts.width = Some(width); - } - opt => { - println!("Unknown option \"{}\"", opt); - parseopts_panic!(progname); - } } + } + opts.in_file = it.next(); + return opts; } |