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