aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorjakobst1n <jakob.stendahl@outlook.com>2024-06-09 15:51:10 +0200
committerjakobst1n <jakob.stendahl@outlook.com>2024-06-09 15:51:10 +0200
commit0197c7069d9c814650cea8caf14731a39b8eca89 (patch)
tree51e16e5374c5f4ec1e35a775c9dd6541c8f3e062
parent4e6a860b275abda39ade147ee7cdc48a3520212a (diff)
downloadtextgraph-0197c7069d9c814650cea8caf14731a39b8eca89.tar.gz
textgraph-0197c7069d9c814650cea8caf14731a39b8eca89.zip
Add some documentation
-rw-r--r--Cargo.toml3
-rw-r--r--src/graph.rs51
-rw-r--r--src/graph_canvas.rs50
-rw-r--r--src/main.rs71
-rw-r--r--src/parseopts.rs197
5 files changed, 287 insertions, 85 deletions
diff --git a/Cargo.toml b/Cargo.toml
index eaeeca4..f3e67b1 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -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;
}