Skip to main content

scrin/widgets/
chart.rs

1use crate::core::buffer::Buffer;
2use crate::core::color::Color;
3use crate::core::rect::Rect;
4use crate::widgets::Widget;
5
6#[derive(Debug, Clone, Copy, PartialEq)]
7pub enum AxisPosition {
8    Left,
9    Right,
10    Bottom,
11    Top,
12}
13
14#[derive(Debug, Clone)]
15pub struct Axis {
16    pub title: String,
17    pub labels: Vec<String>,
18    pub position: AxisPosition,
19}
20
21impl Axis {
22    pub fn new(position: AxisPosition) -> Self {
23        Self {
24            title: String::new(),
25            labels: Vec::new(),
26            position,
27        }
28    }
29
30    pub fn with_title(mut self, title: &str) -> Self {
31        self.title = title.to_string();
32        self
33    }
34
35    pub fn with_labels(mut self, labels: Vec<String>) -> Self {
36        self.labels = labels;
37        self
38    }
39}
40
41#[derive(Debug, Clone)]
42pub struct Dataset {
43    pub name: String,
44    pub color: Color,
45    pub data: Vec<(f64, f64)>,
46    pub marker: char,
47}
48
49impl Dataset {
50    pub fn new(name: &str, data: Vec<(f64, f64)>) -> Self {
51        Self {
52            name: name.to_string(),
53            color: Color::rgb(88, 166, 255),
54            data,
55            marker: '●',
56        }
57    }
58
59    pub fn with_color(mut self, color: Color) -> Self {
60        self.color = color;
61        self
62    }
63}
64
65#[derive(Debug, Clone)]
66pub struct Chart {
67    pub datasets: Vec<Dataset>,
68    pub x_axis: Axis,
69    pub y_axis: Axis,
70    pub labels: Vec<String>,
71    pub graph_area: Option<Rect>,
72}
73
74impl Chart {
75    pub fn new(datasets: Vec<Dataset>) -> Self {
76        Self {
77            datasets,
78            x_axis: Axis::new(AxisPosition::Bottom),
79            y_axis: Axis::new(AxisPosition::Left),
80            labels: Vec::new(),
81            graph_area: None,
82        }
83    }
84
85    pub fn with_x_axis(mut self, axis: Axis) -> Self {
86        self.x_axis = axis;
87        self
88    }
89
90    pub fn with_y_axis(mut self, axis: Axis) -> Self {
91        self.y_axis = axis;
92        self
93    }
94
95    fn render_axes(&self, buffer: &mut Buffer, area: Rect) {
96        let axis_color = Color::rgb(48, 54, 61);
97
98        match self.y_axis.position {
99            AxisPosition::Left => {
100                for y in (area.y + 1) as usize..(area.bottom() - 1) as usize {
101                    buffer.set(
102                        area.x as usize,
103                        y,
104                        crate::core::buffer::Cell {
105                            ch: '│',
106                            fg: axis_color,
107                            bg: None,
108                            bold: false,
109                            italic: false,
110                            underlined: false,
111                        },
112                    );
113                }
114            }
115            AxisPosition::Right => {
116                for y in (area.y + 1) as usize..(area.bottom() - 1) as usize {
117                    buffer.set(
118                        (area.right() - 1) as usize,
119                        y,
120                        crate::core::buffer::Cell {
121                            ch: '│',
122                            fg: axis_color,
123                            bg: None,
124                            bold: false,
125                            italic: false,
126                            underlined: false,
127                        },
128                    );
129                }
130            }
131            _ => {}
132        }
133
134        match self.x_axis.position {
135            AxisPosition::Bottom => {
136                for x in (area.x + 1) as usize..(area.right() - 1) as usize {
137                    buffer.set(
138                        x,
139                        (area.bottom() - 1) as usize,
140                        crate::core::buffer::Cell {
141                            ch: '─',
142                            fg: axis_color,
143                            bg: None,
144                            bold: false,
145                            italic: false,
146                            underlined: false,
147                        },
148                    );
149                }
150            }
151            AxisPosition::Top => {
152                for x in (area.x + 1) as usize..(area.right() - 1) as usize {
153                    buffer.set(
154                        x,
155                        area.y as usize,
156                        crate::core::buffer::Cell {
157                            ch: '─',
158                            fg: axis_color,
159                            bg: None,
160                            bold: false,
161                            italic: false,
162                            underlined: false,
163                        },
164                    );
165                }
166            }
167            _ => {}
168        }
169
170        buffer.set(
171            area.x as usize,
172            (area.bottom() - 1) as usize,
173            crate::core::buffer::Cell {
174                ch: '└',
175                fg: axis_color,
176                bg: None,
177                bold: false,
178                italic: false,
179                underlined: false,
180            },
181        );
182    }
183
184    fn render_points(&self, buffer: &mut Buffer, area: Rect) {
185        if self.datasets.is_empty() || area.width < 4 || area.height < 4 {
186            return;
187        }
188
189        let inner = Rect::new(area.x + 1, area.y + 1, area.width - 2, area.height - 2);
190
191        let all_x: Vec<f64> = self
192            .datasets
193            .iter()
194            .flat_map(|d| d.data.iter().map(|(x, _)| *x))
195            .collect();
196        let all_y: Vec<f64> = self
197            .datasets
198            .iter()
199            .flat_map(|d| d.data.iter().map(|(_, y)| *y))
200            .collect();
201        let x_min = all_x.iter().copied().fold(f64::INFINITY, f64::min);
202        let x_max = all_x.iter().copied().fold(f64::NEG_INFINITY, f64::max);
203        let y_min = all_y.iter().copied().fold(f64::INFINITY, f64::min);
204        let y_max = all_y.iter().copied().fold(f64::NEG_INFINITY, f64::max);
205        let x_range = (x_max - x_min).max(1.0);
206        let y_range = (y_max - y_min).max(1.0);
207
208        for dataset in &self.datasets {
209            for &(x, y) in &dataset.data {
210                let nx = ((x - x_min) / x_range * (inner.width as f64 - 1.0)) as u16;
211                let ny = ((1.0 - (y - y_min) / y_range) * (inner.height as f64 - 1.0)) as u16;
212                let px = (inner.x + nx) as usize;
213                let py = (inner.y + ny) as usize;
214                if px < inner.right() as usize && py < inner.bottom() as usize {
215                    buffer.set(
216                        px,
217                        py,
218                        crate::core::buffer::Cell {
219                            ch: dataset.marker,
220                            fg: dataset.color,
221                            bg: None,
222                            bold: true,
223                            italic: false,
224                            underlined: false,
225                        },
226                    );
227                }
228            }
229        }
230    }
231}
232
233impl Widget for Chart {
234    fn render(&self, buffer: &mut Buffer, area: Rect) {
235        self.render_axes(buffer, area);
236        self.render_points(buffer, area);
237    }
238}