aboutsummaryrefslogtreecommitdiff
path: root/src/graph.rs
diff options
context:
space:
mode:
Diffstat (limited to 'src/graph.rs')
-rw-r--r--src/graph.rs259
1 files changed, 154 insertions, 105 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("")
+// }