diff options
author | jakobst1n <jakob.stendahl@outlook.com> | 2024-06-08 20:15:35 +0200 |
---|---|---|
committer | jakobst1n <jakob.stendahl@outlook.com> | 2024-06-08 20:15:35 +0200 |
commit | 136502f371851efec48426d424115e136033c157 (patch) | |
tree | e4ec71f3dd3733503c5c284ce87fbb0ae0a36909 | |
parent | 6e9628e2d20c09eaeb7eedfe2f7278de0b65a09f (diff) | |
download | textgraph-136502f371851efec48426d424115e136033c157.tar.gz textgraph-136502f371851efec48426d424115e136033c157.zip |
Add some basic functionality
-rw-r--r-- | src/graph.rs | 259 | ||||
-rw-r--r-- | src/graph_canvas.rs | 137 | ||||
-rw-r--r-- | src/lib.rs | 3 | ||||
-rw-r--r-- | src/main.rs | 39 | ||||
-rw-r--r-- | src/parseopts.rs | 140 | ||||
-rw-r--r-- | src/term.rs | 30 |
6 files changed, 481 insertions, 127 deletions
diff --git a/src/graph.rs b/src/graph.rs index f837f68..5ff7f6c 100644 --- a/src/graph.rs +++ b/src/graph.rs @@ -1,4 +1,4 @@ -const _BRAILLE_1: char = '⣿'; +use crate::graph_canvas::{GraphCanvas, GraphPixel}; const ASCII_0: char = '─'; const ASCII_1: char = '│'; @@ -7,136 +7,185 @@ const ASCII_3: char = '╰'; const ASCII_4: char = '╮'; const ASCII_7: char = '╯'; -const BRAILLE_1_0: char = '⡀'; -const BRAILLE_1_1: char = '⣀'; -const BRAILLE_1_2: char = '⣀'; -const BRAILLE_2_0: char = '⡄'; -const BRAILLE_3_0: char = '⡆'; -const BRAILLE_4_0: char = '⡇'; - - -/* - ╭────────╮ -╭─╯ │ -╰ ╰╮ - ╰──────── -*/ - #[derive(Debug)] pub struct GraphOptions { - pub width: f64, - pub height: f64, + pub width: u64, + pub height: u64, + pub interpolate: bool, + pub axis: bool, } -#[derive(Debug)] -pub struct SeriesAspects<T> { - max: T, - min: T, +/** + * Simply downsample, not the most correct way, but will likely not be too bad. + */ +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) + .map(|i| y_values[(i as f64 * factor) as usize]) + .collect() } -pub trait SeriesTraits: std::cmp::PartialOrd + Clone + std::ops::Div + std::ops::Sub {} -impl<T: std::cmp::PartialOrd + Clone + std::ops::Div + std::ops::Sub> SeriesTraits for T {} - -impl<T: SeriesTraits> From<&Vec<T>> for SeriesAspects<T> { - fn from(series: &Vec<T>) -> SeriesAspects<T> { - let mut it = series.iter(); - let first = it.next(); - let mut min = first.expect("TG2"); - let mut max = first.expect("TG3"); - while let Some(i) = it.next() { - if i < min { - min = i; - } - if i > max { - max = i; - } - } - SeriesAspects { - max: max.clone(), - min: min.clone(), - } - } -} - - -pub fn downsample(series: &[f64], column_count: usize) -> Vec<f64> { - let factor = series.len() as f64 / column_count as f64; - (0..column_count).map(|i| series[(i as f64 * factor) as usize]).collect() -} - -pub fn interpolate(series: &[f64], marks: &[f64], column_count: usize) -> Vec<f64> { - let min_mark = marks.iter().cloned().fold(f64::INFINITY, f64::min); - let max_mark = marks.iter().cloned().fold(f64::NEG_INFINITY, f64::max); - let step = (max_mark - min_mark) / (column_count as f64 - 1.0); +/** + * A better way to downsize, heavier and more complex, but should be used when sample speed is uneven. + */ +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); + let step = (max_x - min_x) / (column_count as f64 - 1.0); let mut interpolated_data = Vec::new(); - + for i in 0..column_count { - let target_mark = min_mark + i as f64 * step; + let target_mark = min_x + i as f64 * step; let mut j = 0; - while j < marks.len() - 1 && marks[j + 1] < target_mark { + while j < x_values.len() - 1 && x_values[j + 1] < target_mark { j += 1; } - let t0 = marks[j]; - let t1 = marks[j + 1]; - let d0 = series[j]; - let d1 = series[j + 1]; + let t0 = x_values[j]; + let t1 = x_values[j + 1]; + let d0 = y_values[j]; + let d1 = y_values[j + 1]; let value = d0 + (d1 - d0) * (target_mark - t0) / (t1 - t0); interpolated_data.push(value); } - + interpolated_data } -fn scale(series: &[f64], row_count: usize) -> Vec<usize> { - let min_value = series.iter().cloned().fold(f64::INFINITY, f64::min); - let max_value = series.iter().cloned().fold(f64::NEG_INFINITY, f64::max); +/** + * Scale a value to a new scale, useful for y values which needs to be scaled to fit within a size + */ +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); let scale_factor = (row_count - 1) as f64 / (max_value - min_value); - series.iter().map(|&y| ((y - min_value) * scale_factor).round() as usize).collect() + values + .iter() + .map(|&y| ((y - min_value) * scale_factor).round() as usize) + .collect() } -pub fn star(series: &[f64], options: &GraphOptions) -> String { - let scaled_data = scale(series, options.height as usize); - let mut graph = vec![vec![' '; options.width as usize]; options.height as usize]; - - for (i, &value) in scaled_data.iter().enumerate() { - let y = options.height as usize - value - 1; // Invert y-axis for ASCII graph - graph[y][i] = '*'; - } - - graph.iter().map(|row| row.iter().collect::<String>()).collect::<Vec<String>>().join("\n") -} +pub fn prepare( + y_values: &[f64], + x_values: &[f64], + graph: &GraphCanvas<GraphPixel>, + options: &GraphOptions, +) -> Vec<usize> { + let y_values = if !options.interpolate { + // && x_values.windows(2).all(|w| w[1] - w[0] == w[0] - w[1]) { + if y_values.len() >= graph.width() { + downsample(&y_values, graph.width()) + } else { + y_values.to_vec() + } + } else { + interpolate(&y_values, &x_values, graph.width()) + }; -pub fn ascii_trailing(series: &[f64], options: &GraphOptions) -> String { - let scaled_data = scale(series, options.height as usize); - let mut graph = vec![vec![' '; options.width as usize]; options.height as usize]; - - for (i, &value) in scaled_data.iter().enumerate() { - let y = options.height as usize - value - 1; // Invert y-axis for ASCII graph - graph[y][i] = '*'; - } - - graph.iter().map(|row| row.iter().collect::<String>()).collect::<Vec<String>>().join("\n") + let scaled_data = scale(&y_values, graph.height()); + scaled_data } -pub fn braille(series: &Vec<f64>, options: &GraphOptions) -> String { - let aspects = SeriesAspects::from(series); - let canvas = String::with_capacity((options.width * options.height) as usize); +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 { + graph.axis( + GraphPixel::Normal(ASCII_1), + GraphPixel::Normal(ASCII_0), + GraphPixel::Normal('└'), + GraphPixel::Normal('┌'), + GraphPixel::Normal('┘'), + GraphPixel::Normal('┐'), + ); + } + let y_values = prepare(y_values, x_values, &graph, options); + for (i, &value) in y_values.iter().enumerate() { + let y = graph.height() - value - 1; + graph[(y, i)] = GraphPixel::Normal('*'); + } - /* - r = (max - min) - r' = (max' - min') - y' = (((y - min) * r') / r) + min' - */ - let r = aspects.max - aspects.min; - let r_marked = options.height; + graph.to_string() +} - let norm_after = options.height; +pub fn ascii(y_values: &[f64], x_values: &[f64], options: &GraphOptions) -> String { + let mut graph = GraphCanvas::new_default( + GraphPixel::Blank, + options.width as usize, + options.height as usize, + ); + if options.axis { + graph.axis( + GraphPixel::Normal(ASCII_1), + GraphPixel::Normal(ASCII_0), + GraphPixel::Normal('└'), + GraphPixel::Normal('┌'), + GraphPixel::Normal('┘'), + GraphPixel::Normal('┐'), + ); + } - //for (x, y) in series.iter().enumerate() { - // let y = norm(y.clone(), 0.0, options.height); - // let x = norm(x.clone(), 0.0, options.width); - //} + let y_values = prepare(y_values, x_values, &graph, options); + if options.axis { + graph.set(0, graph.height() - y_values[0], GraphPixel::Green('├')); + graph.set( + graph.full_width() - 1, + graph.height() - y_values[y_values.len() - 1], + GraphPixel::Green('┤'), + ); + } + for i in 0..y_values.len() { + let y1 = graph.height() - y_values[i] - 1; + let y2 = if i < y_values.len() - 1 { + graph.height() - y_values[i + 1] - 1 + } else { + y1 + }; + + if y1 == y2 { + graph[(y1, i)] = GraphPixel::Green(ASCII_0); + } else if y1 > y2 { + graph[(y1, i)] = GraphPixel::Green(ASCII_7); + graph[(y2, i)] = GraphPixel::Green(ASCII_2); + for j in (y2 + 1)..y1 { + graph[(j, i)] = GraphPixel::Green(ASCII_1); + } + } else { + graph[(y1, i)] = GraphPixel::Green(ASCII_4); + graph[(y2, i)] = GraphPixel::Green(ASCII_3); + for j in (y1 + 1)..y2 { + graph[(j, i)] = GraphPixel::Green(ASCII_1); + } + } + } - String::from("") + graph.to_string() } + +//const _BRAILLE_1: char = '⣿'; +//const BRAILLE_1_0: char = '⡀'; +//const BRAILLE_1_1: char = '⣀'; +//const BRAILLE_1_2: char = '⣀'; +//const BRAILLE_2_0: char = '⡄'; +//const BRAILLE_3_0: char = '⡆'; +//const BRAILLE_4_0: char = '⡇'; +// pub fn braille(y_values: &Vec<f64>, options: &GraphOptions) -> String { +// let aspects = SeriesAspects::from(y_values); +// let canvas = String::with_capacity((options.width * options.height) as usize); +// +// /* +// r = (max - min) +// r' = (max' - min') +// y' = (((y - min) * r') / r) + min' +// */ +// let r = aspects.max - aspects.min; +// let r_marked = options.height; +// +// let norm_after = options.height; +// +// //for (x, y) in y_values.iter().enumerate() { +// // let y = norm(y.clone(), 0.0, options.height); +// // let x = norm(x.clone(), 0.0, options.width); +// //} +// +// String::from("") +// } diff --git a/src/graph_canvas.rs b/src/graph_canvas.rs new file mode 100644 index 0000000..be5c4aa --- /dev/null +++ b/src/graph_canvas.rs @@ -0,0 +1,137 @@ +use std::ops::{Index, IndexMut}; +//use std::io::IsTerminal; +//dbg!(std::io::stdout().is_terminal()); + +#[derive(Clone)] +pub enum GraphPixel { + Normal(char), + Green(char), + Blank, +} + +impl std::default::Default for GraphPixel { + fn default() -> Self { + GraphPixel::Blank + } +} + +impl std::fmt::Display for GraphPixel { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + write!( + f, + "{}", + match self { + GraphPixel::Normal(c) => format!("{}", c), + GraphPixel::Green(c) => format!("\x1b[32m{}\x1b[0m", c), + GraphPixel::Blank => String::from(" "), + } + ) + } +} + +pub struct GraphCanvas<T> { + elements: Vec<T>, + width: usize, + height: usize, + draw_width: usize, + draw_height: usize, + col_offset: usize, + row_offset: usize, +} + +impl<T: Clone + Default + std::fmt::Display> GraphCanvas<T> { + pub fn new(width: usize, height: usize) -> Self { + GraphCanvas::new_default(T::default(), width, height) + } + + pub fn new_default(default: T, width: usize, height: usize) -> Self { + GraphCanvas { + elements: vec![default; width * height], + width, + height, + draw_width: width, + draw_height: height, + col_offset: 0, + row_offset: 0, + } + } + + 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() { + out.push_str(&px.to_string()); + if (i + 1) % self.width == 0 && i < (self.height * self.width - 1) { + out.push('\n'); + } + } + out + } + + 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; + } + for i in 0..self.height { + self.elements[i * self.width] = c1.clone(); + self.elements[i * self.width + self.width - 1] = c1.clone(); + } + for i in 1..self.width - 1 { + self.elements[i] = c2.clone(); + self.elements[(self.height - 1) * self.width + i] = c2.clone(); + } + self.elements[0] = c4.clone(); + self.elements[self.width - 1] = c6.clone(); + self.elements[(self.height - 1) * self.width] = c3.clone(); + self.elements[self.height * self.width - 1] = c5.clone(); + if self.draw_height > 2 { + self.draw_height = self.height - 2; + } + if self.draw_width > 2 { + self.draw_width = self.width - 2; + } + self.col_offset = 1; + self.row_offset = 1; + } + + pub fn width(&self) -> usize { + self.draw_width + } + + pub fn full_width(&self) -> usize { + self.width + } + + pub fn height(&self) -> usize { + self.draw_height + } + + pub fn full_height(&self) -> usize { + self.height + } + + pub fn set(&mut self, x: usize, y: usize, px: T) { + let pos = y * self.width + x; + self.elements[pos] = px; + } + + fn element_position(&self, row: usize, col: usize) -> usize { + (row + self.row_offset) * self.width + (col + self.col_offset) + } +} + +impl<T: Clone + Default + std::fmt::Display> Index<(usize, usize)> for GraphCanvas<T> { + type Output = T; + + fn index(&self, index: (usize, usize)) -> &Self::Output { + let (row, col) = index; + &self.elements[self.element_position(row, col)] + } +} + +impl<T: Clone + Default + std::fmt::Display> IndexMut<(usize, usize)> for GraphCanvas<T> { + fn index_mut(&mut self, index: (usize, usize)) -> &mut Self::Output { + let (row, col) = index; + let pos = self.element_position(row, col); + &mut self.elements[pos] + } +} @@ -1 +1,4 @@ pub mod graph; +pub mod graph_canvas; +pub mod parseopts; +pub mod term; diff --git a/src/main.rs b/src/main.rs index 4cae5be..e9798c5 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,31 +1,26 @@ use textgraph::graph; +use textgraph::parseopts::parseopts; fn main() { - let mut line: Vec<f64> = Vec::new(); - let mut marks: Vec<f64> = Vec::new(); - for i in 0..500 { - line.push((i as f64 * std::f64::consts::PI / 120.0).sin()); - marks.push(i as f64); - } - - // Choose one of the methods based on sample speed: - //let downsampled_data = graph::downsample(&line, 100); - let interpolated_data = graph::interpolate(&line, &marks, 100); + let opts = parseopts(); + 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()); + x_values.push(i as f64); + } - //let processed_data = if marks.windows(2).all(|w| w[1] - w[0] == w[0] - w[1]) { - // downsample(&series, options.width) - //} else { - // interpolate(&series, &marks, options.width) - //}; + //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 => { + graph::ascii(&y_values, &x_values, &graph_options) + } + textgraph::parseopts::GraphType::Star => graph::star(&y_values, &x_values, &graph_options), + }; - let g = graph::ascii_trailing( - &interpolated_data, - &graph::GraphOptions { - width: 100.0, - height: 30.0, - }, - ); println!("{}", g); } diff --git a/src/parseopts.rs b/src/parseopts.rs new file mode 100644 index 0000000..7b7b2d1 --- /dev/null +++ b/src/parseopts.rs @@ -0,0 +1,140 @@ +use crate::graph::GraphOptions; +use std::str::FromStr; + +pub enum GraphType { + Star, + Ascii, +} + +pub struct Opts { + pub width: Option<u64>, + pub height: Option<u64>, + pub graph_type: GraphType, + pub interpolate: bool, + pub axis: bool, + pub last_n: Option<u64>, +} + +impl From<&Opts> for GraphOptions { + 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"); + std::process::exit(1); + } + }), + 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"); + std::process::exit(1); + } + }), + interpolate: opts.interpolate, + axis: opts.axis, + } + } +} + +macro_rules! parseopts_panic { + ($progname:expr) => { + println!( + "Usage: {} [-h|--height <height>] [-w|--width <width>] [-t <star|ascii>]", + $progname + ); + std::process::exit(1); + }; +} + +pub fn parseopts() -> Opts { + let mut opts = Opts { + width: None, + height: None, + graph_type: GraphType::Star, + interpolate: false, + axis: false, + last_n: 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; + } + "ascii" => { + opts.graph_type = GraphType::Ascii; + } + t => { + println!( + "Unknown type \"{}\", valid options are \"star\", \"ascii_trailing\".", + t + ); + parseopts_panic!(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); + } + } + } + + return opts; +} diff --git a/src/term.rs b/src/term.rs new file mode 100644 index 0000000..275ab60 --- /dev/null +++ b/src/term.rs @@ -0,0 +1,30 @@ +use std::fs::File; +use std::io::Error; +use std::mem; +use std::os::raw::{c_int, c_ushort}; +use std::os::unix::io::AsRawFd; + +#[repr(C)] +struct Winsize { + ws_row: c_ushort, + ws_col: c_ushort, + ws_xpixel: c_ushort, + ws_ypixel: c_ushort, +} + +const TIOCGWINSZ: c_int = 0x5413; + +extern "C" { + fn ioctl(fd: c_int, request: c_int, ...) -> c_int; +} + +pub fn get_terminal_size() -> Result<(u16, u16), Error> { + let stdout = File::open("/dev/tty")?; + let fd = stdout.as_raw_fd(); + let mut ws: Winsize = unsafe { mem::zeroed() }; + let result = unsafe { ioctl(fd, TIOCGWINSZ, &mut ws) }; + if result == -1 { + return Err(Error::last_os_error()); + } + Ok((ws.ws_col, ws.ws_row)) +} |