scrin 0.1.81

A terminal UI toolkit with panes, widgets, overlays, animations, and Aisling-powered effects/loaders.
Documentation
use crate::core::buffer::Buffer;
use crate::core::color::Color;
use crate::core::rect::Rect;
use crate::widgets::Widget;

#[derive(Debug, Clone, Copy, PartialEq)]
pub enum AxisPosition {
    Left,
    Right,
    Bottom,
    Top,
}

#[derive(Debug, Clone)]
pub struct Axis {
    pub title: String,
    pub labels: Vec<String>,
    pub position: AxisPosition,
}

impl Axis {
    pub fn new(position: AxisPosition) -> Self {
        Self {
            title: String::new(),
            labels: Vec::new(),
            position,
        }
    }

    pub fn with_title(mut self, title: &str) -> Self {
        self.title = title.to_string();
        self
    }

    pub fn with_labels(mut self, labels: Vec<String>) -> Self {
        self.labels = labels;
        self
    }
}

#[derive(Debug, Clone)]
pub struct Dataset {
    pub name: String,
    pub color: Color,
    pub data: Vec<(f64, f64)>,
    pub marker: char,
}

impl Dataset {
    pub fn new(name: &str, data: Vec<(f64, f64)>) -> Self {
        Self {
            name: name.to_string(),
            color: Color::rgb(88, 166, 255),
            data,
            marker: '',
        }
    }

    pub fn with_color(mut self, color: Color) -> Self {
        self.color = color;
        self
    }
}

#[derive(Debug, Clone)]
pub struct Chart {
    pub datasets: Vec<Dataset>,
    pub x_axis: Axis,
    pub y_axis: Axis,
    pub labels: Vec<String>,
    pub graph_area: Option<Rect>,
}

impl Chart {
    pub fn new(datasets: Vec<Dataset>) -> Self {
        Self {
            datasets,
            x_axis: Axis::new(AxisPosition::Bottom),
            y_axis: Axis::new(AxisPosition::Left),
            labels: Vec::new(),
            graph_area: None,
        }
    }

    pub fn with_x_axis(mut self, axis: Axis) -> Self {
        self.x_axis = axis;
        self
    }

    pub fn with_y_axis(mut self, axis: Axis) -> Self {
        self.y_axis = axis;
        self
    }

    fn render_axes(&self, buffer: &mut Buffer, area: Rect) {
        let axis_color = Color::rgb(48, 54, 61);

        match self.y_axis.position {
            AxisPosition::Left => {
                for y in (area.y + 1) as usize..(area.bottom() - 1) as usize {
                    buffer.set(
                        area.x as usize,
                        y,
                        crate::core::buffer::Cell {
                            ch: '',
                            fg: axis_color,
                            bg: None,
                            bold: false,
                            italic: false,
                            underlined: false,
                        },
                    );
                }
            }
            AxisPosition::Right => {
                for y in (area.y + 1) as usize..(area.bottom() - 1) as usize {
                    buffer.set(
                        (area.right() - 1) as usize,
                        y,
                        crate::core::buffer::Cell {
                            ch: '',
                            fg: axis_color,
                            bg: None,
                            bold: false,
                            italic: false,
                            underlined: false,
                        },
                    );
                }
            }
            _ => {}
        }

        match self.x_axis.position {
            AxisPosition::Bottom => {
                for x in (area.x + 1) as usize..(area.right() - 1) as usize {
                    buffer.set(
                        x,
                        (area.bottom() - 1) as usize,
                        crate::core::buffer::Cell {
                            ch: '',
                            fg: axis_color,
                            bg: None,
                            bold: false,
                            italic: false,
                            underlined: false,
                        },
                    );
                }
            }
            AxisPosition::Top => {
                for x in (area.x + 1) as usize..(area.right() - 1) as usize {
                    buffer.set(
                        x,
                        area.y as usize,
                        crate::core::buffer::Cell {
                            ch: '',
                            fg: axis_color,
                            bg: None,
                            bold: false,
                            italic: false,
                            underlined: false,
                        },
                    );
                }
            }
            _ => {}
        }

        buffer.set(
            area.x as usize,
            (area.bottom() - 1) as usize,
            crate::core::buffer::Cell {
                ch: '',
                fg: axis_color,
                bg: None,
                bold: false,
                italic: false,
                underlined: false,
            },
        );
    }

    fn render_points(&self, buffer: &mut Buffer, area: Rect) {
        if self.datasets.is_empty() || area.width < 4 || area.height < 4 {
            return;
        }

        let inner = Rect::new(area.x + 1, area.y + 1, area.width - 2, area.height - 2);

        let all_x: Vec<f64> = self
            .datasets
            .iter()
            .flat_map(|d| d.data.iter().map(|(x, _)| *x))
            .collect();
        let all_y: Vec<f64> = self
            .datasets
            .iter()
            .flat_map(|d| d.data.iter().map(|(_, y)| *y))
            .collect();
        let x_min = all_x.iter().copied().fold(f64::INFINITY, f64::min);
        let x_max = all_x.iter().copied().fold(f64::NEG_INFINITY, f64::max);
        let y_min = all_y.iter().copied().fold(f64::INFINITY, f64::min);
        let y_max = all_y.iter().copied().fold(f64::NEG_INFINITY, f64::max);
        let x_range = (x_max - x_min).max(1.0);
        let y_range = (y_max - y_min).max(1.0);

        for dataset in &self.datasets {
            for &(x, y) in &dataset.data {
                let nx = ((x - x_min) / x_range * (inner.width as f64 - 1.0)) as u16;
                let ny = ((1.0 - (y - y_min) / y_range) * (inner.height as f64 - 1.0)) as u16;
                let px = (inner.x + nx) as usize;
                let py = (inner.y + ny) as usize;
                if px < inner.right() as usize && py < inner.bottom() as usize {
                    buffer.set(
                        px,
                        py,
                        crate::core::buffer::Cell {
                            ch: dataset.marker,
                            fg: dataset.color,
                            bg: None,
                            bold: true,
                            italic: false,
                            underlined: false,
                        },
                    );
                }
            }
        }
    }
}

impl Widget for Chart {
    fn render(&self, buffer: &mut Buffer, area: Rect) {
        self.render_axes(buffer, area);
        self.render_points(buffer, area);
    }
}