aboutsummaryrefslogtreecommitdiff
path: root/src/graph.rs
diff options
context:
space:
mode:
Diffstat (limited to 'src/graph.rs')
-rw-r--r--src/graph.rs460
1 files changed, 300 insertions, 160 deletions
diff --git a/src/graph.rs b/src/graph.rs
index fd616ea..52e332c 100644
--- a/src/graph.rs
+++ b/src/graph.rs
@@ -1,4 +1,5 @@
-use crate::graph_canvas::{GraphCanvas, GraphPixel};
+//use std::io::IsTerminal;
+//dbg!(std::io::stdout().is_terminal());
const ASCII_0: char = '─';
const ASCII_1: char = '│';
@@ -7,193 +8,332 @@ const ASCII_3: char = '╰';
const ASCII_4: char = '╮';
const ASCII_7: char = '╯';
-#[derive(Debug)]
-pub struct GraphOptions {
- pub width: u64,
- pub height: u64,
- pub interpolate: bool,
- pub axis: bool,
+#[derive(Clone)]
+#[allow(dead_code)]
+enum GraphPixel<T> {
+ Normal(T),
+ Green(T),
+ Blue(T),
+ Red(T),
+ Blank,
}
-/// 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)
- .map(|i| y_values[(i as f64 * factor) as usize])
- .collect()
+impl<T> std::default::Default for GraphPixel<T> {
+ fn default() -> Self {
+ GraphPixel::Blank
+ }
}
-/// 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);
- 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_x + i as f64 * step;
- let mut j = 0;
- while j < x_values.len() - 1 && x_values[j + 1] < target_mark {
- 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);
+impl<T: std::fmt::Display> std::fmt::Display for GraphPixel<T> {
+ 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::Blue(c) => format!("\x1b[33m{}\x1b[0m", c),
+ GraphPixel::Red(c) => format!("\x1b[31m{}\x1b[0m", c),
+ GraphPixel::Blank => String::from(" "),
+ }
+ )
}
+}
- interpolated_data
+/// Available options for how the graph should look
+#[derive(Clone)]
+pub enum GraphType {
+ /// Use only * symbols
+ Star,
+ /// Use pretty characters from the ascii range
+ Ascii,
}
-/// 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);
- let scale_factor = (row_count - 1) as f64 / (max_value - min_value);
- values
- .iter()
- .map(|&y| ((y - min_value) * scale_factor).round() as usize)
- .collect()
+impl std::default::Default for GraphType {
+ fn default() -> Self {
+ GraphType::Star
+ }
}
-/// 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],
- 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())
+/// Temporary variables used while building a graph
+#[allow(dead_code)]
+pub struct GraphBuilder {
+ /// A array of pixels, this will ultimately be turned to a string, is initialized to width * height
+ elements: Vec<GraphPixel<char>>,
+ /// 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,
+ /// The values of the x-axis of the graph
+ x_values: Vec<f64>,
+ /// The values of the y-axis of the graph
+ y_values: Vec<f64>,
+ /// Decides whether axis will be drawn on the resulting graph
+ enable_axis: bool,
+ /// Which GraphType to use when the graph is drawn
+ graph_type: GraphType,
+}
+
+impl GraphBuilder {
+ /// 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(x_values: &[f64], y_values: &[f64], width: usize, height: usize) -> Self {
+ GraphBuilder {
+ elements: vec![GraphPixel::default(); width * height],
+ width,
+ height,
+ draw_width: width,
+ draw_height: height,
+ col_offset: 0,
+ row_offset: 0,
+ x_values: x_values.to_vec(),
+ y_values: y_values.to_vec(),
+ enable_axis: false,
+ graph_type: GraphType::default(),
+ }
+ }
+
+ /// Enable or disable axis in output
+ pub fn axis(&mut self, enable_axis: bool) -> &Self {
+ self.enable_axis = enable_axis;
+ self
+ }
+
+ /// Set graph type
+ pub fn graph_type(&mut self, graph_type: GraphType) -> &Self {
+ self.graph_type = graph_type;
+ self
+ }
+
+ /// Build the actual graph,
+ /// this is potentially a heavy operation, and it will mutate &self!
+ /// If you want to only see the "current state", you should clone first!
+ pub fn build(&mut self) -> String {
+ //let min_x = self.x_values.iter().cloned().fold(f64::INFINITY, f64::min);
+ //let max_x = self
+ // .x_values
+ // .iter()
+ // .cloned()
+ // .fold(f64::NEG_INFINITY, f64::max);
+ let min_y = self.y_values.iter().cloned().fold(f64::INFINITY, f64::min);
+ let max_y = self
+ .y_values
+ .iter()
+ .cloned()
+ .fold(f64::NEG_INFINITY, f64::max);
+
+ if self.enable_axis {
+ self.draw_axis(
+ GraphPixel::Normal(ASCII_1),
+ GraphPixel::Normal(ASCII_0),
+ GraphPixel::Normal('└'),
+ GraphPixel::Normal('┌'),
+ GraphPixel::Normal('┘'),
+ GraphPixel::Normal('┐'),
+ );
+ }
+
+ if true {
+ // && x_values.windows(2).all(|w| w[1] - w[0] == w[0] - w[1]) {
+ if self.y_values.len() >= self.draw_width {
+ // Downsample using a common downsampling, this allows us to avoid doing anything
+ // with the x values
+
+ let factor = self.y_values.len() as f64 / self.draw_width as f64;
+ let mut new_values = Vec::with_capacity(self.draw_width);
+ for i in 0..self.draw_width {
+ let new_value = self.y_values[(i as f64 * factor) as usize];
+ new_values.push(new_value);
+ }
+ self.y_values = new_values;
+ }
} else {
- y_values.to_vec()
+ // If the sample size is not consistent, we should interpolate
+ todo!("interpolation is not implemented");
+ //interpolate(&y_values, &x_values, graph.width())
+ };
+
+ // Scale the data
+ let scale_factor = (self.draw_height - 1) as f64 / (max_y - min_y);
+ for i in 0..self.y_values.len() {
+ self.y_values[i] = ((self.y_values[i] - min_y) * scale_factor).round();
}
- } else {
- interpolate(&y_values, &x_values, graph.width())
- };
- let scaled_data = scale(&y_values, graph.height());
- scaled_data
-}
+ match self.graph_type {
+ GraphType::Star => self.draw_star(),
+ GraphType::Ascii => self.draw_ascii(),
+ }
-/// 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 {
- graph.axis(
- GraphPixel::Normal(ASCII_1),
- GraphPixel::Normal(ASCII_0),
- GraphPixel::Normal('└'),
- GraphPixel::Normal('┌'),
- GraphPixel::Normal('┘'),
- GraphPixel::Normal('┐'),
- );
+ self.to_string()
}
- 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('*');
+ /// 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() {
+ out.push_str(&px.to_string());
+ if (i + 1) % self.width == 0 && i < (self.height * self.width - 1) {
+ out.push('\n');
+ }
+ }
+ out
}
- graph.to_string()
-}
+ /// 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
+ fn draw_exact(&mut self, x: usize, y: usize, px: GraphPixel<char>) {
+ let pos = y * self.width + x;
+ self.elements[pos] = px;
+ }
-/// 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,
- 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('┐'),
- );
+ /// Set a pixel in the drawable part of the canvas
+ ///
+ /// # Argument
+ ///
+ /// * `x` - Relative X-position of pixel
+ /// * `y` - Relative Y-position of pixel
+ /// * `px` - The pixel to set
+ fn draw(&mut self, x: usize, y: usize, px: GraphPixel<char>) {
+ let pos = (y + self.row_offset) * self.width + (x + self.col_offset);
+ self.elements[pos] = px;
}
- 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('┤'),
- );
+ /// 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
+ fn draw_axis(
+ &mut self,
+ c1: GraphPixel<char>,
+ c2: GraphPixel<char>,
+ c3: GraphPixel<char>,
+ c4: GraphPixel<char>,
+ c5: GraphPixel<char>,
+ c6: GraphPixel<char>,
+ ) {
+ 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;
}
- 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);
- }
+ /// Draw a graph using * for the pixels of the graph
+ fn draw_star(&mut self) {
+ for i in 0..self.y_values.len() {
+ let y = self.draw_height - (self.y_values[i] as usize) - 1;
+ self.draw(i, y, GraphPixel::Normal('*'));
}
}
- graph.to_string()
+ /// Draw a graph using somewhat pretty ascii characters for pixels of the graph
+ pub fn draw_ascii(&mut self) {
+ if self.enable_axis {
+ self.draw_exact(0, self.draw_height - self.y_values[0] as usize, GraphPixel::Green('├'));
+ self.draw_exact(
+ self.width - 1,
+ self.height - self.y_values[self.y_values.len() - 1] as usize,
+ GraphPixel::Green('┤'),
+ );
+ }
+ for i in 0..self.y_values.len() {
+ let y1 = self.draw_height - (self.y_values[i] as usize) - 1;
+ let y2 = if i < self.y_values.len() - 1 {
+ self.draw_height - (self.y_values[i + 1] as usize) - 1
+ } else {
+ y1
+ };
+
+ if y1 == y2 {
+ self.draw(i, y1, GraphPixel::Green(ASCII_0));
+ } else if y1 > y2 {
+ self.draw(i, y1, GraphPixel::Green(ASCII_7));
+ self.draw(i, y2, GraphPixel::Green(ASCII_2));
+ for j in (y2 + 1)..y1 {
+ self.draw(i, j, GraphPixel::Green(ASCII_1));
+ }
+ } else {
+ self.draw(i, y1, GraphPixel::Green(ASCII_4));
+ self.draw(i, y2, GraphPixel::Green(ASCII_3));
+ for j in (y1 + 1)..y2 {
+ self.draw(i, j, GraphPixel::Green(ASCII_1));
+ }
+ }
+ }
+ }
}
+// /// 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);
+// 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_x + i as f64 * step;
+// let mut j = 0;
+// while j < x_values.len() - 1 && x_values[j + 1] < target_mark {
+// 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
+// }
+
//const _BRAILLE_1: char = '⣿';
//const BRAILLE_1_0: char = '⡀';
//const BRAILLE_1_1: char = '⣀';