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 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 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 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 let draw_ticks = |min: f64, max: f64| -> Vec<f64> {
211 let step = (max - min) / 3.0; vec![min, min + step, min + step * 2.0, max]
213 };
214
215 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 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 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}