1use presentar_core::{
4 widget::{AccessibleRole, LayoutResult, TextStyle},
5 Canvas, Color, Constraints, Point, Rect, Size, TypeId, Widget,
6};
7use serde::{Deserialize, Serialize};
8use std::any::Any;
9
10#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
12pub enum ChartType {
13 #[default]
15 Line,
16 Bar,
18 Scatter,
20 Area,
22 Pie,
24 Histogram,
26 Heatmap,
28 BoxPlot,
30}
31
32#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
34pub struct DataSeries {
35 pub name: String,
37 pub points: Vec<(f64, f64)>,
39 pub color: Color,
41 pub line_width: f32,
43 pub point_size: f32,
45 pub show_points: bool,
47 pub fill: bool,
49}
50
51impl DataSeries {
52 #[must_use]
54 pub fn new(name: impl Into<String>) -> Self {
55 Self {
56 name: name.into(),
57 points: Vec::new(),
58 color: Color::new(0.2, 0.47, 0.96, 1.0),
59 line_width: 2.0,
60 point_size: 4.0,
61 show_points: true,
62 fill: false,
63 }
64 }
65
66 #[must_use]
68 pub fn point(mut self, x: f64, y: f64) -> Self {
69 self.points.push((x, y));
70 self
71 }
72
73 #[must_use]
75 pub fn points(mut self, points: impl IntoIterator<Item = (f64, f64)>) -> Self {
76 self.points.extend(points);
77 self
78 }
79
80 #[must_use]
82 pub const fn color(mut self, color: Color) -> Self {
83 self.color = color;
84 self
85 }
86
87 #[must_use]
89 pub fn line_width(mut self, width: f32) -> Self {
90 self.line_width = width.max(0.5);
91 self
92 }
93
94 #[must_use]
96 pub fn point_size(mut self, size: f32) -> Self {
97 self.point_size = size.max(1.0);
98 self
99 }
100
101 #[must_use]
103 pub const fn show_points(mut self, show: bool) -> Self {
104 self.show_points = show;
105 self
106 }
107
108 #[must_use]
110 pub const fn fill(mut self, fill: bool) -> Self {
111 self.fill = fill;
112 self
113 }
114
115 #[must_use]
117 pub fn x_range(&self) -> Option<(f64, f64)> {
118 if self.points.is_empty() {
119 return None;
120 }
121 let min = self
122 .points
123 .iter()
124 .map(|(x, _)| *x)
125 .fold(f64::INFINITY, f64::min);
126 let max = self
127 .points
128 .iter()
129 .map(|(x, _)| *x)
130 .fold(f64::NEG_INFINITY, f64::max);
131 Some((min, max))
132 }
133
134 #[must_use]
136 pub fn y_range(&self) -> Option<(f64, f64)> {
137 if self.points.is_empty() {
138 return None;
139 }
140 let min = self
141 .points
142 .iter()
143 .map(|(_, y)| *y)
144 .fold(f64::INFINITY, f64::min);
145 let max = self
146 .points
147 .iter()
148 .map(|(_, y)| *y)
149 .fold(f64::NEG_INFINITY, f64::max);
150 Some((min, max))
151 }
152}
153
154#[derive(Debug, Clone, Serialize, Deserialize)]
156pub struct Axis {
157 pub label: Option<String>,
159 pub min: Option<f64>,
161 pub max: Option<f64>,
163 pub grid_lines: usize,
165 pub show_grid: bool,
167 pub color: Color,
169 pub grid_color: Color,
171}
172
173impl Default for Axis {
174 fn default() -> Self {
175 Self {
176 label: None,
177 min: None,
178 max: None,
179 grid_lines: 5,
180 show_grid: true,
181 color: Color::new(0.3, 0.3, 0.3, 1.0),
182 grid_color: Color::new(0.9, 0.9, 0.9, 1.0),
183 }
184 }
185}
186
187impl Axis {
188 #[must_use]
190 pub fn new() -> Self {
191 Self::default()
192 }
193
194 #[must_use]
196 pub fn label(mut self, label: impl Into<String>) -> Self {
197 self.label = Some(label.into());
198 self
199 }
200
201 #[must_use]
203 pub const fn min(mut self, min: f64) -> Self {
204 self.min = Some(min);
205 self
206 }
207
208 #[must_use]
210 pub const fn max(mut self, max: f64) -> Self {
211 self.max = Some(max);
212 self
213 }
214
215 #[must_use]
217 pub const fn range(mut self, min: f64, max: f64) -> Self {
218 self.min = Some(min);
219 self.max = Some(max);
220 self
221 }
222
223 #[must_use]
225 pub fn grid_lines(mut self, count: usize) -> Self {
226 self.grid_lines = count.max(2);
227 self
228 }
229
230 #[must_use]
232 pub const fn show_grid(mut self, show: bool) -> Self {
233 self.show_grid = show;
234 self
235 }
236
237 #[must_use]
239 pub const fn color(mut self, color: Color) -> Self {
240 self.color = color;
241 self
242 }
243
244 #[must_use]
246 pub const fn grid_color(mut self, color: Color) -> Self {
247 self.grid_color = color;
248 self
249 }
250}
251
252#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
254pub enum LegendPosition {
255 None,
257 #[default]
259 TopRight,
260 TopLeft,
262 BottomRight,
264 BottomLeft,
266}
267
268#[derive(Debug, Clone, Serialize, Deserialize)]
270pub struct Chart {
271 kind: ChartType,
273 series: Vec<DataSeries>,
275 title: Option<String>,
277 x_axis: Axis,
279 y_axis: Axis,
281 legend: LegendPosition,
283 background: Color,
285 padding: f32,
287 width: Option<f32>,
289 height: Option<f32>,
291 accessible_name_value: Option<String>,
293 test_id_value: Option<String>,
295 #[serde(skip)]
297 bounds: Rect,
298}
299
300impl Default for Chart {
301 fn default() -> Self {
302 Self {
303 kind: ChartType::Line,
304 series: Vec::new(),
305 title: None,
306 x_axis: Axis::default(),
307 y_axis: Axis::default(),
308 legend: LegendPosition::TopRight,
309 background: Color::WHITE,
310 padding: 40.0,
311 width: None,
312 height: None,
313 accessible_name_value: None,
314 test_id_value: None,
315 bounds: Rect::default(),
316 }
317 }
318}
319
320impl Chart {
321 #[must_use]
323 pub fn new() -> Self {
324 Self::default()
325 }
326
327 #[must_use]
329 pub fn line() -> Self {
330 Self::new().chart_type(ChartType::Line)
331 }
332
333 #[must_use]
335 pub fn bar() -> Self {
336 Self::new().chart_type(ChartType::Bar)
337 }
338
339 #[must_use]
341 pub fn scatter() -> Self {
342 Self::new().chart_type(ChartType::Scatter)
343 }
344
345 #[must_use]
347 pub fn area() -> Self {
348 Self::new().chart_type(ChartType::Area)
349 }
350
351 #[must_use]
353 pub fn pie() -> Self {
354 Self::new().chart_type(ChartType::Pie)
355 }
356
357 #[must_use]
359 pub fn heatmap() -> Self {
360 Self::new().chart_type(ChartType::Heatmap)
361 }
362
363 #[must_use]
365 pub fn boxplot() -> Self {
366 Self::new().chart_type(ChartType::BoxPlot)
367 }
368
369 #[must_use]
371 pub const fn chart_type(mut self, chart_type: ChartType) -> Self {
372 self.kind = chart_type;
373 self
374 }
375
376 #[must_use]
378 pub fn series(mut self, series: DataSeries) -> Self {
379 self.series.push(series);
380 self
381 }
382
383 #[must_use]
385 pub fn add_series(mut self, series: impl IntoIterator<Item = DataSeries>) -> Self {
386 self.series.extend(series);
387 self
388 }
389
390 #[must_use]
392 pub fn title(mut self, title: impl Into<String>) -> Self {
393 self.title = Some(title.into());
394 self
395 }
396
397 #[must_use]
399 pub fn x_axis(mut self, axis: Axis) -> Self {
400 self.x_axis = axis;
401 self
402 }
403
404 #[must_use]
406 pub fn y_axis(mut self, axis: Axis) -> Self {
407 self.y_axis = axis;
408 self
409 }
410
411 #[must_use]
413 pub const fn legend(mut self, position: LegendPosition) -> Self {
414 self.legend = position;
415 self
416 }
417
418 #[must_use]
420 pub const fn background(mut self, color: Color) -> Self {
421 self.background = color;
422 self
423 }
424
425 #[must_use]
427 pub fn padding(mut self, padding: f32) -> Self {
428 self.padding = padding.max(0.0);
429 self
430 }
431
432 #[must_use]
434 pub fn width(mut self, width: f32) -> Self {
435 self.width = Some(width.max(100.0));
436 self
437 }
438
439 #[must_use]
441 pub fn height(mut self, height: f32) -> Self {
442 self.height = Some(height.max(100.0));
443 self
444 }
445
446 #[must_use]
448 pub fn accessible_name(mut self, name: impl Into<String>) -> Self {
449 self.accessible_name_value = Some(name.into());
450 self
451 }
452
453 #[must_use]
455 pub fn test_id(mut self, id: impl Into<String>) -> Self {
456 self.test_id_value = Some(id.into());
457 self
458 }
459
460 #[must_use]
462 pub const fn get_chart_type(&self) -> ChartType {
463 self.kind
464 }
465
466 #[must_use]
468 pub fn get_series(&self) -> &[DataSeries] {
469 &self.series
470 }
471
472 #[must_use]
474 pub fn series_count(&self) -> usize {
475 self.series.len()
476 }
477
478 #[must_use]
480 pub fn has_data(&self) -> bool {
481 self.series.iter().any(|s| !s.points.is_empty())
482 }
483
484 #[must_use]
486 pub fn get_title(&self) -> Option<&str> {
487 self.title.as_deref()
488 }
489
490 #[must_use]
492 pub fn data_bounds(&self) -> Option<(f64, f64, f64, f64)> {
493 if !self.has_data() {
494 return None;
495 }
496
497 let mut x_min = f64::INFINITY;
498 let mut x_max = f64::NEG_INFINITY;
499 let mut y_min = f64::INFINITY;
500 let mut y_max = f64::NEG_INFINITY;
501
502 for series in &self.series {
503 if let Some((min, max)) = series.x_range() {
504 x_min = x_min.min(min);
505 x_max = x_max.max(max);
506 }
507 if let Some((min, max)) = series.y_range() {
508 y_min = y_min.min(min);
509 y_max = y_max.max(max);
510 }
511 }
512
513 if let Some(min) = self.x_axis.min {
515 x_min = min;
516 }
517 if let Some(max) = self.x_axis.max {
518 x_max = max;
519 }
520 if let Some(min) = self.y_axis.min {
521 y_min = min;
522 }
523 if let Some(max) = self.y_axis.max {
524 y_max = max;
525 }
526
527 Some((x_min, x_max, y_min, y_max))
528 }
529
530 fn plot_area(&self) -> Rect {
532 let title_height = if self.title.is_some() { 30.0 } else { 0.0 };
533 Rect::new(
534 self.bounds.x + self.padding,
535 self.bounds.y + self.padding + title_height,
536 self.padding.mul_add(-2.0, self.bounds.width),
537 self.padding.mul_add(-2.0, self.bounds.height) - title_height,
538 )
539 }
540
541 fn map_point(&self, x: f64, y: f64, bounds: &(f64, f64, f64, f64), plot: &Rect) -> Point {
543 let (x_min, x_max, y_min, y_max) = *bounds;
544 let x_range = (x_max - x_min).max(1e-10);
545 let y_range = (y_max - y_min).max(1e-10);
546
547 let px = (((x - x_min) / x_range) as f32).mul_add(plot.width, plot.x);
548 let py = (((y - y_min) / y_range) as f32).mul_add(-plot.height, plot.y + plot.height);
549
550 Point::new(px, py)
551 }
552
553 fn paint_grid(&self, canvas: &mut dyn Canvas, plot: &Rect, bounds: &(f64, f64, f64, f64)) {
555 let (x_min, x_max, y_min, y_max) = *bounds;
556
557 if self.x_axis.show_grid {
559 for i in 0..=self.x_axis.grid_lines {
560 let t = i as f32 / self.x_axis.grid_lines as f32;
561 let x = t.mul_add(plot.width, plot.x);
562 canvas.draw_line(
563 Point::new(x, plot.y),
564 Point::new(x, plot.y + plot.height),
565 self.x_axis.grid_color,
566 1.0,
567 );
568 }
569 }
570
571 if self.y_axis.show_grid {
573 for i in 0..=self.y_axis.grid_lines {
574 let t = i as f32 / self.y_axis.grid_lines as f32;
575 let y = t.mul_add(plot.height, plot.y);
576 canvas.draw_line(
577 Point::new(plot.x, y),
578 Point::new(plot.x + plot.width, y),
579 self.y_axis.grid_color,
580 1.0,
581 );
582 }
583 }
584
585 let text_style = TextStyle {
587 size: 10.0,
588 color: self.x_axis.color,
589 ..TextStyle::default()
590 };
591
592 for i in 0..=self.x_axis.grid_lines {
594 let t = i as f64 / self.x_axis.grid_lines as f64;
595 let value = t.mul_add(x_max - x_min, x_min);
596 let x = (t as f32).mul_add(plot.width, plot.x);
597 canvas.draw_text(
598 &format!("{value:.1}"),
599 Point::new(x, plot.y + plot.height + 15.0),
600 &text_style,
601 );
602 }
603
604 for i in 0..=self.y_axis.grid_lines {
606 let t = i as f64 / self.y_axis.grid_lines as f64;
607 let value = t.mul_add(-(y_max - y_min), y_max);
608 let y = (t as f32).mul_add(plot.height, plot.y);
609 canvas.draw_text(
610 &format!("{value:.1}"),
611 Point::new(plot.x - 35.0, y + 4.0),
612 &text_style,
613 );
614 }
615 }
616
617 fn paint_line(&self, canvas: &mut dyn Canvas, plot: &Rect, bounds: &(f64, f64, f64, f64)) {
619 for series in &self.series {
620 if series.points.len() < 2 {
621 continue;
622 }
623
624 let path_points: Vec<Point> = series
626 .points
627 .iter()
628 .map(|&(x, y)| self.map_point(x, y, bounds, plot))
629 .collect();
630
631 canvas.draw_path(&path_points, series.color, series.line_width);
633
634 if series.fill {
636 let mut fill_points = path_points.clone();
637 if let (Some(first), Some(last)) = (path_points.first(), path_points.last()) {
639 fill_points.push(Point::new(last.x, plot.y + plot.height));
640 fill_points.push(Point::new(first.x, plot.y + plot.height));
641 }
642 let mut fill_color = series.color;
643 fill_color.a = 0.3; canvas.fill_polygon(&fill_points, fill_color);
645 }
646
647 if series.show_points {
649 for &(x, y) in &series.points {
650 let pt = self.map_point(x, y, bounds, plot);
651 canvas.fill_circle(pt, series.point_size / 2.0, series.color);
652 }
653 }
654 }
655 }
656
657 fn paint_bar(&self, canvas: &mut dyn Canvas, plot: &Rect, bounds: &(f64, f64, f64, f64)) {
659 let (_, _, y_min, y_max) = *bounds;
660 let y_range = (y_max - y_min).max(1e-10);
661
662 let series_count = self.series.len();
663 if series_count == 0 {
664 return;
665 }
666
667 let max_points = self
669 .series
670 .iter()
671 .map(|s| s.points.len())
672 .max()
673 .unwrap_or(1);
674 let group_width = plot.width / max_points as f32;
675 let bar_width = (group_width * 0.8) / series_count as f32;
676 let bar_gap = group_width * 0.1;
677
678 for (si, series) in self.series.iter().enumerate() {
679 for (i, &(_, y)) in series.points.iter().enumerate() {
680 let bar_height = ((y - y_min) / y_range) as f32 * plot.height;
681 let x = (si as f32)
682 .mul_add(bar_width, (i as f32).mul_add(group_width, plot.x + bar_gap));
683 let rect = Rect::new(
684 x,
685 plot.y + plot.height - bar_height,
686 bar_width - 2.0,
687 bar_height,
688 );
689 canvas.fill_rect(rect, series.color);
690 }
691 }
692 }
693
694 fn paint_scatter(&self, canvas: &mut dyn Canvas, plot: &Rect, bounds: &(f64, f64, f64, f64)) {
696 for series in &self.series {
697 for &(x, y) in &series.points {
698 let pt = self.map_point(x, y, bounds, plot);
699 canvas.fill_circle(pt, series.point_size / 2.0, series.color);
700 }
701 }
702 }
703
704 fn paint_pie(&self, canvas: &mut dyn Canvas, plot: &Rect) {
706 let total: f64 = self
708 .series
709 .iter()
710 .flat_map(|s| s.points.iter().map(|(_, y)| *y))
711 .sum();
712
713 if total <= 0.0 {
714 return;
715 }
716
717 let cx = plot.x + plot.width / 2.0;
718 let cy = plot.y + plot.height / 2.0;
719 let radius = plot.width.min(plot.height) / 2.0 * 0.8;
720 let center = Point::new(cx, cy);
721
722 let mut start_angle: f32 = -std::f32::consts::FRAC_PI_2; for series in &self.series {
725 for &(_, y) in &series.points {
726 let fraction = (y / total) as f32;
727 let sweep = fraction * std::f32::consts::TAU;
728 let end_angle = start_angle + sweep;
729
730 canvas.fill_arc(center, radius, start_angle, end_angle, series.color);
731
732 start_angle = end_angle;
733 }
734 }
735 }
736
737 fn paint_heatmap(&self, canvas: &mut dyn Canvas, plot: &Rect, bounds: &(f64, f64, f64, f64)) {
739 let (_, _, y_min, y_max) = *bounds;
740 let y_range = (y_max - y_min).max(1e-10);
741
742 let row_count = self.series.len();
744 if row_count == 0 {
745 return;
746 }
747
748 let col_count = self
749 .series
750 .iter()
751 .map(|s| s.points.len())
752 .max()
753 .unwrap_or(1);
754
755 let cell_width = plot.width / col_count as f32;
756 let cell_height = plot.height / row_count as f32;
757
758 for (row, series) in self.series.iter().enumerate() {
759 for (col, &(_, value)) in series.points.iter().enumerate() {
760 let t = ((value - y_min) / y_range) as f32;
762 let color = Color::new(t, 0.2, 1.0 - t, 1.0);
763
764 let rect = Rect::new(
765 (col as f32).mul_add(cell_width, plot.x),
766 (row as f32).mul_add(cell_height, plot.y),
767 cell_width - 1.0,
768 cell_height - 1.0,
769 );
770 canvas.fill_rect(rect, color);
771 }
772 }
773 }
774
775 fn paint_boxplot(&self, canvas: &mut dyn Canvas, plot: &Rect, bounds: &(f64, f64, f64, f64)) {
777 let (_, _, y_min, y_max) = *bounds;
778 let y_range = (y_max - y_min).max(1e-10);
779
780 let series_count = self.series.len();
781 if series_count == 0 {
782 return;
783 }
784
785 let box_width = (plot.width / series_count as f32) * 0.6;
786 let gap = (plot.width / series_count as f32) * 0.2;
787
788 for (i, series) in self.series.iter().enumerate() {
789 if series.points.len() < 5 {
790 continue; }
792
793 let mut values: Vec<f64> = series.points.iter().map(|(_, y)| *y).collect();
795 values.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
796
797 let min_val = values[0];
798 let q1 = values[values.len() / 4];
799 let median = values[values.len() / 2];
800 let q3 = values[3 * values.len() / 4];
801 let max_val = values[values.len() - 1];
802
803 let x_center = (i as f32).mul_add(plot.width / series_count as f32, plot.x + gap);
804
805 let map_y = |v: f64| -> f32 {
807 let t = (v - y_min) / y_range;
808 (1.0 - t as f32).mul_add(plot.height, plot.y)
809 };
810
811 let y_min_px = map_y(min_val);
812 let y_q1 = map_y(q1);
813 let y_median = map_y(median);
814 let y_q3 = map_y(q3);
815 let y_max_px = map_y(max_val);
816
817 canvas.draw_line(
819 Point::new(x_center + box_width / 2.0, y_min_px),
820 Point::new(x_center + box_width / 2.0, y_q1),
821 series.color,
822 1.0,
823 );
824 canvas.draw_line(
825 Point::new(x_center + box_width / 2.0, y_q3),
826 Point::new(x_center + box_width / 2.0, y_max_px),
827 series.color,
828 1.0,
829 );
830
831 let box_rect = Rect::new(x_center, y_q3, box_width, y_q1 - y_q3);
833 canvas.fill_rect(box_rect, series.color);
834 canvas.stroke_rect(box_rect, Color::new(0.0, 0.0, 0.0, 1.0), 1.0);
835
836 canvas.draw_line(
838 Point::new(x_center, y_median),
839 Point::new(x_center + box_width, y_median),
840 Color::new(0.0, 0.0, 0.0, 1.0),
841 2.0,
842 );
843
844 let cap_width = box_width * 0.3;
846 canvas.draw_line(
847 Point::new(x_center + box_width / 2.0 - cap_width / 2.0, y_min_px),
848 Point::new(x_center + box_width / 2.0 + cap_width / 2.0, y_min_px),
849 series.color,
850 1.0,
851 );
852 canvas.draw_line(
853 Point::new(x_center + box_width / 2.0 - cap_width / 2.0, y_max_px),
854 Point::new(x_center + box_width / 2.0 + cap_width / 2.0, y_max_px),
855 series.color,
856 1.0,
857 );
858 }
859 }
860
861 fn paint_legend(&self, canvas: &mut dyn Canvas) {
863 if self.legend == LegendPosition::None || self.series.is_empty() {
864 return;
865 }
866
867 let entry_height = 20.0;
868 let legend_width = 100.0;
869 let legend_height = (self.series.len() as f32).mul_add(entry_height, 10.0);
870
871 let (lx, ly) = match self.legend {
872 LegendPosition::TopRight => (
873 self.bounds.x + self.bounds.width - legend_width - 10.0,
874 self.bounds.y + self.padding + 10.0,
875 ),
876 LegendPosition::TopLeft => (
877 self.bounds.x + self.padding + 10.0,
878 self.bounds.y + self.padding + 10.0,
879 ),
880 LegendPosition::BottomRight => (
881 self.bounds.x + self.bounds.width - legend_width - 10.0,
882 self.bounds.y + self.bounds.height - legend_height - 10.0,
883 ),
884 LegendPosition::BottomLeft => (
885 self.bounds.x + self.padding + 10.0,
886 self.bounds.y + self.bounds.height - legend_height - 10.0,
887 ),
888 LegendPosition::None => return,
889 };
890
891 canvas.fill_rect(
893 Rect::new(lx, ly, legend_width, legend_height),
894 Color::new(1.0, 1.0, 1.0, 0.9),
895 );
896 canvas.stroke_rect(
897 Rect::new(lx, ly, legend_width, legend_height),
898 Color::new(0.8, 0.8, 0.8, 1.0),
899 1.0,
900 );
901
902 let text_style = TextStyle {
904 size: 12.0,
905 color: Color::new(0.2, 0.2, 0.2, 1.0),
906 ..TextStyle::default()
907 };
908
909 for (i, series) in self.series.iter().enumerate() {
910 let ey = (i as f32).mul_add(entry_height, ly + 5.0);
911 canvas.fill_rect(Rect::new(lx + 5.0, ey + 4.0, 12.0, 12.0), series.color);
913 canvas.draw_text(&series.name, Point::new(lx + 22.0, ey + 14.0), &text_style);
915 }
916 }
917}
918
919impl Widget for Chart {
920 fn type_id(&self) -> TypeId {
921 TypeId::of::<Self>()
922 }
923
924 fn measure(&self, constraints: Constraints) -> Size {
925 let width = self.width.unwrap_or(400.0);
926 let height = self.height.unwrap_or(300.0);
927 constraints.constrain(Size::new(width, height))
928 }
929
930 fn layout(&mut self, bounds: Rect) -> LayoutResult {
931 self.bounds = bounds;
932 LayoutResult {
933 size: bounds.size(),
934 }
935 }
936
937 fn paint(&self, canvas: &mut dyn Canvas) {
938 canvas.fill_rect(self.bounds, self.background);
940
941 if let Some(ref title) = self.title {
943 let text_style = TextStyle {
944 size: 16.0,
945 color: Color::new(0.1, 0.1, 0.1, 1.0),
946 ..TextStyle::default()
947 };
948 canvas.draw_text(
949 title,
950 Point::new(
951 (title.len() as f32).mul_add(-4.0, self.bounds.x + self.bounds.width / 2.0),
952 self.bounds.y + 25.0,
953 ),
954 &text_style,
955 );
956 }
957
958 let plot = self.plot_area();
959
960 let Some(bounds) = self.data_bounds() else {
962 return;
963 };
964
965 self.paint_grid(canvas, &plot, &bounds);
967
968 match self.kind {
970 ChartType::Line | ChartType::Area => self.paint_line(canvas, &plot, &bounds),
971 ChartType::Bar | ChartType::Histogram => self.paint_bar(canvas, &plot, &bounds),
972 ChartType::Scatter => self.paint_scatter(canvas, &plot, &bounds),
973 ChartType::Pie => self.paint_pie(canvas, &plot),
974 ChartType::Heatmap => self.paint_heatmap(canvas, &plot, &bounds),
975 ChartType::BoxPlot => self.paint_boxplot(canvas, &plot, &bounds),
976 }
977
978 self.paint_legend(canvas);
980 }
981
982 fn event(&mut self, _event: &presentar_core::Event) -> Option<Box<dyn Any + Send>> {
983 None
985 }
986
987 fn children(&self) -> &[Box<dyn Widget>] {
988 &[]
989 }
990
991 fn children_mut(&mut self) -> &mut [Box<dyn Widget>] {
992 &mut []
993 }
994
995 fn is_interactive(&self) -> bool {
996 false
997 }
998
999 fn is_focusable(&self) -> bool {
1000 false
1001 }
1002
1003 fn accessible_name(&self) -> Option<&str> {
1004 self.accessible_name_value
1005 .as_deref()
1006 .or(self.title.as_deref())
1007 }
1008
1009 fn accessible_role(&self) -> AccessibleRole {
1010 AccessibleRole::Image }
1012
1013 fn test_id(&self) -> Option<&str> {
1014 self.test_id_value.as_deref()
1015 }
1016}
1017
1018#[cfg(test)]
1019mod tests {
1020 use super::*;
1021
1022 #[test]
1025 fn test_chart_type_default() {
1026 assert_eq!(ChartType::default(), ChartType::Line);
1027 }
1028
1029 #[test]
1030 fn test_chart_type_variants() {
1031 let types = [
1032 ChartType::Line,
1033 ChartType::Bar,
1034 ChartType::Scatter,
1035 ChartType::Area,
1036 ChartType::Pie,
1037 ChartType::Histogram,
1038 ChartType::Heatmap,
1039 ChartType::BoxPlot,
1040 ];
1041 assert_eq!(types.len(), 8);
1042 }
1043
1044 #[test]
1045 fn test_chart_heatmap() {
1046 let chart = Chart::new().chart_type(ChartType::Heatmap);
1047 assert_eq!(chart.get_chart_type(), ChartType::Heatmap);
1048 }
1049
1050 #[test]
1051 fn test_chart_boxplot() {
1052 let chart = Chart::new().chart_type(ChartType::BoxPlot);
1053 assert_eq!(chart.get_chart_type(), ChartType::BoxPlot);
1054 }
1055
1056 #[test]
1059 fn test_data_series_new() {
1060 let series = DataSeries::new("Sales");
1061 assert_eq!(series.name, "Sales");
1062 assert!(series.points.is_empty());
1063 assert!(series.show_points);
1064 assert!(!series.fill);
1065 }
1066
1067 #[test]
1068 fn test_data_series_point() {
1069 let series = DataSeries::new("Data")
1070 .point(1.0, 10.0)
1071 .point(2.0, 20.0)
1072 .point(3.0, 15.0);
1073 assert_eq!(series.points.len(), 3);
1074 assert_eq!(series.points[0], (1.0, 10.0));
1075 }
1076
1077 #[test]
1078 fn test_data_series_points() {
1079 let data = vec![(1.0, 5.0), (2.0, 10.0), (3.0, 7.0)];
1080 let series = DataSeries::new("Data").points(data);
1081 assert_eq!(series.points.len(), 3);
1082 }
1083
1084 #[test]
1085 fn test_data_series_color() {
1086 let series = DataSeries::new("Data").color(Color::RED);
1087 assert_eq!(series.color, Color::RED);
1088 }
1089
1090 #[test]
1091 fn test_data_series_line_width() {
1092 let series = DataSeries::new("Data").line_width(3.0);
1093 assert_eq!(series.line_width, 3.0);
1094 }
1095
1096 #[test]
1097 fn test_data_series_line_width_min() {
1098 let series = DataSeries::new("Data").line_width(0.1);
1099 assert_eq!(series.line_width, 0.5);
1100 }
1101
1102 #[test]
1103 fn test_data_series_point_size() {
1104 let series = DataSeries::new("Data").point_size(6.0);
1105 assert_eq!(series.point_size, 6.0);
1106 }
1107
1108 #[test]
1109 fn test_data_series_point_size_min() {
1110 let series = DataSeries::new("Data").point_size(0.5);
1111 assert_eq!(series.point_size, 1.0);
1112 }
1113
1114 #[test]
1115 fn test_data_series_show_points() {
1116 let series = DataSeries::new("Data").show_points(false);
1117 assert!(!series.show_points);
1118 }
1119
1120 #[test]
1121 fn test_data_series_fill() {
1122 let series = DataSeries::new("Data").fill(true);
1123 assert!(series.fill);
1124 }
1125
1126 #[test]
1127 fn test_data_series_x_range() {
1128 let series = DataSeries::new("Data")
1129 .point(1.0, 10.0)
1130 .point(5.0, 20.0)
1131 .point(3.0, 15.0);
1132 assert_eq!(series.x_range(), Some((1.0, 5.0)));
1133 }
1134
1135 #[test]
1136 fn test_data_series_x_range_empty() {
1137 let series = DataSeries::new("Data");
1138 assert_eq!(series.x_range(), None);
1139 }
1140
1141 #[test]
1142 fn test_data_series_y_range() {
1143 let series = DataSeries::new("Data")
1144 .point(1.0, 10.0)
1145 .point(2.0, 30.0)
1146 .point(3.0, 5.0);
1147 assert_eq!(series.y_range(), Some((5.0, 30.0)));
1148 }
1149
1150 #[test]
1151 fn test_data_series_y_range_empty() {
1152 let series = DataSeries::new("Data");
1153 assert_eq!(series.y_range(), None);
1154 }
1155
1156 #[test]
1159 fn test_axis_default() {
1160 let axis = Axis::default();
1161 assert!(axis.label.is_none());
1162 assert!(axis.min.is_none());
1163 assert!(axis.max.is_none());
1164 assert_eq!(axis.grid_lines, 5);
1165 assert!(axis.show_grid);
1166 }
1167
1168 #[test]
1169 fn test_axis_label() {
1170 let axis = Axis::new().label("Time");
1171 assert_eq!(axis.label, Some("Time".to_string()));
1172 }
1173
1174 #[test]
1175 fn test_axis_min_max() {
1176 let axis = Axis::new().min(0.0).max(100.0);
1177 assert_eq!(axis.min, Some(0.0));
1178 assert_eq!(axis.max, Some(100.0));
1179 }
1180
1181 #[test]
1182 fn test_axis_range() {
1183 let axis = Axis::new().range(10.0, 50.0);
1184 assert_eq!(axis.min, Some(10.0));
1185 assert_eq!(axis.max, Some(50.0));
1186 }
1187
1188 #[test]
1189 fn test_axis_grid_lines() {
1190 let axis = Axis::new().grid_lines(10);
1191 assert_eq!(axis.grid_lines, 10);
1192 }
1193
1194 #[test]
1195 fn test_axis_grid_lines_min() {
1196 let axis = Axis::new().grid_lines(1);
1197 assert_eq!(axis.grid_lines, 2);
1198 }
1199
1200 #[test]
1201 fn test_axis_show_grid() {
1202 let axis = Axis::new().show_grid(false);
1203 assert!(!axis.show_grid);
1204 }
1205
1206 #[test]
1207 fn test_axis_colors() {
1208 let axis = Axis::new().color(Color::RED).grid_color(Color::BLUE);
1209 assert_eq!(axis.color, Color::RED);
1210 assert_eq!(axis.grid_color, Color::BLUE);
1211 }
1212
1213 #[test]
1216 fn test_legend_position_default() {
1217 assert_eq!(LegendPosition::default(), LegendPosition::TopRight);
1218 }
1219
1220 #[test]
1223 fn test_chart_new() {
1224 let chart = Chart::new();
1225 assert_eq!(chart.get_chart_type(), ChartType::Line);
1226 assert_eq!(chart.series_count(), 0);
1227 assert!(!chart.has_data());
1228 }
1229
1230 #[test]
1231 fn test_chart_line() {
1232 let chart = Chart::line();
1233 assert_eq!(chart.get_chart_type(), ChartType::Line);
1234 }
1235
1236 #[test]
1237 fn test_chart_bar() {
1238 let chart = Chart::bar();
1239 assert_eq!(chart.get_chart_type(), ChartType::Bar);
1240 }
1241
1242 #[test]
1243 fn test_chart_scatter() {
1244 let chart = Chart::scatter();
1245 assert_eq!(chart.get_chart_type(), ChartType::Scatter);
1246 }
1247
1248 #[test]
1249 fn test_chart_area() {
1250 let chart = Chart::area();
1251 assert_eq!(chart.get_chart_type(), ChartType::Area);
1252 }
1253
1254 #[test]
1255 fn test_chart_pie() {
1256 let chart = Chart::pie();
1257 assert_eq!(chart.get_chart_type(), ChartType::Pie);
1258 }
1259
1260 #[test]
1261 fn test_chart_builder() {
1262 let chart = Chart::new()
1263 .chart_type(ChartType::Bar)
1264 .series(DataSeries::new("Sales").point(1.0, 100.0))
1265 .series(DataSeries::new("Expenses").point(1.0, 80.0))
1266 .title("Revenue")
1267 .x_axis(Axis::new().label("Month"))
1268 .y_axis(Axis::new().label("Amount"))
1269 .legend(LegendPosition::BottomRight)
1270 .background(Color::WHITE)
1271 .padding(50.0)
1272 .width(600.0)
1273 .height(400.0)
1274 .accessible_name("Revenue chart")
1275 .test_id("revenue-chart");
1276
1277 assert_eq!(chart.get_chart_type(), ChartType::Bar);
1278 assert_eq!(chart.series_count(), 2);
1279 assert!(chart.has_data());
1280 assert_eq!(chart.get_title(), Some("Revenue"));
1281 assert_eq!(Widget::accessible_name(&chart), Some("Revenue chart"));
1282 assert_eq!(Widget::test_id(&chart), Some("revenue-chart"));
1283 }
1284
1285 #[test]
1286 fn test_chart_add_series() {
1287 let series_list = vec![DataSeries::new("A"), DataSeries::new("B")];
1288 let chart = Chart::new().add_series(series_list);
1289 assert_eq!(chart.series_count(), 2);
1290 }
1291
1292 #[test]
1295 fn test_chart_data_bounds() {
1296 let chart = Chart::new()
1297 .series(DataSeries::new("S1").point(0.0, 10.0).point(5.0, 20.0))
1298 .series(DataSeries::new("S2").point(1.0, 5.0).point(4.0, 25.0));
1299
1300 let bounds = chart.data_bounds().unwrap();
1301 assert_eq!(bounds.0, 0.0); assert_eq!(bounds.1, 5.0); assert_eq!(bounds.2, 5.0); assert_eq!(bounds.3, 25.0); }
1306
1307 #[test]
1308 fn test_chart_data_bounds_with_axis_override() {
1309 let chart = Chart::new()
1310 .series(DataSeries::new("S1").point(0.0, 10.0).point(5.0, 20.0))
1311 .x_axis(Axis::new().min(-5.0).max(10.0))
1312 .y_axis(Axis::new().min(0.0).max(50.0));
1313
1314 let bounds = chart.data_bounds().unwrap();
1315 assert_eq!(bounds.0, -5.0); assert_eq!(bounds.1, 10.0); assert_eq!(bounds.2, 0.0); assert_eq!(bounds.3, 50.0); }
1320
1321 #[test]
1322 fn test_chart_data_bounds_empty() {
1323 let chart = Chart::new();
1324 assert!(chart.data_bounds().is_none());
1325 }
1326
1327 #[test]
1330 fn test_chart_padding_min() {
1331 let chart = Chart::new().padding(-10.0);
1332 assert_eq!(chart.padding, 0.0);
1333 }
1334
1335 #[test]
1336 fn test_chart_width_min() {
1337 let chart = Chart::new().width(50.0);
1338 assert_eq!(chart.width, Some(100.0));
1339 }
1340
1341 #[test]
1342 fn test_chart_height_min() {
1343 let chart = Chart::new().height(50.0);
1344 assert_eq!(chart.height, Some(100.0));
1345 }
1346
1347 #[test]
1350 fn test_chart_type_id() {
1351 let chart = Chart::new();
1352 assert_eq!(Widget::type_id(&chart), TypeId::of::<Chart>());
1353 }
1354
1355 #[test]
1356 fn test_chart_measure_default() {
1357 let chart = Chart::new();
1358 let size = chart.measure(Constraints::loose(Size::new(1000.0, 1000.0)));
1359 assert_eq!(size.width, 400.0);
1360 assert_eq!(size.height, 300.0);
1361 }
1362
1363 #[test]
1364 fn test_chart_measure_custom() {
1365 let chart = Chart::new().width(600.0).height(400.0);
1366 let size = chart.measure(Constraints::loose(Size::new(1000.0, 1000.0)));
1367 assert_eq!(size.width, 600.0);
1368 assert_eq!(size.height, 400.0);
1369 }
1370
1371 #[test]
1372 fn test_chart_layout() {
1373 let mut chart = Chart::new();
1374 let bounds = Rect::new(10.0, 20.0, 500.0, 300.0);
1375 let result = chart.layout(bounds);
1376 assert_eq!(result.size, Size::new(500.0, 300.0));
1377 assert_eq!(chart.bounds, bounds);
1378 }
1379
1380 #[test]
1381 fn test_chart_children() {
1382 let chart = Chart::new();
1383 assert!(chart.children().is_empty());
1384 }
1385
1386 #[test]
1387 fn test_chart_is_interactive() {
1388 let chart = Chart::new();
1389 assert!(!chart.is_interactive());
1390 }
1391
1392 #[test]
1393 fn test_chart_is_focusable() {
1394 let chart = Chart::new();
1395 assert!(!chart.is_focusable());
1396 }
1397
1398 #[test]
1399 fn test_chart_accessible_role() {
1400 let chart = Chart::new();
1401 assert_eq!(chart.accessible_role(), AccessibleRole::Image);
1402 }
1403
1404 #[test]
1405 fn test_chart_accessible_name_from_title() {
1406 let chart = Chart::new().title("Sales Chart");
1407 assert_eq!(Widget::accessible_name(&chart), Some("Sales Chart"));
1408 }
1409
1410 #[test]
1411 fn test_chart_accessible_name_explicit() {
1412 let chart = Chart::new()
1413 .title("Sales Chart")
1414 .accessible_name("Custom name");
1415 assert_eq!(Widget::accessible_name(&chart), Some("Custom name"));
1416 }
1417
1418 #[test]
1419 fn test_chart_test_id() {
1420 let chart = Chart::new().test_id("my-chart");
1421 assert_eq!(Widget::test_id(&chart), Some("my-chart"));
1422 }
1423
1424 #[test]
1427 fn test_chart_plot_area_no_title() {
1428 let mut chart = Chart::new().padding(40.0);
1429 chart.bounds = Rect::new(0.0, 0.0, 400.0, 300.0);
1430 let plot = chart.plot_area();
1431 assert_eq!(plot.x, 40.0);
1432 assert_eq!(plot.y, 40.0);
1433 assert_eq!(plot.width, 320.0);
1434 assert_eq!(plot.height, 220.0);
1435 }
1436
1437 #[test]
1438 fn test_chart_plot_area_with_title() {
1439 let mut chart = Chart::new().padding(40.0).title("Test");
1440 chart.bounds = Rect::new(0.0, 0.0, 400.0, 300.0);
1441 let plot = chart.plot_area();
1442 assert_eq!(plot.y, 70.0); }
1444
1445 #[test]
1448 fn test_chart_map_point() {
1449 let chart = Chart::new();
1450 let bounds = (0.0, 10.0, 0.0, 100.0);
1451 let plot = Rect::new(0.0, 0.0, 100.0, 100.0);
1452
1453 let pt = chart.map_point(5.0, 50.0, &bounds, &plot);
1454 assert!((pt.x - 50.0).abs() < 0.1);
1455 assert!((pt.y - 50.0).abs() < 0.1);
1456 }
1457
1458 #[test]
1459 fn test_chart_map_point_origin() {
1460 let chart = Chart::new();
1461 let bounds = (0.0, 10.0, 0.0, 100.0);
1462 let plot = Rect::new(0.0, 0.0, 100.0, 100.0);
1463
1464 let pt = chart.map_point(0.0, 0.0, &bounds, &plot);
1465 assert!((pt.x - 0.0).abs() < 0.1);
1466 assert!((pt.y - 100.0).abs() < 0.1); }
1468
1469 #[test]
1472 fn test_chart_has_data_empty_series() {
1473 let chart = Chart::new().series(DataSeries::new("Empty"));
1474 assert!(!chart.has_data());
1475 }
1476
1477 #[test]
1478 fn test_chart_has_data_with_points() {
1479 let chart = Chart::new().series(DataSeries::new("Data").point(1.0, 1.0));
1480 assert!(chart.has_data());
1481 }
1482
1483 #[test]
1488 fn test_data_series_eq() {
1489 let s1 = DataSeries::new("A").point(1.0, 2.0);
1490 let s2 = DataSeries::new("A").point(1.0, 2.0);
1491 assert_eq!(s1, s2);
1492 }
1493
1494 #[test]
1495 fn test_chart_type_eq() {
1496 assert_eq!(ChartType::Line, ChartType::Line);
1497 assert_ne!(ChartType::Line, ChartType::Bar);
1498 }
1499
1500 #[test]
1501 fn test_legend_position_all_variants() {
1502 let positions = [
1503 LegendPosition::None,
1504 LegendPosition::TopRight,
1505 LegendPosition::TopLeft,
1506 LegendPosition::BottomRight,
1507 LegendPosition::BottomLeft,
1508 ];
1509 assert_eq!(positions.len(), 5);
1510 }
1511
1512 #[test]
1513 fn test_chart_children_mut() {
1514 let mut chart = Chart::new();
1515 assert!(chart.children_mut().is_empty());
1516 }
1517
1518 #[test]
1519 fn test_chart_event_returns_none() {
1520 let mut chart = Chart::new();
1521 let result = chart.event(&presentar_core::Event::KeyDown {
1522 key: presentar_core::Key::Down,
1523 });
1524 assert!(result.is_none());
1525 }
1526
1527 #[test]
1528 fn test_axis_default_colors() {
1529 let axis = Axis::default();
1530 assert_eq!(axis.color.a, 1.0);
1531 assert_eq!(axis.grid_color.a, 1.0);
1532 }
1533
1534 #[test]
1535 fn test_chart_get_series() {
1536 let chart = Chart::new()
1537 .series(DataSeries::new("A"))
1538 .series(DataSeries::new("B"));
1539 assert_eq!(chart.get_series().len(), 2);
1540 assert_eq!(chart.get_series()[0].name, "A");
1541 }
1542
1543 #[test]
1544 fn test_chart_histogram() {
1545 let chart = Chart::new().chart_type(ChartType::Histogram);
1546 assert_eq!(chart.get_chart_type(), ChartType::Histogram);
1547 }
1548
1549 #[test]
1550 fn test_chart_data_bounds_single_point() {
1551 let chart = Chart::new().series(DataSeries::new("S").point(5.0, 10.0));
1552 let bounds = chart.data_bounds().unwrap();
1553 assert_eq!(bounds.0, 5.0); assert_eq!(bounds.1, 5.0); }
1556
1557 #[test]
1558 fn test_chart_legend_none() {
1559 let chart = Chart::new().legend(LegendPosition::None);
1560 assert_eq!(chart.legend, LegendPosition::None);
1561 }
1562
1563 #[test]
1564 fn test_chart_legend_top_left() {
1565 let chart = Chart::new().legend(LegendPosition::TopLeft);
1566 assert_eq!(chart.legend, LegendPosition::TopLeft);
1567 }
1568
1569 #[test]
1570 fn test_chart_legend_bottom_left() {
1571 let chart = Chart::new().legend(LegendPosition::BottomLeft);
1572 assert_eq!(chart.legend, LegendPosition::BottomLeft);
1573 }
1574
1575 #[test]
1576 fn test_chart_test_id_none() {
1577 let chart = Chart::new();
1578 assert!(Widget::test_id(&chart).is_none());
1579 }
1580
1581 #[test]
1582 fn test_chart_accessible_name_none() {
1583 let chart = Chart::new();
1584 assert!(Widget::accessible_name(&chart).is_none());
1585 }
1586
1587 #[test]
1588 fn test_data_series_default_values() {
1589 let series = DataSeries::new("Test");
1590 assert_eq!(series.line_width, 2.0);
1591 assert_eq!(series.point_size, 4.0);
1592 }
1593}