realtime_termgraph 0.2.1

A realtime plotting library that outputs a graph to the terminal
Documentation
#[allow(unused_imports)]
use crossterm::{
    cursor, terminal,
    terminal::{Clear, ClearType},
    ExecutableCommand, QueueableCommand,
};
use map_range::MapRange;
use std::{
    io::{stdout, Write},
    ops::RangeInclusive,
};

use super::Point;

#[derive(Clone)]
pub struct Canvas {
    dimensions: (u16, u16),
    x_bounds: RangeInclusive<f32>,
    y_bounds: RangeInclusive<f32>,
    start_row: u16,
    marker: char,
    border_char: char,
}

impl Default for Canvas {
    fn default() -> Self {
        Self {
            dimensions: (20, 10),
            x_bounds: -10.0..=10.0,
            y_bounds: -10.0..=10.0,
            start_row: cursor::position().unwrap_or((0, 0)).1,
            marker: '.',
            border_char: '#',
        }
    }
}

impl Drop for Canvas {
    fn drop(&mut self) {
        self.go_to_exit_pos();
        crossterm::execute!(stdout(), cursor::Show).unwrap();
    }
}

impl Canvas {
    pub fn new(rows: u16, columns: u16) -> Self {
        Self {
            dimensions: (rows, columns),
            x_bounds: -10.0..=10.0,
            y_bounds: -10.0..=10.0,
            ..Self::default()
        }
        .init()
    }

    pub fn custom(
        rows: u16,
        columns: u16,
        x_bounds: RangeInclusive<f32>,
        y_bounds: RangeInclusive<f32>,
        marker: char,
        border_char: char,
    ) -> Self {
        Self {
            dimensions: (rows, columns),
            x_bounds,
            y_bounds,
            marker,
            border_char,
            ..Self::default()
        }
        .init()
    }

    pub fn init(mut self) -> Self {
        Self::clear_rows(self.dimensions.1 + 4);
        crossterm::execute!(stdout(), cursor::MoveUp(self.dimensions.1 + 4)).unwrap();
        self.start_row = cursor::position().unwrap_or((0, 0)).1;
        self.print_border();

        self
    }

    pub fn style(mut self, marker: char, border_char: char) -> Self {
        self.marker = marker;
        self.border_char = border_char;
        self
    }

    pub fn x_bounds(mut self, bounds: RangeInclusive<f32>) -> Self {
        self.x_bounds = bounds;
        self
    }

    pub fn y_bounds(mut self, bounds: RangeInclusive<f32>) -> Self {
        self.y_bounds = bounds;
        self
    }

    pub fn bounds(self, x_bounds: RangeInclusive<f32>, y_bounds: RangeInclusive<f32>) -> Self {
        self.x_bounds(x_bounds).y_bounds(y_bounds)
    }

    pub fn plot_points(&self, points: &[Point]) {
        for point in points {
            self.plot_point(point);
        }
    }

    pub fn plot_point(&self, point: &Point) {
        if let Some((x, y)) = self.map_point(point) {
            self.char_at_position(y, x, self.marker);
        }
    }

    fn map_point(&self, point: &Point) -> Option<(u16, u16)> {
        let (x, y) = point;
        if self.x_bounds.contains(x) && self.y_bounds.contains(y) {
            let (xs, xe) = (self.x_bounds.start(), self.x_bounds.end());
            let (ys, ye) = (self.y_bounds.start(), self.y_bounds.end());

            let nx = x.map_range(*xs..*xe, 0.0..(self.dimensions.0 as f32));
            let ny = y.map_range(*ys..*ye, (self.dimensions.1 as f32)..0.0);

            return Some((nx.round() as u16, ny.round() as u16));
        }

        None
    }

    fn final_row(&self) -> u16 {
        self.start_row + self.dimensions.1 - 1
    }

    fn go_to_exit_pos(&self) {
        crossterm::execute!(stdout(), cursor::MoveTo(0, self.final_row() + 4)).unwrap();
    }

    fn clear_rows(count: u16) {
        (0..count).for_each(|_| println!());
    }

    pub fn print_border(&self) {
        let border_text = (0..=self.dimensions.0 + 2).map(|_| '#').collect::<String>();

        self.write_to_row(0, &border_text);
        self.write_to_row(self.dimensions.1 + 2, &border_text);

        for row in 1..=self.dimensions.1 + 1 {
            char_at_position(self.start_row + row, 0, self.border_char).unwrap();
            char_at_position(self.start_row + row, self.dimensions.0 + 2, '#').unwrap();
        }
        self.go_to_exit_pos();
    }

    fn write_to_row(&self, row: u16, text: &str) {
        write_to_row(self.start_row + row, text).unwrap();
        self.go_to_exit_pos();
    }

    fn char_at_position(&self, row: u16, column: u16, char: char) {
        char_at_position(self.start_row + row + 1, column + 1, char).unwrap();
        self.go_to_exit_pos();
    }
}

fn write_to_row(row: u16, text: &str) -> Result<(), std::io::Error> {
    let mut stdout = stdout();
    stdout.execute(cursor::Hide)?;

    stdout.queue(cursor::MoveTo(0, row))?;

    stdout.queue(Clear(ClearType::CurrentLine))?;
    stdout.write_all(text.as_bytes())?;

    stdout.flush()?;

    Ok(())
}

fn char_at_position(row: u16, column: u16, char: char) -> Result<(), std::io::Error> {
    let mut stdout = stdout();
    stdout.execute(cursor::Hide)?;

    stdout.queue(cursor::MoveTo(column, row))?;

    stdout.write_all(&[char as u8])?;

    stdout.flush()?;

    Ok(())
}

#[cfg(test)]
mod tests {
    #[allow(unused_imports)]
    use super::*;

    #[test]
    fn test_write() -> Result<(), std::io::Error> {
        let text: &str = "Hello from Grant!";
        write_to_row(4, text)?;
        char_at_position(3, 0, '.')?;

        Ok(())
    }

    #[test]
    fn test_map_point_default() {
        let canvas: Canvas = Canvas::default();
        let expected_maps = [
            ((0.0, 0.0), (10, 5)),
            ((-10.0, 0.0), (0, 5)),
            ((10.0, 0.0), (20, 5)),
            ((-10.0, 10.0), (0, 0)),
            ((10.0, -10.0), (20, 10)),
        ];

        for (point, map) in expected_maps {
            assert_eq!(canvas.map_point(&point), Some(map));
        }
    }

    #[test]
    fn test_map_point_dimensions() {
        let canvas: Canvas = Canvas::new(40, 10);
        let expected_maps = [
            ((0.0, 0.0), (20, 5)),
            ((-10.0, 0.0), (0, 5)),
            ((10.0, 0.0), (40, 5)),
            ((-10.0, 10.0), (0, 0)),
            ((10.0, -10.0), (40, 10)),
        ];

        for (point, map) in expected_maps {
            assert_eq!(canvas.map_point(&point), Some(map));
        }
    }

    #[test]
    fn test_map_point_range() {
        let canvas = Canvas::custom(20, 10, 0.0..=40.0, 0.0..=20.0, '+', '#');
        let expected_maps = [((0.0, 10.0), (0, 5)), ((10.0, 0.0), (5, 10))];

        for (point, map) in expected_maps {
            assert_eq!(canvas.map_point(&point), Some(map));
        }
    }
}