Skip to main content

termplot_rs/
charts.rs

1use crate::canvas::BrailleCanvas;
2use colored::Color;
3use std::f64::consts::PI;
4
5pub struct ChartContext {
6    pub canvas: BrailleCanvas,
7}
8
9impl ChartContext {
10    pub fn new(width: usize, height: usize) -> Self {
11        Self {
12            canvas: BrailleCanvas::new(width, height),
13        }
14    }
15
16    pub fn get_auto_range(points: &[(f64, f64)], padding: f64) -> ((f64, f64), (f64, f64)) {
17        let valid_points: Vec<&(f64, f64)> = points
18            .iter()
19            .filter(|(x, y)| x.is_finite() && y.is_finite())
20            .collect();
21
22        if valid_points.is_empty() {
23            return ((0.0, 1.0), (0.0, 1.0));
24        }
25
26        let (min_x, max_x) = valid_points.iter().fold(
27            (f64::INFINITY, f64::NEG_INFINITY),
28            |(min, max), p| (min.min(p.0), max.max(p.0)),
29        );
30
31        let (min_y, max_y) = valid_points.iter().fold(
32            (f64::INFINITY, f64::NEG_INFINITY),
33            |(min, max), p| (min.min(p.1), max.max(p.1)),
34        );
35
36        let rx = if (max_x - min_x).abs() < 1e-9 { 1.0 } else { max_x - min_x };
37        let ry = if (max_y - min_y).abs() < 1e-9 { 1.0 } else { max_y - min_y };
38
39        (
40            (min_x - rx * padding, max_x + rx * padding),
41            (min_y - ry * padding, max_y + ry * padding),
42        )
43    }
44
45    fn map_coords(&self, x: f64, y: f64, x_range: (f64, f64), y_range: (f64, f64)) -> (isize, isize) {
46        let (min_x, max_x) = x_range;
47        let (min_y, max_y) = y_range;
48        let width = self.canvas.pixel_width() as f64;
49        let height = self.canvas.pixel_height() as f64;
50        let range_x = (max_x - min_x).max(1e-9);
51        let range_y = (max_y - min_y).max(1e-9);
52
53        let px = ((x - min_x) / range_x * (width - 1.0)).round();
54        let py = ((y - min_y) / range_y * (height - 1.0)).round();
55
56        (px as isize, py as isize)
57    }
58
59    // --- GRÁFICOS ---
60
61    pub fn scatter(&mut self, points: &[(f64, f64)], color: Option<Color>) {
62        if points.is_empty() { return; }
63        let (x_range, y_range) = Self::get_auto_range(points, 0.05);
64        let w_px = self.canvas.pixel_width();
65        let h_px = self.canvas.pixel_height();
66
67        for &(x, y) in points {
68            if !x.is_finite() || !y.is_finite() { continue; }
69            let (px, py) = self.map_coords(x, y, x_range, y_range);
70            if px >= 0 && py >= 0 && (px as usize) < w_px && (py as usize) < h_px {
71                self.canvas.set_pixel(px as usize, py as usize, color);
72            }
73        }
74    }
75
76    pub fn line_chart(&mut self, points: &[(f64, f64)], color: Option<Color>) {
77        if points.len() < 2 { return; }
78        let (x_range, y_range) = Self::get_auto_range(points, 0.05);
79
80        for window in points.windows(2) {
81            let (x0, y0) = window[0];
82            let (x1, y1) = window[1];
83            if !x0.is_finite() || !y0.is_finite() || !x1.is_finite() || !y1.is_finite() { continue; }
84
85            let p0 = self.map_coords(x0, y0, x_range, y_range);
86            let p1 = self.map_coords(x1, y1, x_range, y_range);
87            self.canvas.line(p0.0, p0.1, p1.0, p1.1, color);
88        }
89    }
90
91    pub fn bar_chart(&mut self, values: &[(f64, Option<Color>)]) {
92        if values.is_empty() { return; }
93        let max_val = values.iter()
94            .filter_map(|(v, _)| if v.is_finite() { Some(*v) } else { None })
95            .fold(0.0f64, f64::max);
96
97        if max_val <= 1e-9 { return; }
98
99        let w_px = self.canvas.pixel_width();
100        let h_px = self.canvas.pixel_height();
101        let bar_width = (w_px / values.len()).max(1);
102
103        for (i, &(val, color)) in values.iter().enumerate() {
104            if !val.is_finite() || val <= 0.0 { continue; }
105            let normalized_h = (val / max_val * (h_px as f64)).round();
106            let bar_height = (normalized_h as usize).min(h_px);
107            let x_start = i * bar_width;
108            let x_end = (x_start + bar_width).min(w_px);
109            if x_start >= w_px { break; }
110
111            for x in x_start..x_end {
112                self.canvas.line(x as isize, 0, x as isize, bar_height as isize, color);
113            }
114        }
115    }
116
117    pub fn polygon(&mut self, vertices: &[(f64, f64)], color: Option<Color>) {
118        if vertices.len() < 2 { return; }
119        let (x_range, y_range) = Self::get_auto_range(vertices, 0.05);
120
121        for i in 0..vertices.len() {
122            let (x0, y0) = vertices[i];
123            let (x1, y1) = vertices[(i + 1) % vertices.len()];
124            if !x0.is_finite() || !y0.is_finite() || !x1.is_finite() || !y1.is_finite() { continue; }
125            let p0 = self.map_coords(x0, y0, x_range, y_range);
126            let p1 = self.map_coords(x1, y1, x_range, y_range);
127            self.canvas.line(p0.0, p0.1, p1.0, p1.1, color);
128        }
129    }
130
131    pub fn pie_chart(&mut self, slices: &[(f64, Option<Color>)]) {
132        let total: f64 = slices.iter()
133            .filter_map(|(v, _)| if v.is_finite() && *v > 0.0 { Some(*v) } else { None })
134            .sum();
135        if total <= 1e-9 { return; }
136
137        let w_px = self.canvas.pixel_width() as isize;
138        let h_px = self.canvas.pixel_height() as isize;
139        let cx = w_px / 2;
140        let cy = h_px / 2;
141        let radius = (w_px.min(h_px) as f64 / 2.0 * 0.95) as isize;
142        let mut current_angle = 0.0;
143
144        for (value, color) in slices {
145            if !value.is_finite() || *value <= 0.0 { continue; }
146            let slice_angle = (value / total) * 2.0 * PI;
147            let end_angle = current_angle + slice_angle;
148
149            let end_x = cx + (radius as f64 * end_angle.cos()) as isize;
150            let end_y = cy + (radius as f64 * end_angle.sin()) as isize;
151
152            self.canvas.line(cx, cy, end_x, end_y, *color);
153            current_angle = end_angle;
154        }
155    }
156
157    pub fn draw_circle(&mut self, center: (f64, f64), radius_norm: f64, color: Option<Color>) {
158        let w_px = self.canvas.pixel_width() as f64;
159        let h_px = self.canvas.pixel_height() as f64;
160        let min_dim = w_px.min(h_px);
161
162        let r_px = (radius_norm * min_dim) as isize;
163        let cx_px = (center.0 * (w_px - 1.0)) as isize;
164        let cy_px = (center.1 * (h_px - 1.0)) as isize;
165
166        self.canvas.circle(cx_px, cy_px, r_px, color);
167    }
168
169    pub fn plot_function<F>(&mut self, func: F, min_x: f64, max_x: f64, color: Option<Color>)
170    where
171        F: Fn(f64) -> f64,
172    {
173        let steps = self.canvas.pixel_width();
174        let mut points = Vec::with_capacity(steps);
175
176        for i in 0..=steps {
177            let t = i as f64 / steps as f64;
178            let x = min_x + t * (max_x - min_x);
179            let y = func(x);
180            if y.is_finite() {
181                points.push((x, y));
182            }
183        }
184        self.line_chart(&points, color);
185    }
186
187    // --- UTILIDADES ---
188
189    pub fn text(&mut self, text: &str, x_norm: f64, y_norm: f64, color: Option<Color>) {
190        let w = self.canvas.width;
191        let h = self.canvas.height;
192        let cx = (x_norm * (w.saturating_sub(1)) as f64).round() as usize;
193        let cy = (y_norm * (h.saturating_sub(1)) as f64).round() as usize;
194
195        for (i, ch) in text.chars().enumerate() {
196            if cx + i >= w { break; }
197            self.canvas.set_char(cx + i, cy, ch, color);
198        }
199    }
200
201    /// Dibuja los ejes calculando "ticks" intermedios de forma automática.
202    pub fn draw_axes(&mut self, x_range: (f64, f64), y_range: (f64, f64), color: Option<Color>) {
203        let w_px = self.canvas.pixel_width() as isize;
204        let h_px = self.canvas.pixel_height() as isize;
205
206        self.canvas.line(0, 0, 0, h_px - 1, color);
207        self.canvas.line(0, 0, w_px - 1, 0, color);
208
209        // helper para generar ~4 marcas equidistantes
210        let draw_ticks = |min: f64, max: f64| -> Vec<f64> {
211            let step = (max - min) / 3.0; // 4 marcas incluyendo bordes
212            vec![min, min + step, min + step * 2.0, max]
213        };
214
215        // Y Ticks
216        let y_ticks = draw_ticks(y_range.0, y_range.1);
217        for (i, &val) in y_ticks.iter().enumerate() {
218            let norm_y = i as f64 / (y_ticks.len() - 1) as f64;
219            self.text(&format!("{:.1}", val), 0.0, norm_y, color);
220        }
221
222        // X Ticks
223        let x_ticks = draw_ticks(x_range.0, x_range.1);
224        for (i, &val) in x_ticks.iter().enumerate() {
225            let norm_x = i as f64 / (x_ticks.len() - 1) as f64;
226            // Desplazamos un poco el primero y último para que no se corten
227            let safe_x = norm_x.clamp(0.05, 0.90);
228            self.text(&format!("{:.1}", val), safe_x, 0.0, color);
229        }
230    }
231
232    pub fn draw_grid(&mut self, divs_x: usize, divs_y: usize, color: Option<Color>) {
233         let w_px = self.canvas.pixel_width() as isize;
234         let h_px = self.canvas.pixel_height() as isize;
235
236         for i in 1..divs_x {
237             let x = (i as f64 / divs_x as f64 * (w_px as f64)).round() as isize;
238             self.canvas.line(x, 0, x, h_px, color);
239         }
240
241         for i in 1..divs_y {
242             let y = (i as f64 / divs_y as f64 * (h_px as f64)).round() as isize;
243             self.canvas.line(0, y, w_px, y, color);
244         }
245    }
246}