1use 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 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 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 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 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); if x_start >= w_px {
131 break;
132 } 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 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 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 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 pub fn text(&mut self, text: &str, x_norm: f64, y_norm: f64, color: Option<Color>) {
201 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 } self.canvas.set_char(x, cy, ch, color);
211 }
212 }
213 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 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 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 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 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 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 if y.is_finite() {
289 points.push((x, y));
290 }
291 }
292 self.line_chart(&points, color);
293 }
294
295 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); self.text(&y_max_str, 0.92, 1.0, color); 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}