From 136502f371851efec48426d424115e136033c157 Mon Sep 17 00:00:00 2001 From: jakobst1n Date: Sat, 8 Jun 2024 20:15:35 +0200 Subject: Add some basic functionality --- src/graph.rs | 259 +++++++++++++++++++++++++++++++++++------------------------ 1 file changed, 154 insertions(+), 105 deletions(-) (limited to 'src/graph.rs') 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 { - 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 { + 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 SeriesTraits for T {} - -impl From<&Vec> for SeriesAspects { - fn from(series: &Vec) -> SeriesAspects { - 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 { - 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 { - 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 { + 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 { - 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 { + 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::()).collect::>().join("\n") -} +pub fn prepare( + y_values: &[f64], + x_values: &[f64], + graph: &GraphCanvas, + options: &GraphOptions, +) -> Vec { + 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::()).collect::>().join("\n") + let scaled_data = scale(&y_values, graph.height()); + scaled_data } -pub fn braille(series: &Vec, 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, 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("") +// } -- cgit v1.2.3