1use alloc::vec;
16use alloc::vec::Vec;
17
18use rlvgl_core::draw::draw_widget_bg;
19use rlvgl_core::event::Event;
20use rlvgl_core::renderer::Renderer;
21use rlvgl_core::style::Style;
22use rlvgl_core::widget::{Color, Rect, Widget};
23
24#[derive(Debug, Clone, Copy, PartialEq, Eq)]
33pub enum ChartType {
34 Line,
36 Bar,
38 Scatter,
40}
41
42#[derive(Debug, Clone, Copy, PartialEq, Eq)]
51pub enum ChartAxis {
52 Primary,
54 Secondary,
56}
57
58#[derive(Debug, Clone, Copy, PartialEq, Eq)]
67pub struct SeriesId(pub u8);
68
69pub const SERIES_NONE: SeriesId = SeriesId(u8::MAX);
71
72struct SeriesDescriptor {
77 color: Color,
78 #[allow(dead_code)]
79 axis: ChartAxis,
80 points: Vec<i32>,
81}
82
83pub struct Chart {
100 bounds: Rect,
101 chart_type: ChartType,
102 y_min: i32,
103 y_max: i32,
104 point_count: usize,
105 h_div: u8,
106 v_div: u8,
107 series: Vec<SeriesDescriptor>,
108 cursor_index: Option<usize>,
110 pub style: Style,
112 pub grid_color: Color,
114}
115
116impl Chart {
117 pub fn new(bounds: Rect) -> Self {
120 Self {
121 bounds,
122 chart_type: ChartType::Line,
123 y_min: 0,
124 y_max: 100,
125 point_count: 10,
126 h_div: 5,
127 v_div: 5,
128 series: Vec::new(),
129 cursor_index: None,
130 style: Style::default(),
131 grid_color: Color(180, 180, 180, 180),
132 }
133 }
134
135 pub fn set_type(&mut self, t: ChartType) {
139 self.chart_type = t;
140 }
141
142 pub fn chart_type(&self) -> ChartType {
144 self.chart_type
145 }
146
147 pub fn add_series(&mut self, color: Color, axis: ChartAxis) -> SeriesId {
153 if self.series.len() >= u8::MAX as usize {
154 return SERIES_NONE;
155 }
156 let id = SeriesId(self.series.len() as u8);
157 self.series.push(SeriesDescriptor {
158 color,
159 axis,
160 points: vec![0; self.point_count],
161 });
162 id
163 }
164
165 pub fn set_point_count(&mut self, count: usize) {
170 self.point_count = count;
171 for s in &mut self.series {
172 s.points.resize(count, 0);
173 }
174 if let Some(idx) = self.cursor_index {
176 if count == 0 {
177 self.cursor_index = None;
178 } else if idx >= count {
179 self.cursor_index = Some(count - 1);
180 }
181 }
182 }
183
184 pub fn point_count(&self) -> usize {
186 self.point_count
187 }
188
189 pub fn set_point(&mut self, series: SeriesId, idx: usize, value: i32) {
193 if let Some(s) = self.series.get_mut(series.0 as usize)
194 && let Some(slot) = s.points.get_mut(idx)
195 {
196 *slot = value;
197 }
198 }
199
200 pub fn get_point(&self, series: SeriesId, idx: usize) -> Option<i32> {
202 self.series
203 .get(series.0 as usize)
204 .and_then(|s| s.points.get(idx).copied())
205 }
206
207 pub fn set_points(&mut self, series: SeriesId, values: &[i32]) {
209 if let Some(s) = self.series.get_mut(series.0 as usize) {
210 let n = values.len().min(self.point_count);
211 s.points[..n].copy_from_slice(&values[..n]);
212 }
213 }
214
215 pub fn set_series_color(&mut self, series: SeriesId, color: Color) {
217 if let Some(s) = self.series.get_mut(series.0 as usize) {
218 s.color = color;
219 }
220 }
221
222 pub fn set_axis_range(&mut self, _axis: ChartAxis, min: i32, max: i32) {
228 self.y_min = min;
229 self.y_max = max;
230 }
231
232 pub fn set_div_line_count(&mut self, h_div: u8, v_div: u8) {
236 self.h_div = h_div;
237 self.v_div = v_div;
238 }
239
240 pub fn h_div_line_count(&self) -> u8 {
242 self.h_div
243 }
244
245 pub fn v_div_line_count(&self) -> u8 {
247 self.v_div
248 }
249
250 pub fn cursor_index(&self) -> Option<usize> {
255 self.cursor_index
256 }
257
258 pub fn activate_cursor(&mut self) {
260 if self.point_count > 0 {
261 self.cursor_index = Some(0);
262 }
263 }
264
265 pub fn clear_cursor(&mut self) {
267 self.cursor_index = None;
268 }
269
270 pub fn navigate_cursor_left(&mut self) {
274 if self.point_count == 0 {
275 return;
276 }
277 let idx = self.cursor_index.unwrap_or(0);
278 self.cursor_index = Some(if idx == 0 {
279 self.point_count - 1
280 } else {
281 idx - 1
282 });
283 }
284
285 pub fn navigate_cursor_right(&mut self) {
289 if self.point_count == 0 {
290 return;
291 }
292 let idx = self.cursor_index.unwrap_or(self.point_count - 1);
293 self.cursor_index = Some((idx + 1) % self.point_count);
294 }
295
296 fn value_to_y(&self, value: i32) -> i32 {
300 let range = self.y_max - self.y_min;
301 if range == 0 {
302 return self.bounds.y + self.bounds.height / 2;
303 }
304 let clamped = value.clamp(self.y_min, self.y_max);
305 let frac_num = (clamped - self.y_min) as i64;
306 let frac_den = range as i64;
307 let h = self.bounds.height as i64;
308 let offset = (frac_num * h / frac_den) as i32;
309 self.bounds.y + self.bounds.height - offset
310 }
311
312 fn point_to_x(&self, idx: usize) -> i32 {
314 if self.point_count == 0 {
315 return self.bounds.x;
316 }
317 let slot_w = self.bounds.width / self.point_count as i32;
318 let slot_w = slot_w.max(1);
319 self.bounds.x + idx as i32 * slot_w + slot_w / 2
320 }
321
322 fn draw_grid(&self, renderer: &mut dyn Renderer) {
323 let color = self.grid_color.with_alpha(self.style.alpha);
324 if self.h_div > 0 {
326 let step = self.bounds.height / (self.h_div as i32 + 1);
327 for i in 1..=self.h_div as i32 {
328 let y = self.bounds.y + i * step;
329 renderer.fill_rect(
330 Rect {
331 x: self.bounds.x,
332 y,
333 width: self.bounds.width,
334 height: 1,
335 },
336 color,
337 );
338 }
339 }
340 if self.v_div > 0 {
342 let step = self.bounds.width / (self.v_div as i32 + 1);
343 for i in 1..=self.v_div as i32 {
344 let x = self.bounds.x + i * step;
345 renderer.fill_rect(
346 Rect {
347 x,
348 y: self.bounds.y,
349 width: 1,
350 height: self.bounds.height,
351 },
352 color,
353 );
354 }
355 }
356 }
357
358 fn draw_series(&self, renderer: &mut dyn Renderer) {
359 for s in &self.series {
360 let color = s.color.with_alpha(self.style.alpha);
361 match self.chart_type {
362 ChartType::Line => self.draw_line_series(renderer, s, color),
363 ChartType::Bar => self.draw_bar_series(renderer, s, color),
364 ChartType::Scatter => self.draw_scatter_series(renderer, s, color),
365 }
366 }
367 }
368
369 fn draw_line_series(&self, renderer: &mut dyn Renderer, s: &SeriesDescriptor, color: Color) {
370 if s.points.len() < 2 {
371 return;
372 }
373 for i in 0..s.points.len() - 1 {
374 let x0 = self.point_to_x(i);
375 let y0 = self.value_to_y(s.points[i]);
376 let x1 = self.point_to_x(i + 1);
377 let y1 = self.value_to_y(s.points[i + 1]);
378 renderer.stroke_line_aa(
379 rlvgl_core::raster::PointF {
380 x: x0 as f32,
381 y: y0 as f32,
382 },
383 rlvgl_core::raster::PointF {
384 x: x1 as f32,
385 y: y1 as f32,
386 },
387 1.0,
388 color,
389 );
390 }
391 }
392
393 fn draw_bar_series(&self, renderer: &mut dyn Renderer, s: &SeriesDescriptor, color: Color) {
394 if self.point_count == 0 {
395 return;
396 }
397 let slot_w = (self.bounds.width / self.point_count as i32).max(1);
398 let bar_w = (slot_w * 3 / 5).max(1);
399 let baseline_y = self.value_to_y(self.y_min.max(0).min(self.y_max));
400 for (i, &v) in s.points.iter().enumerate() {
401 let x = self.bounds.x + i as i32 * slot_w + (slot_w - bar_w) / 2;
402 let y = self.value_to_y(v);
403 let (top, height) = if y <= baseline_y {
404 (y, baseline_y - y)
405 } else {
406 (baseline_y, y - baseline_y)
407 };
408 if height > 0 {
409 renderer.fill_rect(
410 Rect {
411 x,
412 y: top,
413 width: bar_w,
414 height,
415 },
416 color,
417 );
418 }
419 }
420 }
421
422 fn draw_scatter_series(&self, renderer: &mut dyn Renderer, s: &SeriesDescriptor, color: Color) {
423 for (i, &v) in s.points.iter().enumerate() {
424 let cx = self.point_to_x(i);
425 let cy = self.value_to_y(v);
426 renderer.fill_rect(
427 Rect {
428 x: cx - 2,
429 y: cy - 2,
430 width: 5,
431 height: 5,
432 },
433 color,
434 );
435 }
436 }
437
438 fn draw_cursor(&self, renderer: &mut dyn Renderer) {
439 let Some(idx) = self.cursor_index else {
440 return;
441 };
442 let cx = self.point_to_x(idx);
443 let cursor_color = Color(255, 80, 80, 200).with_alpha(self.style.alpha);
444 renderer.fill_rect(
446 Rect {
447 x: cx,
448 y: self.bounds.y,
449 width: 1,
450 height: self.bounds.height,
451 },
452 cursor_color,
453 );
454 }
455}
456
457impl Widget for Chart {
458 fn bounds(&self) -> Rect {
459 self.bounds
460 }
461
462 fn set_bounds(&mut self, bounds: Rect) {
463 self.bounds = bounds;
464 }
465
466 fn draw(&self, renderer: &mut dyn Renderer) {
467 draw_widget_bg(renderer, self.bounds, &self.style);
469 self.draw_grid(renderer);
471 self.draw_series(renderer);
473 self.draw_cursor(renderer);
475 }
476
477 fn handle_event(&mut self, _event: &Event) -> bool {
478 false
479 }
480}
481
482#[cfg(test)]
487mod tests {
488 use super::*;
489
490 fn r(x: i32, y: i32, w: i32, h: i32) -> Rect {
491 Rect {
492 x,
493 y,
494 width: w,
495 height: h,
496 }
497 }
498
499 #[test]
500 fn add_series_returns_sequential_ids() {
501 let mut chart = Chart::new(r(0, 0, 100, 100));
502 let a = chart.add_series(Color(255, 0, 0, 255), ChartAxis::Primary);
503 let b = chart.add_series(Color(0, 255, 0, 255), ChartAxis::Primary);
504 assert_eq!(a, SeriesId(0));
505 assert_eq!(b, SeriesId(1));
506 }
507
508 #[test]
509 fn set_and_get_point() {
510 let mut chart = Chart::new(r(0, 0, 200, 100));
511 let s = chart.add_series(Color(0, 0, 255, 255), ChartAxis::Primary);
512 chart.set_point(s, 3, 42);
513 assert_eq!(chart.get_point(s, 3), Some(42));
514 assert_eq!(chart.get_point(s, 9), Some(0));
515 }
516
517 #[test]
518 fn set_points_bulk() {
519 let mut chart = Chart::new(r(0, 0, 200, 100));
520 let s = chart.add_series(Color(0, 0, 0, 255), ChartAxis::Primary);
521 chart.set_points(s, &[1, 2, 3, 4, 5]);
522 assert_eq!(chart.get_point(s, 0), Some(1));
523 assert_eq!(chart.get_point(s, 4), Some(5));
524 }
525
526 #[test]
527 fn value_to_y_mapping() {
528 let chart = Chart::new(r(0, 0, 100, 100));
529 assert_eq!(chart.value_to_y(0), 100);
531 assert_eq!(chart.value_to_y(100), 0);
533 assert_eq!(chart.value_to_y(50), 50);
535 }
536
537 #[test]
538 fn chart_type_dispatch() {
539 let mut chart = Chart::new(r(0, 0, 100, 100));
540 chart.set_type(ChartType::Bar);
541 assert_eq!(chart.chart_type(), ChartType::Bar);
542 chart.set_type(ChartType::Scatter);
543 assert_eq!(chart.chart_type(), ChartType::Scatter);
544 }
545
546 #[test]
547 fn cursor_navigation_wraps() {
548 let mut chart = Chart::new(r(0, 0, 100, 100));
549 chart.set_point_count(5);
550 chart.activate_cursor();
551 assert_eq!(chart.cursor_index(), Some(0));
552 chart.navigate_cursor_left();
553 assert_eq!(chart.cursor_index(), Some(4)); chart.navigate_cursor_right();
555 assert_eq!(chart.cursor_index(), Some(0)); }
557
558 #[test]
559 fn set_point_count_clamps_cursor() {
560 let mut chart = Chart::new(r(0, 0, 100, 100));
561 chart.activate_cursor();
562 chart.navigate_cursor_right();
563 chart.navigate_cursor_right();
564 chart.set_point_count(2);
566 assert_eq!(chart.cursor_index(), Some(1));
567 }
568
569 #[test]
570 fn resize_via_set_bounds() {
571 let mut chart = Chart::new(r(0, 0, 100, 100));
572 chart.set_bounds(r(10, 20, 200, 150));
573 assert_eq!(chart.bounds(), r(10, 20, 200, 150));
574 }
575
576 #[test]
577 fn div_line_counts_stored() {
578 let mut chart = Chart::new(r(0, 0, 100, 100));
579 chart.set_div_line_count(3, 7);
580 assert_eq!(chart.h_div_line_count(), 3);
581 assert_eq!(chart.v_div_line_count(), 7);
582 }
583}