Skip to main content

termplot_rs/
charts.rs

1// src/charts.rs
2use crate::canvas::BrailleCanvas;
3use colored::Color;
4use std::f64::consts::PI;
5
6pub struct ChartOptions {
7    pub padding: f64,
8    pub clamp_min: Option<f64>,
9    pub clamp_max: Option<f64>,
10}
11
12impl Default for ChartOptions {
13    fn default() -> Self {
14        Self {
15            padding: 0.1,
16            clamp_min: None,
17            clamp_max: None,
18        }
19    }
20}
21
22pub struct ChartContext {
23    pub canvas: BrailleCanvas,
24}
25
26impl ChartContext {
27    pub fn new(width: usize, height: usize) -> Self {
28        Self {
29            canvas: BrailleCanvas::new(width, height),
30        }
31    }
32
33    pub fn get_auto_range(points: &[(f64, f64)], padding: f64) -> ((f64, f64), (f64, f64)) {
34        if points.is_empty() {
35            return ((0.0, 1.0), (0.0, 1.0));
36        }
37
38        let (min_x, max_x) = points
39            .iter()
40            .fold((f64::INFINITY, f64::NEG_INFINITY), |(min, max), p| {
41                (min.min(p.0), max.max(p.0))
42            });
43        let (min_y, max_y) = points
44            .iter()
45            .fold((f64::INFINITY, f64::NEG_INFINITY), |(min, max), p| {
46                (min.min(p.1), max.max(p.1))
47            });
48
49        let rx = if (max_x - min_x).abs() < 1e-9 {
50            1.0
51        } else {
52            max_x - min_x
53        };
54        let ry = if (max_y - min_y).abs() < 1e-9 {
55            1.0
56        } else {
57            max_y - min_y
58        };
59
60        (
61            (min_x - rx * padding, max_x + rx * padding),
62            (min_y - ry * padding, max_y + ry * padding),
63        )
64    }
65
66    // Helper para obtener dimensiones en píxeles
67    fn get_px_dims(&self) -> (f64, f64) {
68        (
69            (self.canvas.width * 2) as f64,
70            (self.canvas.height * 4) as f64,
71        )
72    }
73
74    /// Nube de puntos con color opcional
75    pub fn scatter(&mut self, points: &[(f64, f64)], color: Option<Color>) {
76        if points.is_empty() {
77            return;
78        }
79
80        let (min_x, max_x) = points
81            .iter()
82            .fold((f64::INFINITY, f64::NEG_INFINITY), |(min, max), p| {
83                (min.min(p.0), max.max(p.0))
84            });
85        let (min_y, max_y) = points
86            .iter()
87            .fold((f64::INFINITY, f64::NEG_INFINITY), |(min, max), p| {
88                (min.min(p.1), max.max(p.1))
89            });
90
91        let range_x = if (max_x - min_x).abs() < 1e-9 {
92            1.0
93        } else {
94            max_x - min_x
95        };
96        let range_y = if (max_y - min_y).abs() < 1e-9 {
97            1.0
98        } else {
99            max_y - min_y
100        };
101        let (w_px, h_px) = self.get_px_dims();
102
103        for &(x, y) in points {
104            let px = ((x - min_x) / range_x * (w_px - 1.0)) as usize;
105            let py = ((y - min_y) / range_y * (h_px - 1.0)) as usize;
106            self.canvas.set_pixel(px, py, color);
107        }
108    }
109
110    /// Gráfico de barras con colores por barra opcionales
111    pub fn bar_chart(&mut self, values: &[(f64, Option<Color>)]) {
112        if values.is_empty() {
113            return;
114        }
115        let max_val = values
116            .iter()
117            .map(|(v, _)| *v)
118            .fold(f64::NEG_INFINITY, f64::max);
119        let w_px = self.canvas.width * 2;
120        let h_px = self.canvas.height * 4;
121
122        // Si hay más barras que píxeles, forzamos un ancho mínimo de 1
123        let bar_width = (w_px / values.len()).max(1);
124
125        for (i, &(val, color)) in values.iter().enumerate() {
126            let bar_height = ((val / max_val) * (h_px as f64)).round() as usize;
127            let x_start = i * bar_width;
128            let x_end = (x_start + bar_width).min(w_px); // Evita desbordamiento horizontal
129
130            if x_start >= w_px {
131                break;
132            } // No dibujar fuera del canvas
133
134            for x in x_start..x_end {
135                for y in 0..bar_height.min(h_px) {
136                    self.canvas.set_pixel(x, y, color);
137                }
138            }
139        }
140    }
141
142    /// Polígono con color opcional
143    pub fn polygon(&mut self, vertices: &[(f64, f64)], color: Option<Color>) {
144        if vertices.len() < 2 {
145            return;
146        }
147        let (w_px, h_px) = self.get_px_dims();
148        let map = |v: (f64, f64)| -> (isize, isize) {
149            ((v.0 * (w_px - 1.0)) as isize, (v.1 * (h_px - 1.0)) as isize)
150        };
151
152        for i in 0..vertices.len() {
153            let p1 = map(vertices[i]);
154            let p2 = map(vertices[(i + 1) % vertices.len()]);
155            self.canvas.line(p1.0, p1.1, p2.0, p2.1, color);
156        }
157    }
158
159    /// Dibuja un círculo en coordenadas normalizadas (0.0-1.0)
160    pub fn draw_circle(&mut self, center: (f64, f64), radius_norm: f64, color: Option<Color>) {
161        let (w_px, h_px) = self.get_px_dims();
162        let min_dim = w_px.min(h_px);
163
164        let r_px = (radius_norm * min_dim) as isize;
165        let cx_px = (center.0 * (w_px - 1.0)) as isize;
166        let cy_px = (center.1 * (h_px - 1.0)) as isize;
167
168        self.canvas.circle(cx_px, cy_px, r_px, color);
169    }
170
171    /// Gráfico de Pastel (Estilo Radar/Radios)
172    pub fn pie_chart(&mut self, slices: &[(f64, Option<Color>)]) {
173        let total: f64 = slices.iter().map(|(v, _)| v).sum();
174        if total <= 0.0 {
175            return;
176        }
177
178        let (w_px, h_px) = self.get_px_dims();
179        let cx = (w_px / 2.0) as isize;
180        let cy = (h_px / 2.0) as isize;
181        let radius = w_px.min(h_px) / 2.0 * 0.9;
182
183        let mut current_angle = 0.0;
184
185        for (value, color) in slices {
186            let slice_angle = (value / total) * 2.0 * PI;
187            let end_angle = current_angle + slice_angle;
188
189            let end_x = cx + (radius * end_angle.cos()) as isize;
190            let end_y = cy + (radius * end_angle.sin()) as isize;
191
192            self.canvas.line(cx, cy, end_x, end_y, *color);
193
194            current_angle = end_angle;
195        }
196        self.canvas.circle(cx, cy, radius as isize, None);
197    }
198
199    /// Escribe texto en coordenadas del gráfico (0.0 - 1.0)
200    pub fn text(&mut self, text: &str, x_norm: f64, y_norm: f64, color: Option<Color>) {
201        // Clamping para asegurar que el inicio esté dentro del canvas
202        let cx = (x_norm * (self.canvas.width.saturating_sub(1)) as f64).round() as usize;
203        let cy = (y_norm * (self.canvas.height.saturating_sub(1)) as f64).round() as usize;
204
205        for (i, ch) in text.chars().enumerate() {
206            let x = cx + i;
207            if x >= self.canvas.width || cy >= self.canvas.height {
208                break;
209            } // Límite de seguridad
210            self.canvas.set_char(x, cy, ch, color);
211        }
212    }
213    /// Gráfico de Línea (conecta puntos ordenados)
214    pub fn line_chart(&mut self, points: &[(f64, f64)], color: Option<Color>) {
215        if points.len() < 2 {
216            return;
217        }
218
219        let (min_x, max_x) = points
220            .iter()
221            .fold((f64::INFINITY, f64::NEG_INFINITY), |(min, max), p| {
222                (min.min(p.0), max.max(p.0))
223            });
224        let (min_y, max_y) = points
225            .iter()
226            .fold((f64::INFINITY, f64::NEG_INFINITY), |(min, max), p| {
227                (min.min(p.1), max.max(p.1))
228            });
229
230        let range_x = if (max_x - min_x).abs() < 1e-9 {
231            1.0
232        } else {
233            max_x - min_x
234        };
235        let range_y = if (max_y - min_y).abs() < 1e-9 {
236            1.0
237        } else {
238            max_y - min_y
239        };
240
241        let (w_px, h_px) = self.get_px_dims();
242
243        let map = |p: (f64, f64)| -> (isize, isize) {
244            let px = ((p.0 - min_x) / range_x * (w_px - 1.0)) as isize;
245            let py = ((p.1 - min_y) / range_y * (h_px - 1.0)) as isize;
246            (px, py)
247        };
248
249        for window in points.windows(2) {
250            let p0 = map(window[0]);
251            let p1 = map(window[1]);
252            self.canvas.line(p0.0, p0.1, p1.0, p1.1, color);
253        }
254    }
255
256    // --- Dibuja una rejilla de fondo ---
257    pub fn draw_grid(&mut self, divs_x: usize, divs_y: usize, color: Option<Color>) {
258        let (w_px, h_px) = self.get_px_dims();
259
260        // Líneas Verticales
261        for i in 1..divs_x {
262            let x = (i as f64 / divs_x as f64 * (w_px - 1.0)).round() as isize;
263            self.canvas.line(x, 0, x, h_px as isize - 1, color);
264        }
265
266        // Líneas Horizontales
267        for i in 1..divs_y {
268            let y = (i as f64 / divs_y as f64 * (h_px - 1.0)).round() as isize;
269            self.canvas.line(0, y, w_px as isize - 1, y, color);
270        }
271    }
272
273    // Plotea una función matemática directamente ---
274    // Acepta una clausura (closure) como |x| x.sin()
275    pub fn plot_function<F>(&mut self, func: F, min_x: f64, max_x: f64, color: Option<Color>)
276    where
277        F: Fn(f64) -> f64,
278    {
279        // Resolución: 1 punto por cada píxel horizontal del canvas
280        let steps = self.canvas.width * 2;
281        let mut points = Vec::with_capacity(steps);
282
283        for i in 0..=steps {
284            let t = i as f64 / steps as f64;
285            let x = min_x + t * (max_x - min_x);
286            let y = func(x);
287            // Solo añadimos si es un número válido
288            if y.is_finite() {
289                points.push((x, y));
290            }
291        }
292        self.line_chart(&points, color);
293    }
294
295    /// Dibuja un marco alrededor del gráfico con etiquetas de rango min/max
296    pub fn draw_axes(&mut self, x_range: (f64, f64), y_range: (f64, f64), color: Option<Color>) {
297        let (w_px, h_px) = self.get_px_dims();
298        self.canvas.line(0, 0, 0, h_px as isize - 1, color);
299        self.canvas.line(0, 0, w_px as isize - 1, 0, color);
300
301        let y_max_str = format!("{:.1}", y_range.1);
302        let y_min_str = format!("{:.1}", y_range.0);
303
304        self.text(&y_max_str, 0.0, 1.0, color); // Izquierda
305        self.text(&y_max_str, 0.92, 1.0, color); // Derecha
306        self.text(&y_min_str, 0.0, 0.0, color);
307
308        let x_min_str = format!("{:.1}", x_range.0);
309        let x_max_str = format!("{:.1}", x_range.1);
310        self.text(&x_min_str, 0.1, 0.0, color);
311        self.text(&x_max_str, 0.9, 0.0, color);
312    }
313}