1use crate::canvas::BrailleCanvas;
3use colored::Color;
4use std::f64::consts::PI;
5
6#[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 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 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 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 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 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 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}