aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorjakobst1n <jakob.stendahl@outlook.com>2024-06-08 20:15:35 +0200
committerjakobst1n <jakob.stendahl@outlook.com>2024-06-08 20:15:35 +0200
commit136502f371851efec48426d424115e136033c157 (patch)
treee4ec71f3dd3733503c5c284ce87fbb0ae0a36909
parent6e9628e2d20c09eaeb7eedfe2f7278de0b65a09f (diff)
downloadtextgraph-136502f371851efec48426d424115e136033c157.tar.gz
textgraph-136502f371851efec48426d424115e136033c157.zip
Add some basic functionality
-rw-r--r--src/graph.rs259
-rw-r--r--src/graph_canvas.rs137
-rw-r--r--src/lib.rs3
-rw-r--r--src/main.rs39
-rw-r--r--src/parseopts.rs140
-rw-r--r--src/term.rs30
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]
+ }
+}
diff --git a/src/lib.rs b/src/lib.rs
index 6f94350..11f9db9 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -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))
+}