1use presentar_core::{
4 widget::{AccessibleRole, LayoutResult, TextStyle},
5 Brick, BrickAssertion, BrickBudget, BrickVerification, Canvas, Color, Constraints, Point, Rect,
6 Size, TypeId, Widget,
7};
8use serde::{Deserialize, Serialize};
9use std::any::Any;
10use std::time::Duration;
11
12#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
14pub enum ChartType {
15 #[default]
17 Line,
18 Bar,
20 Scatter,
22 Area,
24 Pie,
26 Histogram,
28 Heatmap,
30 BoxPlot,
32}
33
34#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
36pub struct DataSeries {
37 pub name: String,
39 pub points: Vec<(f64, f64)>,
41 pub color: Color,
43 pub line_width: f32,
45 pub point_size: f32,
47 pub show_points: bool,
49 pub fill: bool,
51}
52
53impl DataSeries {
54 #[must_use]
56 pub fn new(name: impl Into<String>) -> Self {
57 Self {
58 name: name.into(),
59 points: Vec::new(),
60 color: Color::new(0.2, 0.47, 0.96, 1.0),
61 line_width: 2.0,
62 point_size: 4.0,
63 show_points: true,
64 fill: false,
65 }
66 }
67
68 #[must_use]
70 pub fn point(mut self, x: f64, y: f64) -> Self {
71 self.points.push((x, y));
72 self
73 }
74
75 #[must_use]
77 pub fn points(mut self, points: impl IntoIterator<Item = (f64, f64)>) -> Self {
78 self.points.extend(points);
79 self
80 }
81
82 #[must_use]
84 pub const fn color(mut self, color: Color) -> Self {
85 self.color = color;
86 self
87 }
88
89 #[must_use]
91 pub fn line_width(mut self, width: f32) -> Self {
92 self.line_width = width.max(0.5);
93 self
94 }
95
96 #[must_use]
98 pub fn point_size(mut self, size: f32) -> Self {
99 self.point_size = size.max(1.0);
100 self
101 }
102
103 #[must_use]
105 pub const fn show_points(mut self, show: bool) -> Self {
106 self.show_points = show;
107 self
108 }
109
110 #[must_use]
112 pub const fn fill(mut self, fill: bool) -> Self {
113 self.fill = fill;
114 self
115 }
116
117 #[must_use]
119 pub fn x_range(&self) -> Option<(f64, f64)> {
120 if self.points.is_empty() {
121 return None;
122 }
123 let min = self
124 .points
125 .iter()
126 .map(|(x, _)| *x)
127 .fold(f64::INFINITY, f64::min);
128 let max = self
129 .points
130 .iter()
131 .map(|(x, _)| *x)
132 .fold(f64::NEG_INFINITY, f64::max);
133 Some((min, max))
134 }
135
136 #[must_use]
138 pub fn y_range(&self) -> Option<(f64, f64)> {
139 if self.points.is_empty() {
140 return None;
141 }
142 let min = self
143 .points
144 .iter()
145 .map(|(_, y)| *y)
146 .fold(f64::INFINITY, f64::min);
147 let max = self
148 .points
149 .iter()
150 .map(|(_, y)| *y)
151 .fold(f64::NEG_INFINITY, f64::max);
152 Some((min, max))
153 }
154}
155
156#[derive(Debug, Clone, Serialize, Deserialize)]
158pub struct Axis {
159 pub label: Option<String>,
161 pub min: Option<f64>,
163 pub max: Option<f64>,
165 pub grid_lines: usize,
167 pub show_grid: bool,
169 pub color: Color,
171 pub grid_color: Color,
173}
174
175impl Default for Axis {
176 fn default() -> Self {
177 Self {
178 label: None,
179 min: None,
180 max: None,
181 grid_lines: 5,
182 show_grid: true,
183 color: Color::new(0.3, 0.3, 0.3, 1.0),
184 grid_color: Color::new(0.9, 0.9, 0.9, 1.0),
185 }
186 }
187}
188
189impl Axis {
190 #[must_use]
192 pub fn new() -> Self {
193 Self::default()
194 }
195
196 #[must_use]
198 pub fn label(mut self, label: impl Into<String>) -> Self {
199 self.label = Some(label.into());
200 self
201 }
202
203 #[must_use]
205 pub const fn min(mut self, min: f64) -> Self {
206 self.min = Some(min);
207 self
208 }
209
210 #[must_use]
212 pub const fn max(mut self, max: f64) -> Self {
213 self.max = Some(max);
214 self
215 }
216
217 #[must_use]
219 pub const fn range(mut self, min: f64, max: f64) -> Self {
220 self.min = Some(min);
221 self.max = Some(max);
222 self
223 }
224
225 #[must_use]
227 pub fn grid_lines(mut self, count: usize) -> Self {
228 self.grid_lines = count.max(2);
229 self
230 }
231
232 #[must_use]
234 pub const fn show_grid(mut self, show: bool) -> Self {
235 self.show_grid = show;
236 self
237 }
238
239 #[must_use]
241 pub const fn color(mut self, color: Color) -> Self {
242 self.color = color;
243 self
244 }
245
246 #[must_use]
248 pub const fn grid_color(mut self, color: Color) -> Self {
249 self.grid_color = color;
250 self
251 }
252}
253
254#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
256pub enum LegendPosition {
257 None,
259 #[default]
261 TopRight,
262 TopLeft,
264 BottomRight,
266 BottomLeft,
268}
269
270#[derive(Debug, Clone, Serialize, Deserialize)]
272pub struct Chart {
273 kind: ChartType,
275 series: Vec<DataSeries>,
277 title: Option<String>,
279 x_axis: Axis,
281 y_axis: Axis,
283 legend: LegendPosition,
285 background: Color,
287 padding: f32,
289 width: Option<f32>,
291 height: Option<f32>,
293 accessible_name_value: Option<String>,
295 test_id_value: Option<String>,
297 #[serde(skip)]
299 bounds: Rect,
300}
301
302impl Default for Chart {
303 fn default() -> Self {
304 Self {
305 kind: ChartType::Line,
306 series: Vec::new(),
307 title: None,
308 x_axis: Axis::default(),
309 y_axis: Axis::default(),
310 legend: LegendPosition::TopRight,
311 background: Color::WHITE,
312 padding: 40.0,
313 width: None,
314 height: None,
315 accessible_name_value: None,
316 test_id_value: None,
317 bounds: Rect::default(),
318 }
319 }
320}
321
322impl Chart {
323 #[must_use]
325 pub fn new() -> Self {
326 Self::default()
327 }
328
329 #[must_use]
331 pub fn line() -> Self {
332 Self::new().chart_type(ChartType::Line)
333 }
334
335 #[must_use]
337 pub fn bar() -> Self {
338 Self::new().chart_type(ChartType::Bar)
339 }
340
341 #[must_use]
343 pub fn scatter() -> Self {
344 Self::new().chart_type(ChartType::Scatter)
345 }
346
347 #[must_use]
349 pub fn area() -> Self {
350 Self::new().chart_type(ChartType::Area)
351 }
352
353 #[must_use]
355 pub fn pie() -> Self {
356 Self::new().chart_type(ChartType::Pie)
357 }
358
359 #[must_use]
361 pub fn heatmap() -> Self {
362 Self::new().chart_type(ChartType::Heatmap)
363 }
364
365 #[must_use]
367 pub fn boxplot() -> Self {
368 Self::new().chart_type(ChartType::BoxPlot)
369 }
370
371 #[must_use]
373 pub const fn chart_type(mut self, chart_type: ChartType) -> Self {
374 self.kind = chart_type;
375 self
376 }
377
378 #[must_use]
380 pub fn series(mut self, series: DataSeries) -> Self {
381 self.series.push(series);
382 self
383 }
384
385 #[must_use]
387 pub fn add_series(mut self, series: impl IntoIterator<Item = DataSeries>) -> Self {
388 self.series.extend(series);
389 self
390 }
391
392 #[must_use]
394 pub fn title(mut self, title: impl Into<String>) -> Self {
395 self.title = Some(title.into());
396 self
397 }
398
399 #[must_use]
401 pub fn x_axis(mut self, axis: Axis) -> Self {
402 self.x_axis = axis;
403 self
404 }
405
406 #[must_use]
408 pub fn y_axis(mut self, axis: Axis) -> Self {
409 self.y_axis = axis;
410 self
411 }
412
413 #[must_use]
415 pub const fn legend(mut self, position: LegendPosition) -> Self {
416 self.legend = position;
417 self
418 }
419
420 #[must_use]
422 pub const fn background(mut self, color: Color) -> Self {
423 self.background = color;
424 self
425 }
426
427 #[must_use]
429 pub fn padding(mut self, padding: f32) -> Self {
430 self.padding = padding.max(0.0);
431 self
432 }
433
434 #[must_use]
436 pub fn width(mut self, width: f32) -> Self {
437 self.width = Some(width.max(100.0));
438 self
439 }
440
441 #[must_use]
443 pub fn height(mut self, height: f32) -> Self {
444 self.height = Some(height.max(100.0));
445 self
446 }
447
448 #[must_use]
450 pub fn accessible_name(mut self, name: impl Into<String>) -> Self {
451 self.accessible_name_value = Some(name.into());
452 self
453 }
454
455 #[must_use]
457 pub fn test_id(mut self, id: impl Into<String>) -> Self {
458 self.test_id_value = Some(id.into());
459 self
460 }
461
462 #[must_use]
464 pub const fn get_chart_type(&self) -> ChartType {
465 self.kind
466 }
467
468 #[must_use]
470 pub fn get_series(&self) -> &[DataSeries] {
471 &self.series
472 }
473
474 #[must_use]
476 pub fn series_count(&self) -> usize {
477 self.series.len()
478 }
479
480 #[must_use]
482 pub fn has_data(&self) -> bool {
483 self.series.iter().any(|s| !s.points.is_empty())
484 }
485
486 #[must_use]
488 pub fn get_title(&self) -> Option<&str> {
489 self.title.as_deref()
490 }
491
492 #[must_use]
494 pub fn data_bounds(&self) -> Option<(f64, f64, f64, f64)> {
495 if !self.has_data() {
496 return None;
497 }
498
499 let mut x_min = f64::INFINITY;
500 let mut x_max = f64::NEG_INFINITY;
501 let mut y_min = f64::INFINITY;
502 let mut y_max = f64::NEG_INFINITY;
503
504 for series in &self.series {
505 if let Some((min, max)) = series.x_range() {
506 x_min = x_min.min(min);
507 x_max = x_max.max(max);
508 }
509 if let Some((min, max)) = series.y_range() {
510 y_min = y_min.min(min);
511 y_max = y_max.max(max);
512 }
513 }
514
515 if let Some(min) = self.x_axis.min {
517 x_min = min;
518 }
519 if let Some(max) = self.x_axis.max {
520 x_max = max;
521 }
522 if let Some(min) = self.y_axis.min {
523 y_min = min;
524 }
525 if let Some(max) = self.y_axis.max {
526 y_max = max;
527 }
528
529 Some((x_min, x_max, y_min, y_max))
530 }
531
532 fn plot_area(&self) -> Rect {
534 let title_height = if self.title.is_some() { 30.0 } else { 0.0 };
535 Rect::new(
536 self.bounds.x + self.padding,
537 self.bounds.y + self.padding + title_height,
538 self.padding.mul_add(-2.0, self.bounds.width),
539 self.padding.mul_add(-2.0, self.bounds.height) - title_height,
540 )
541 }
542
543 fn map_point(&self, x: f64, y: f64, bounds: &(f64, f64, f64, f64), plot: &Rect) -> Point {
545 let (x_min, x_max, y_min, y_max) = *bounds;
546 let x_range = (x_max - x_min).max(1e-10);
547 let y_range = (y_max - y_min).max(1e-10);
548
549 let px = (((x - x_min) / x_range) as f32).mul_add(plot.width, plot.x);
550 let py = (((y - y_min) / y_range) as f32).mul_add(-plot.height, plot.y + plot.height);
551
552 Point::new(px, py)
553 }
554
555 fn paint_grid(&self, canvas: &mut dyn Canvas, plot: &Rect, bounds: &(f64, f64, f64, f64)) {
557 let (x_min, x_max, y_min, y_max) = *bounds;
558
559 if self.x_axis.show_grid {
561 for i in 0..=self.x_axis.grid_lines {
562 let t = i as f32 / self.x_axis.grid_lines as f32;
563 let x = t.mul_add(plot.width, plot.x);
564 canvas.draw_line(
565 Point::new(x, plot.y),
566 Point::new(x, plot.y + plot.height),
567 self.x_axis.grid_color,
568 1.0,
569 );
570 }
571 }
572
573 if self.y_axis.show_grid {
575 for i in 0..=self.y_axis.grid_lines {
576 let t = i as f32 / self.y_axis.grid_lines as f32;
577 let y = t.mul_add(plot.height, plot.y);
578 canvas.draw_line(
579 Point::new(plot.x, y),
580 Point::new(plot.x + plot.width, y),
581 self.y_axis.grid_color,
582 1.0,
583 );
584 }
585 }
586
587 let text_style = TextStyle {
589 size: 10.0,
590 color: self.x_axis.color,
591 ..TextStyle::default()
592 };
593
594 for i in 0..=self.x_axis.grid_lines {
596 let t = i as f64 / self.x_axis.grid_lines as f64;
597 let value = t.mul_add(x_max - x_min, x_min);
598 let x = (t as f32).mul_add(plot.width, plot.x);
599 canvas.draw_text(
600 &format!("{value:.1}"),
601 Point::new(x, plot.y + plot.height + 15.0),
602 &text_style,
603 );
604 }
605
606 for i in 0..=self.y_axis.grid_lines {
608 let t = i as f64 / self.y_axis.grid_lines as f64;
609 let value = t.mul_add(-(y_max - y_min), y_max);
610 let y = (t as f32).mul_add(plot.height, plot.y);
611 canvas.draw_text(
612 &format!("{value:.1}"),
613 Point::new(plot.x - 35.0, y + 4.0),
614 &text_style,
615 );
616 }
617 }
618
619 fn paint_line(&self, canvas: &mut dyn Canvas, plot: &Rect, bounds: &(f64, f64, f64, f64)) {
621 for series in &self.series {
622 if series.points.len() < 2 {
623 continue;
624 }
625
626 let path_points: Vec<Point> = series
628 .points
629 .iter()
630 .map(|&(x, y)| self.map_point(x, y, bounds, plot))
631 .collect();
632
633 canvas.draw_path(&path_points, series.color, series.line_width);
635
636 if series.fill {
638 let mut fill_points = path_points.clone();
639 if let (Some(first), Some(last)) = (path_points.first(), path_points.last()) {
641 fill_points.push(Point::new(last.x, plot.y + plot.height));
642 fill_points.push(Point::new(first.x, plot.y + plot.height));
643 }
644 let mut fill_color = series.color;
645 fill_color.a = 0.3; canvas.fill_polygon(&fill_points, fill_color);
647 }
648
649 if series.show_points {
651 for &(x, y) in &series.points {
652 let pt = self.map_point(x, y, bounds, plot);
653 canvas.fill_circle(pt, series.point_size / 2.0, series.color);
654 }
655 }
656 }
657 }
658
659 fn paint_bar(&self, canvas: &mut dyn Canvas, plot: &Rect, bounds: &(f64, f64, f64, f64)) {
661 let (_, _, y_min, y_max) = *bounds;
662 let y_range = (y_max - y_min).max(1e-10);
663
664 let series_count = self.series.len();
665 if series_count == 0 {
666 return;
667 }
668
669 let max_points = self
671 .series
672 .iter()
673 .map(|s| s.points.len())
674 .max()
675 .unwrap_or(1);
676 let group_width = plot.width / max_points as f32;
677 let bar_width = (group_width * 0.8) / series_count as f32;
678 let bar_gap = group_width * 0.1;
679
680 for (si, series) in self.series.iter().enumerate() {
681 for (i, &(_, y)) in series.points.iter().enumerate() {
682 let bar_height = ((y - y_min) / y_range) as f32 * plot.height;
683 let x = (si as f32)
684 .mul_add(bar_width, (i as f32).mul_add(group_width, plot.x + bar_gap));
685 let rect = Rect::new(
686 x,
687 plot.y + plot.height - bar_height,
688 bar_width - 2.0,
689 bar_height,
690 );
691 canvas.fill_rect(rect, series.color);
692 }
693 }
694 }
695
696 fn paint_scatter(&self, canvas: &mut dyn Canvas, plot: &Rect, bounds: &(f64, f64, f64, f64)) {
698 for series in &self.series {
699 for &(x, y) in &series.points {
700 let pt = self.map_point(x, y, bounds, plot);
701 canvas.fill_circle(pt, series.point_size / 2.0, series.color);
702 }
703 }
704 }
705
706 fn paint_pie(&self, canvas: &mut dyn Canvas, plot: &Rect) {
708 let total: f64 = self
710 .series
711 .iter()
712 .flat_map(|s| s.points.iter().map(|(_, y)| *y))
713 .sum();
714
715 if total <= 0.0 {
716 return;
717 }
718
719 let cx = plot.x + plot.width / 2.0;
720 let cy = plot.y + plot.height / 2.0;
721 let radius = plot.width.min(plot.height) / 2.0 * 0.8;
722 let center = Point::new(cx, cy);
723
724 let mut start_angle: f32 = -std::f32::consts::FRAC_PI_2; for series in &self.series {
727 for &(_, y) in &series.points {
728 let fraction = (y / total) as f32;
729 let sweep = fraction * std::f32::consts::TAU;
730 let end_angle = start_angle + sweep;
731
732 canvas.fill_arc(center, radius, start_angle, end_angle, series.color);
733
734 start_angle = end_angle;
735 }
736 }
737 }
738
739 fn paint_heatmap(&self, canvas: &mut dyn Canvas, plot: &Rect, bounds: &(f64, f64, f64, f64)) {
741 let (_, _, y_min, y_max) = *bounds;
742 let y_range = (y_max - y_min).max(1e-10);
743
744 let row_count = self.series.len();
746 if row_count == 0 {
747 return;
748 }
749
750 let col_count = self
751 .series
752 .iter()
753 .map(|s| s.points.len())
754 .max()
755 .unwrap_or(1);
756
757 let cell_width = plot.width / col_count as f32;
758 let cell_height = plot.height / row_count as f32;
759
760 for (row, series) in self.series.iter().enumerate() {
761 for (col, &(_, value)) in series.points.iter().enumerate() {
762 let t = ((value - y_min) / y_range) as f32;
764 let color = Color::new(t, 0.2, 1.0 - t, 1.0);
765
766 let rect = Rect::new(
767 (col as f32).mul_add(cell_width, plot.x),
768 (row as f32).mul_add(cell_height, plot.y),
769 cell_width - 1.0,
770 cell_height - 1.0,
771 );
772 canvas.fill_rect(rect, color);
773 }
774 }
775 }
776
777 fn paint_boxplot(&self, canvas: &mut dyn Canvas, plot: &Rect, bounds: &(f64, f64, f64, f64)) {
779 let (_, _, y_min, y_max) = *bounds;
780 let y_range = (y_max - y_min).max(1e-10);
781
782 let series_count = self.series.len();
783 if series_count == 0 {
784 return;
785 }
786
787 let box_width = (plot.width / series_count as f32) * 0.6;
788 let gap = (plot.width / series_count as f32) * 0.2;
789
790 for (i, series) in self.series.iter().enumerate() {
791 if series.points.len() < 5 {
792 continue; }
794
795 let mut values: Vec<f64> = series.points.iter().map(|(_, y)| *y).collect();
797 values.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
798
799 let min_val = values[0];
800 let q1 = values[values.len() / 4];
801 let median = values[values.len() / 2];
802 let q3 = values[3 * values.len() / 4];
803 let max_val = values[values.len() - 1];
804
805 let x_center = (i as f32).mul_add(plot.width / series_count as f32, plot.x + gap);
806
807 let map_y = |v: f64| -> f32 {
809 let t = (v - y_min) / y_range;
810 (1.0 - t as f32).mul_add(plot.height, plot.y)
811 };
812
813 let y_min_px = map_y(min_val);
814 let y_q1 = map_y(q1);
815 let y_median = map_y(median);
816 let y_q3 = map_y(q3);
817 let y_max_px = map_y(max_val);
818
819 canvas.draw_line(
821 Point::new(x_center + box_width / 2.0, y_min_px),
822 Point::new(x_center + box_width / 2.0, y_q1),
823 series.color,
824 1.0,
825 );
826 canvas.draw_line(
827 Point::new(x_center + box_width / 2.0, y_q3),
828 Point::new(x_center + box_width / 2.0, y_max_px),
829 series.color,
830 1.0,
831 );
832
833 let box_rect = Rect::new(x_center, y_q3, box_width, y_q1 - y_q3);
835 canvas.fill_rect(box_rect, series.color);
836 canvas.stroke_rect(box_rect, Color::new(0.0, 0.0, 0.0, 1.0), 1.0);
837
838 canvas.draw_line(
840 Point::new(x_center, y_median),
841 Point::new(x_center + box_width, y_median),
842 Color::new(0.0, 0.0, 0.0, 1.0),
843 2.0,
844 );
845
846 let cap_width = box_width * 0.3;
848 canvas.draw_line(
849 Point::new(x_center + box_width / 2.0 - cap_width / 2.0, y_min_px),
850 Point::new(x_center + box_width / 2.0 + cap_width / 2.0, y_min_px),
851 series.color,
852 1.0,
853 );
854 canvas.draw_line(
855 Point::new(x_center + box_width / 2.0 - cap_width / 2.0, y_max_px),
856 Point::new(x_center + box_width / 2.0 + cap_width / 2.0, y_max_px),
857 series.color,
858 1.0,
859 );
860 }
861 }
862
863 fn paint_legend(&self, canvas: &mut dyn Canvas) {
865 if self.legend == LegendPosition::None || self.series.is_empty() {
866 return;
867 }
868
869 let entry_height = 20.0;
870 let legend_width = 100.0;
871 let legend_height = (self.series.len() as f32).mul_add(entry_height, 10.0);
872
873 let (lx, ly) = match self.legend {
874 LegendPosition::TopRight => (
875 self.bounds.x + self.bounds.width - legend_width - 10.0,
876 self.bounds.y + self.padding + 10.0,
877 ),
878 LegendPosition::TopLeft => (
879 self.bounds.x + self.padding + 10.0,
880 self.bounds.y + self.padding + 10.0,
881 ),
882 LegendPosition::BottomRight => (
883 self.bounds.x + self.bounds.width - legend_width - 10.0,
884 self.bounds.y + self.bounds.height - legend_height - 10.0,
885 ),
886 LegendPosition::BottomLeft => (
887 self.bounds.x + self.padding + 10.0,
888 self.bounds.y + self.bounds.height - legend_height - 10.0,
889 ),
890 LegendPosition::None => return,
891 };
892
893 canvas.fill_rect(
895 Rect::new(lx, ly, legend_width, legend_height),
896 Color::new(1.0, 1.0, 1.0, 0.9),
897 );
898 canvas.stroke_rect(
899 Rect::new(lx, ly, legend_width, legend_height),
900 Color::new(0.8, 0.8, 0.8, 1.0),
901 1.0,
902 );
903
904 let text_style = TextStyle {
906 size: 12.0,
907 color: Color::new(0.2, 0.2, 0.2, 1.0),
908 ..TextStyle::default()
909 };
910
911 for (i, series) in self.series.iter().enumerate() {
912 let ey = (i as f32).mul_add(entry_height, ly + 5.0);
913 canvas.fill_rect(Rect::new(lx + 5.0, ey + 4.0, 12.0, 12.0), series.color);
915 canvas.draw_text(&series.name, Point::new(lx + 22.0, ey + 14.0), &text_style);
917 }
918 }
919}
920
921impl Widget for Chart {
922 fn type_id(&self) -> TypeId {
923 TypeId::of::<Self>()
924 }
925
926 fn measure(&self, constraints: Constraints) -> Size {
927 let width = self.width.unwrap_or(400.0);
928 let height = self.height.unwrap_or(300.0);
929 constraints.constrain(Size::new(width, height))
930 }
931
932 fn layout(&mut self, bounds: Rect) -> LayoutResult {
933 self.bounds = bounds;
934 LayoutResult {
935 size: bounds.size(),
936 }
937 }
938
939 fn paint(&self, canvas: &mut dyn Canvas) {
940 canvas.fill_rect(self.bounds, self.background);
942
943 if let Some(ref title) = self.title {
945 let text_style = TextStyle {
946 size: 16.0,
947 color: Color::new(0.1, 0.1, 0.1, 1.0),
948 ..TextStyle::default()
949 };
950 canvas.draw_text(
951 title,
952 Point::new(
953 (title.len() as f32).mul_add(-4.0, self.bounds.x + self.bounds.width / 2.0),
954 self.bounds.y + 25.0,
955 ),
956 &text_style,
957 );
958 }
959
960 let plot = self.plot_area();
961
962 let Some(bounds) = self.data_bounds() else {
964 return;
965 };
966
967 self.paint_grid(canvas, &plot, &bounds);
969
970 match self.kind {
972 ChartType::Line | ChartType::Area => self.paint_line(canvas, &plot, &bounds),
973 ChartType::Bar | ChartType::Histogram => self.paint_bar(canvas, &plot, &bounds),
974 ChartType::Scatter => self.paint_scatter(canvas, &plot, &bounds),
975 ChartType::Pie => self.paint_pie(canvas, &plot),
976 ChartType::Heatmap => self.paint_heatmap(canvas, &plot, &bounds),
977 ChartType::BoxPlot => self.paint_boxplot(canvas, &plot, &bounds),
978 }
979
980 self.paint_legend(canvas);
982 }
983
984 fn event(&mut self, _event: &presentar_core::Event) -> Option<Box<dyn Any + Send>> {
985 None
987 }
988
989 fn children(&self) -> &[Box<dyn Widget>] {
990 &[]
991 }
992
993 fn children_mut(&mut self) -> &mut [Box<dyn Widget>] {
994 &mut []
995 }
996
997 fn is_interactive(&self) -> bool {
998 false
999 }
1000
1001 fn is_focusable(&self) -> bool {
1002 false
1003 }
1004
1005 fn accessible_name(&self) -> Option<&str> {
1006 self.accessible_name_value
1007 .as_deref()
1008 .or(self.title.as_deref())
1009 }
1010
1011 fn accessible_role(&self) -> AccessibleRole {
1012 AccessibleRole::Image }
1014
1015 fn test_id(&self) -> Option<&str> {
1016 self.test_id_value.as_deref()
1017 }
1018}
1019
1020impl Brick for Chart {
1022 fn brick_name(&self) -> &'static str {
1023 "Chart"
1024 }
1025
1026 fn assertions(&self) -> &[BrickAssertion] {
1027 &[BrickAssertion::MaxLatencyMs(16)]
1028 }
1029
1030 fn budget(&self) -> BrickBudget {
1031 BrickBudget::uniform(16)
1032 }
1033
1034 fn verify(&self) -> BrickVerification {
1035 BrickVerification {
1036 passed: self.assertions().to_vec(),
1037 failed: vec![],
1038 verification_time: Duration::from_micros(10),
1039 }
1040 }
1041
1042 fn to_html(&self) -> String {
1043 let test_id = self.test_id_value.as_deref().unwrap_or("chart");
1044 let title = self.title.as_deref().unwrap_or("Chart");
1045 format!(
1046 r#"<div class="brick-chart" data-testid="{test_id}" role="img" aria-label="{title}">{title}</div>"#
1047 )
1048 }
1049
1050 fn to_css(&self) -> String {
1051 ".brick-chart { display: block; }".into()
1052 }
1053
1054 fn test_id(&self) -> Option<&str> {
1055 self.test_id_value.as_deref()
1056 }
1057}
1058
1059#[cfg(test)]
1060mod tests {
1061 use super::*;
1062
1063 #[test]
1066 fn test_chart_type_default() {
1067 assert_eq!(ChartType::default(), ChartType::Line);
1068 }
1069
1070 #[test]
1071 fn test_chart_type_variants() {
1072 let types = [
1073 ChartType::Line,
1074 ChartType::Bar,
1075 ChartType::Scatter,
1076 ChartType::Area,
1077 ChartType::Pie,
1078 ChartType::Histogram,
1079 ChartType::Heatmap,
1080 ChartType::BoxPlot,
1081 ];
1082 assert_eq!(types.len(), 8);
1083 }
1084
1085 #[test]
1086 fn test_chart_heatmap() {
1087 let chart = Chart::new().chart_type(ChartType::Heatmap);
1088 assert_eq!(chart.get_chart_type(), ChartType::Heatmap);
1089 }
1090
1091 #[test]
1092 fn test_chart_boxplot() {
1093 let chart = Chart::new().chart_type(ChartType::BoxPlot);
1094 assert_eq!(chart.get_chart_type(), ChartType::BoxPlot);
1095 }
1096
1097 #[test]
1100 fn test_data_series_new() {
1101 let series = DataSeries::new("Sales");
1102 assert_eq!(series.name, "Sales");
1103 assert!(series.points.is_empty());
1104 assert!(series.show_points);
1105 assert!(!series.fill);
1106 }
1107
1108 #[test]
1109 fn test_data_series_point() {
1110 let series = DataSeries::new("Data")
1111 .point(1.0, 10.0)
1112 .point(2.0, 20.0)
1113 .point(3.0, 15.0);
1114 assert_eq!(series.points.len(), 3);
1115 assert_eq!(series.points[0], (1.0, 10.0));
1116 }
1117
1118 #[test]
1119 fn test_data_series_points() {
1120 let data = vec![(1.0, 5.0), (2.0, 10.0), (3.0, 7.0)];
1121 let series = DataSeries::new("Data").points(data);
1122 assert_eq!(series.points.len(), 3);
1123 }
1124
1125 #[test]
1126 fn test_data_series_color() {
1127 let series = DataSeries::new("Data").color(Color::RED);
1128 assert_eq!(series.color, Color::RED);
1129 }
1130
1131 #[test]
1132 fn test_data_series_line_width() {
1133 let series = DataSeries::new("Data").line_width(3.0);
1134 assert_eq!(series.line_width, 3.0);
1135 }
1136
1137 #[test]
1138 fn test_data_series_line_width_min() {
1139 let series = DataSeries::new("Data").line_width(0.1);
1140 assert_eq!(series.line_width, 0.5);
1141 }
1142
1143 #[test]
1144 fn test_data_series_point_size() {
1145 let series = DataSeries::new("Data").point_size(6.0);
1146 assert_eq!(series.point_size, 6.0);
1147 }
1148
1149 #[test]
1150 fn test_data_series_point_size_min() {
1151 let series = DataSeries::new("Data").point_size(0.5);
1152 assert_eq!(series.point_size, 1.0);
1153 }
1154
1155 #[test]
1156 fn test_data_series_show_points() {
1157 let series = DataSeries::new("Data").show_points(false);
1158 assert!(!series.show_points);
1159 }
1160
1161 #[test]
1162 fn test_data_series_fill() {
1163 let series = DataSeries::new("Data").fill(true);
1164 assert!(series.fill);
1165 }
1166
1167 #[test]
1168 fn test_data_series_x_range() {
1169 let series = DataSeries::new("Data")
1170 .point(1.0, 10.0)
1171 .point(5.0, 20.0)
1172 .point(3.0, 15.0);
1173 assert_eq!(series.x_range(), Some((1.0, 5.0)));
1174 }
1175
1176 #[test]
1177 fn test_data_series_x_range_empty() {
1178 let series = DataSeries::new("Data");
1179 assert_eq!(series.x_range(), None);
1180 }
1181
1182 #[test]
1183 fn test_data_series_y_range() {
1184 let series = DataSeries::new("Data")
1185 .point(1.0, 10.0)
1186 .point(2.0, 30.0)
1187 .point(3.0, 5.0);
1188 assert_eq!(series.y_range(), Some((5.0, 30.0)));
1189 }
1190
1191 #[test]
1192 fn test_data_series_y_range_empty() {
1193 let series = DataSeries::new("Data");
1194 assert_eq!(series.y_range(), None);
1195 }
1196
1197 #[test]
1200 fn test_axis_default() {
1201 let axis = Axis::default();
1202 assert!(axis.label.is_none());
1203 assert!(axis.min.is_none());
1204 assert!(axis.max.is_none());
1205 assert_eq!(axis.grid_lines, 5);
1206 assert!(axis.show_grid);
1207 }
1208
1209 #[test]
1210 fn test_axis_label() {
1211 let axis = Axis::new().label("Time");
1212 assert_eq!(axis.label, Some("Time".to_string()));
1213 }
1214
1215 #[test]
1216 fn test_axis_min_max() {
1217 let axis = Axis::new().min(0.0).max(100.0);
1218 assert_eq!(axis.min, Some(0.0));
1219 assert_eq!(axis.max, Some(100.0));
1220 }
1221
1222 #[test]
1223 fn test_axis_range() {
1224 let axis = Axis::new().range(10.0, 50.0);
1225 assert_eq!(axis.min, Some(10.0));
1226 assert_eq!(axis.max, Some(50.0));
1227 }
1228
1229 #[test]
1230 fn test_axis_grid_lines() {
1231 let axis = Axis::new().grid_lines(10);
1232 assert_eq!(axis.grid_lines, 10);
1233 }
1234
1235 #[test]
1236 fn test_axis_grid_lines_min() {
1237 let axis = Axis::new().grid_lines(1);
1238 assert_eq!(axis.grid_lines, 2);
1239 }
1240
1241 #[test]
1242 fn test_axis_show_grid() {
1243 let axis = Axis::new().show_grid(false);
1244 assert!(!axis.show_grid);
1245 }
1246
1247 #[test]
1248 fn test_axis_colors() {
1249 let axis = Axis::new().color(Color::RED).grid_color(Color::BLUE);
1250 assert_eq!(axis.color, Color::RED);
1251 assert_eq!(axis.grid_color, Color::BLUE);
1252 }
1253
1254 #[test]
1257 fn test_legend_position_default() {
1258 assert_eq!(LegendPosition::default(), LegendPosition::TopRight);
1259 }
1260
1261 #[test]
1264 fn test_chart_new() {
1265 let chart = Chart::new();
1266 assert_eq!(chart.get_chart_type(), ChartType::Line);
1267 assert_eq!(chart.series_count(), 0);
1268 assert!(!chart.has_data());
1269 }
1270
1271 #[test]
1272 fn test_chart_line() {
1273 let chart = Chart::line();
1274 assert_eq!(chart.get_chart_type(), ChartType::Line);
1275 }
1276
1277 #[test]
1278 fn test_chart_bar() {
1279 let chart = Chart::bar();
1280 assert_eq!(chart.get_chart_type(), ChartType::Bar);
1281 }
1282
1283 #[test]
1284 fn test_chart_scatter() {
1285 let chart = Chart::scatter();
1286 assert_eq!(chart.get_chart_type(), ChartType::Scatter);
1287 }
1288
1289 #[test]
1290 fn test_chart_area() {
1291 let chart = Chart::area();
1292 assert_eq!(chart.get_chart_type(), ChartType::Area);
1293 }
1294
1295 #[test]
1296 fn test_chart_pie() {
1297 let chart = Chart::pie();
1298 assert_eq!(chart.get_chart_type(), ChartType::Pie);
1299 }
1300
1301 #[test]
1302 fn test_chart_builder() {
1303 let chart = Chart::new()
1304 .chart_type(ChartType::Bar)
1305 .series(DataSeries::new("Sales").point(1.0, 100.0))
1306 .series(DataSeries::new("Expenses").point(1.0, 80.0))
1307 .title("Revenue")
1308 .x_axis(Axis::new().label("Month"))
1309 .y_axis(Axis::new().label("Amount"))
1310 .legend(LegendPosition::BottomRight)
1311 .background(Color::WHITE)
1312 .padding(50.0)
1313 .width(600.0)
1314 .height(400.0)
1315 .accessible_name("Revenue chart")
1316 .test_id("revenue-chart");
1317
1318 assert_eq!(chart.get_chart_type(), ChartType::Bar);
1319 assert_eq!(chart.series_count(), 2);
1320 assert!(chart.has_data());
1321 assert_eq!(chart.get_title(), Some("Revenue"));
1322 assert_eq!(Widget::accessible_name(&chart), Some("Revenue chart"));
1323 assert_eq!(Widget::test_id(&chart), Some("revenue-chart"));
1324 }
1325
1326 #[test]
1327 fn test_chart_add_series() {
1328 let series_list = vec![DataSeries::new("A"), DataSeries::new("B")];
1329 let chart = Chart::new().add_series(series_list);
1330 assert_eq!(chart.series_count(), 2);
1331 }
1332
1333 #[test]
1336 fn test_chart_data_bounds() {
1337 let chart = Chart::new()
1338 .series(DataSeries::new("S1").point(0.0, 10.0).point(5.0, 20.0))
1339 .series(DataSeries::new("S2").point(1.0, 5.0).point(4.0, 25.0));
1340
1341 let bounds = chart.data_bounds().unwrap();
1342 assert_eq!(bounds.0, 0.0); assert_eq!(bounds.1, 5.0); assert_eq!(bounds.2, 5.0); assert_eq!(bounds.3, 25.0); }
1347
1348 #[test]
1349 fn test_chart_data_bounds_with_axis_override() {
1350 let chart = Chart::new()
1351 .series(DataSeries::new("S1").point(0.0, 10.0).point(5.0, 20.0))
1352 .x_axis(Axis::new().min(-5.0).max(10.0))
1353 .y_axis(Axis::new().min(0.0).max(50.0));
1354
1355 let bounds = chart.data_bounds().unwrap();
1356 assert_eq!(bounds.0, -5.0); assert_eq!(bounds.1, 10.0); assert_eq!(bounds.2, 0.0); assert_eq!(bounds.3, 50.0); }
1361
1362 #[test]
1363 fn test_chart_data_bounds_empty() {
1364 let chart = Chart::new();
1365 assert!(chart.data_bounds().is_none());
1366 }
1367
1368 #[test]
1371 fn test_chart_padding_min() {
1372 let chart = Chart::new().padding(-10.0);
1373 assert_eq!(chart.padding, 0.0);
1374 }
1375
1376 #[test]
1377 fn test_chart_width_min() {
1378 let chart = Chart::new().width(50.0);
1379 assert_eq!(chart.width, Some(100.0));
1380 }
1381
1382 #[test]
1383 fn test_chart_height_min() {
1384 let chart = Chart::new().height(50.0);
1385 assert_eq!(chart.height, Some(100.0));
1386 }
1387
1388 #[test]
1391 fn test_chart_type_id() {
1392 let chart = Chart::new();
1393 assert_eq!(Widget::type_id(&chart), TypeId::of::<Chart>());
1394 }
1395
1396 #[test]
1397 fn test_chart_measure_default() {
1398 let chart = Chart::new();
1399 let size = chart.measure(Constraints::loose(Size::new(1000.0, 1000.0)));
1400 assert_eq!(size.width, 400.0);
1401 assert_eq!(size.height, 300.0);
1402 }
1403
1404 #[test]
1405 fn test_chart_measure_custom() {
1406 let chart = Chart::new().width(600.0).height(400.0);
1407 let size = chart.measure(Constraints::loose(Size::new(1000.0, 1000.0)));
1408 assert_eq!(size.width, 600.0);
1409 assert_eq!(size.height, 400.0);
1410 }
1411
1412 #[test]
1413 fn test_chart_layout() {
1414 let mut chart = Chart::new();
1415 let bounds = Rect::new(10.0, 20.0, 500.0, 300.0);
1416 let result = chart.layout(bounds);
1417 assert_eq!(result.size, Size::new(500.0, 300.0));
1418 assert_eq!(chart.bounds, bounds);
1419 }
1420
1421 #[test]
1422 fn test_chart_children() {
1423 let chart = Chart::new();
1424 assert!(chart.children().is_empty());
1425 }
1426
1427 #[test]
1428 fn test_chart_is_interactive() {
1429 let chart = Chart::new();
1430 assert!(!chart.is_interactive());
1431 }
1432
1433 #[test]
1434 fn test_chart_is_focusable() {
1435 let chart = Chart::new();
1436 assert!(!chart.is_focusable());
1437 }
1438
1439 #[test]
1440 fn test_chart_accessible_role() {
1441 let chart = Chart::new();
1442 assert_eq!(chart.accessible_role(), AccessibleRole::Image);
1443 }
1444
1445 #[test]
1446 fn test_chart_accessible_name_from_title() {
1447 let chart = Chart::new().title("Sales Chart");
1448 assert_eq!(Widget::accessible_name(&chart), Some("Sales Chart"));
1449 }
1450
1451 #[test]
1452 fn test_chart_accessible_name_explicit() {
1453 let chart = Chart::new()
1454 .title("Sales Chart")
1455 .accessible_name("Custom name");
1456 assert_eq!(Widget::accessible_name(&chart), Some("Custom name"));
1457 }
1458
1459 #[test]
1460 fn test_chart_test_id() {
1461 let chart = Chart::new().test_id("my-chart");
1462 assert_eq!(Widget::test_id(&chart), Some("my-chart"));
1463 }
1464
1465 #[test]
1468 fn test_chart_plot_area_no_title() {
1469 let mut chart = Chart::new().padding(40.0);
1470 chart.bounds = Rect::new(0.0, 0.0, 400.0, 300.0);
1471 let plot = chart.plot_area();
1472 assert_eq!(plot.x, 40.0);
1473 assert_eq!(plot.y, 40.0);
1474 assert_eq!(plot.width, 320.0);
1475 assert_eq!(plot.height, 220.0);
1476 }
1477
1478 #[test]
1479 fn test_chart_plot_area_with_title() {
1480 let mut chart = Chart::new().padding(40.0).title("Test");
1481 chart.bounds = Rect::new(0.0, 0.0, 400.0, 300.0);
1482 let plot = chart.plot_area();
1483 assert_eq!(plot.y, 70.0); }
1485
1486 #[test]
1489 fn test_chart_map_point() {
1490 let chart = Chart::new();
1491 let bounds = (0.0, 10.0, 0.0, 100.0);
1492 let plot = Rect::new(0.0, 0.0, 100.0, 100.0);
1493
1494 let pt = chart.map_point(5.0, 50.0, &bounds, &plot);
1495 assert!((pt.x - 50.0).abs() < 0.1);
1496 assert!((pt.y - 50.0).abs() < 0.1);
1497 }
1498
1499 #[test]
1500 fn test_chart_map_point_origin() {
1501 let chart = Chart::new();
1502 let bounds = (0.0, 10.0, 0.0, 100.0);
1503 let plot = Rect::new(0.0, 0.0, 100.0, 100.0);
1504
1505 let pt = chart.map_point(0.0, 0.0, &bounds, &plot);
1506 assert!((pt.x - 0.0).abs() < 0.1);
1507 assert!((pt.y - 100.0).abs() < 0.1); }
1509
1510 #[test]
1513 fn test_chart_has_data_empty_series() {
1514 let chart = Chart::new().series(DataSeries::new("Empty"));
1515 assert!(!chart.has_data());
1516 }
1517
1518 #[test]
1519 fn test_chart_has_data_with_points() {
1520 let chart = Chart::new().series(DataSeries::new("Data").point(1.0, 1.0));
1521 assert!(chart.has_data());
1522 }
1523
1524 #[test]
1529 fn test_data_series_eq() {
1530 let s1 = DataSeries::new("A").point(1.0, 2.0);
1531 let s2 = DataSeries::new("A").point(1.0, 2.0);
1532 assert_eq!(s1, s2);
1533 }
1534
1535 #[test]
1536 fn test_chart_type_eq() {
1537 assert_eq!(ChartType::Line, ChartType::Line);
1538 assert_ne!(ChartType::Line, ChartType::Bar);
1539 }
1540
1541 #[test]
1542 fn test_legend_position_all_variants() {
1543 let positions = [
1544 LegendPosition::None,
1545 LegendPosition::TopRight,
1546 LegendPosition::TopLeft,
1547 LegendPosition::BottomRight,
1548 LegendPosition::BottomLeft,
1549 ];
1550 assert_eq!(positions.len(), 5);
1551 }
1552
1553 #[test]
1554 fn test_chart_children_mut() {
1555 let mut chart = Chart::new();
1556 assert!(chart.children_mut().is_empty());
1557 }
1558
1559 #[test]
1560 fn test_chart_event_returns_none() {
1561 let mut chart = Chart::new();
1562 let result = chart.event(&presentar_core::Event::KeyDown {
1563 key: presentar_core::Key::Down,
1564 });
1565 assert!(result.is_none());
1566 }
1567
1568 #[test]
1569 fn test_axis_default_colors() {
1570 let axis = Axis::default();
1571 assert_eq!(axis.color.a, 1.0);
1572 assert_eq!(axis.grid_color.a, 1.0);
1573 }
1574
1575 #[test]
1576 fn test_chart_get_series() {
1577 let chart = Chart::new()
1578 .series(DataSeries::new("A"))
1579 .series(DataSeries::new("B"));
1580 assert_eq!(chart.get_series().len(), 2);
1581 assert_eq!(chart.get_series()[0].name, "A");
1582 }
1583
1584 #[test]
1585 fn test_chart_histogram() {
1586 let chart = Chart::new().chart_type(ChartType::Histogram);
1587 assert_eq!(chart.get_chart_type(), ChartType::Histogram);
1588 }
1589
1590 #[test]
1591 fn test_chart_data_bounds_single_point() {
1592 let chart = Chart::new().series(DataSeries::new("S").point(5.0, 10.0));
1593 let bounds = chart.data_bounds().unwrap();
1594 assert_eq!(bounds.0, 5.0); assert_eq!(bounds.1, 5.0); }
1597
1598 #[test]
1599 fn test_chart_legend_none() {
1600 let chart = Chart::new().legend(LegendPosition::None);
1601 assert_eq!(chart.legend, LegendPosition::None);
1602 }
1603
1604 #[test]
1605 fn test_chart_legend_top_left() {
1606 let chart = Chart::new().legend(LegendPosition::TopLeft);
1607 assert_eq!(chart.legend, LegendPosition::TopLeft);
1608 }
1609
1610 #[test]
1611 fn test_chart_legend_bottom_left() {
1612 let chart = Chart::new().legend(LegendPosition::BottomLeft);
1613 assert_eq!(chart.legend, LegendPosition::BottomLeft);
1614 }
1615
1616 #[test]
1617 fn test_chart_test_id_none() {
1618 let chart = Chart::new();
1619 assert!(Widget::test_id(&chart).is_none());
1620 }
1621
1622 #[test]
1623 fn test_chart_accessible_name_none() {
1624 let chart = Chart::new();
1625 assert!(Widget::accessible_name(&chart).is_none());
1626 }
1627
1628 #[test]
1629 fn test_data_series_default_values() {
1630 let series = DataSeries::new("Test");
1631 assert_eq!(series.line_width, 2.0);
1632 assert_eq!(series.point_size, 4.0);
1633 }
1634
1635 #[test]
1640 fn test_chart_brick_name() {
1641 let chart = Chart::new();
1642 assert_eq!(chart.brick_name(), "Chart");
1643 }
1644
1645 #[test]
1646 fn test_chart_brick_assertions() {
1647 let chart = Chart::new();
1648 let assertions = chart.assertions();
1649 assert!(!assertions.is_empty());
1650 assert!(matches!(assertions[0], BrickAssertion::MaxLatencyMs(16)));
1651 }
1652
1653 #[test]
1654 fn test_chart_brick_budget() {
1655 let chart = Chart::new();
1656 let budget = chart.budget();
1657 assert!(budget.layout_ms > 0);
1659 assert!(budget.paint_ms > 0);
1660 }
1661
1662 #[test]
1663 fn test_chart_brick_verify() {
1664 let chart = Chart::new();
1665 let verification = chart.verify();
1666 assert!(!verification.passed.is_empty());
1667 assert!(verification.failed.is_empty());
1668 }
1669
1670 #[test]
1671 fn test_chart_brick_to_html() {
1672 let chart = Chart::new().test_id("my-chart").title("Test Chart");
1673 let html = chart.to_html();
1674 assert!(html.contains("brick-chart"));
1675 assert!(html.contains("my-chart"));
1676 assert!(html.contains("Test Chart"));
1677 }
1678
1679 #[test]
1680 fn test_chart_brick_to_html_default() {
1681 let chart = Chart::new();
1682 let html = chart.to_html();
1683 assert!(html.contains("data-testid=\"chart\""));
1684 assert!(html.contains("aria-label=\"Chart\""));
1685 }
1686
1687 #[test]
1688 fn test_chart_brick_to_css() {
1689 let chart = Chart::new();
1690 let css = chart.to_css();
1691 assert!(css.contains(".brick-chart"));
1692 assert!(css.contains("display: block"));
1693 }
1694
1695 #[test]
1696 fn test_chart_brick_test_id() {
1697 let chart = Chart::new().test_id("chart-1");
1698 assert_eq!(Brick::test_id(&chart), Some("chart-1"));
1699 }
1700
1701 #[test]
1702 fn test_chart_brick_test_id_none() {
1703 let chart = Chart::new();
1704 assert!(Brick::test_id(&chart).is_none());
1705 }
1706
1707 #[test]
1712 fn test_chart_heatmap_constructor() {
1713 let chart = Chart::heatmap();
1714 assert_eq!(chart.get_chart_type(), ChartType::Heatmap);
1715 }
1716
1717 #[test]
1718 fn test_chart_boxplot_constructor() {
1719 let chart = Chart::boxplot();
1720 assert_eq!(chart.get_chart_type(), ChartType::BoxPlot);
1721 }
1722
1723 #[test]
1728 fn test_chart_data_bounds_with_partial_axis_override() {
1729 let chart = Chart::new()
1731 .series(DataSeries::new("S1").point(0.0, 10.0).point(5.0, 20.0))
1732 .x_axis(Axis::new().min(-10.0));
1733
1734 let bounds = chart.data_bounds().unwrap();
1735 assert_eq!(bounds.0, -10.0); assert_eq!(bounds.1, 5.0); }
1738
1739 #[test]
1740 fn test_chart_data_bounds_only_y_axis_override() {
1741 let chart = Chart::new()
1742 .series(DataSeries::new("S1").point(0.0, 10.0).point(5.0, 20.0))
1743 .y_axis(Axis::new().max(100.0));
1744
1745 let bounds = chart.data_bounds().unwrap();
1746 assert_eq!(bounds.3, 100.0); }
1748
1749 #[test]
1750 fn test_chart_map_point_with_zero_range() {
1751 let chart = Chart::new();
1752 let bounds = (5.0, 5.0, 10.0, 10.0); let plot = Rect::new(0.0, 0.0, 100.0, 100.0);
1755
1756 let pt = chart.map_point(5.0, 10.0, &bounds, &plot);
1758 assert!(pt.x.is_finite());
1759 assert!(pt.y.is_finite());
1760 }
1761
1762 #[test]
1763 fn test_chart_measure_constrained() {
1764 let chart = Chart::new().width(800.0).height(600.0);
1765 let size = chart.measure(Constraints::tight(Size::new(400.0, 300.0)));
1767 assert_eq!(size.width, 400.0);
1768 assert_eq!(size.height, 300.0);
1769 }
1770
1771 #[test]
1772 fn test_data_series_x_range_single_point() {
1773 let series = DataSeries::new("Data").point(5.0, 10.0);
1774 let range = series.x_range().unwrap();
1775 assert_eq!(range.0, 5.0);
1776 assert_eq!(range.1, 5.0);
1777 }
1778
1779 #[test]
1780 fn test_data_series_y_range_single_point() {
1781 let series = DataSeries::new("Data").point(5.0, 10.0);
1782 let range = series.y_range().unwrap();
1783 assert_eq!(range.0, 10.0);
1784 assert_eq!(range.1, 10.0);
1785 }
1786
1787 #[test]
1788 fn test_axis_new() {
1789 let axis = Axis::new();
1790 assert!(axis.label.is_none());
1791 assert!(axis.min.is_none());
1792 assert!(axis.max.is_none());
1793 }
1794
1795 #[test]
1796 fn test_chart_type_clone() {
1797 let ct = ChartType::Histogram;
1798 let cloned = ct;
1799 assert_eq!(cloned, ChartType::Histogram);
1800 }
1801
1802 #[test]
1803 fn test_legend_position_eq() {
1804 assert_eq!(LegendPosition::TopRight, LegendPosition::TopRight);
1805 assert_ne!(LegendPosition::TopRight, LegendPosition::TopLeft);
1806 }
1807
1808 #[test]
1809 fn test_chart_with_empty_series() {
1810 let chart = Chart::new()
1811 .series(DataSeries::new("Empty1"))
1812 .series(DataSeries::new("Empty2").point(1.0, 2.0));
1813
1814 assert!(chart.has_data()); assert_eq!(chart.series_count(), 2);
1816 }
1817
1818 #[test]
1819 fn test_chart_multiple_series_data_bounds() {
1820 let chart = Chart::new()
1821 .series(DataSeries::new("S1").point(-5.0, 0.0).point(0.0, 10.0))
1822 .series(DataSeries::new("S2").point(0.0, -10.0).point(10.0, 50.0));
1823
1824 let bounds = chart.data_bounds().unwrap();
1825 assert_eq!(bounds.0, -5.0); assert_eq!(bounds.1, 10.0); assert_eq!(bounds.2, -10.0); assert_eq!(bounds.3, 50.0); }
1830
1831 #[test]
1832 fn test_chart_legend_bottom_right() {
1833 let chart = Chart::new().legend(LegendPosition::BottomRight);
1834 assert_eq!(chart.legend, LegendPosition::BottomRight);
1835 }
1836
1837 #[test]
1838 fn test_chart_background_setter() {
1839 let chart = Chart::new().background(Color::BLACK);
1840 assert_eq!(chart.background, Color::BLACK);
1841 }
1842
1843 #[test]
1844 fn test_data_series_clone() {
1845 let series = DataSeries::new("Test").point(1.0, 2.0);
1846 let cloned = series.clone();
1847 assert_eq!(cloned.name, "Test");
1848 assert_eq!(cloned.points.len(), 1);
1849 }
1850
1851 #[test]
1852 fn test_axis_clone() {
1853 let axis = Axis::new().label("X").min(0.0).max(100.0);
1854 let cloned = axis.clone();
1855 assert_eq!(cloned.label, Some("X".to_string()));
1856 assert_eq!(cloned.min, Some(0.0));
1857 assert_eq!(cloned.max, Some(100.0));
1858 }
1859
1860 #[test]
1861 fn test_chart_clone() {
1862 let chart = Chart::new()
1863 .title("Test")
1864 .series(DataSeries::new("S1").point(1.0, 2.0));
1865 let cloned = chart.clone();
1866 assert_eq!(cloned.get_title(), Some("Test"));
1867 assert_eq!(cloned.series_count(), 1);
1868 }
1869
1870 #[test]
1871 fn test_chart_type_debug() {
1872 let ct = ChartType::Pie;
1873 let debug_str = format!("{:?}", ct);
1874 assert!(debug_str.contains("Pie"));
1875 }
1876
1877 #[test]
1878 fn test_legend_position_debug() {
1879 let lp = LegendPosition::TopRight;
1880 let debug_str = format!("{:?}", lp);
1881 assert!(debug_str.contains("TopRight"));
1882 }
1883
1884 #[test]
1885 fn test_data_series_debug() {
1886 let series = DataSeries::new("Test");
1887 let debug_str = format!("{:?}", series);
1888 assert!(debug_str.contains("Test"));
1889 }
1890
1891 #[test]
1892 fn test_axis_debug() {
1893 let axis = Axis::new().label("Time");
1894 let debug_str = format!("{:?}", axis);
1895 assert!(debug_str.contains("Time"));
1896 }
1897
1898 #[test]
1899 fn test_chart_debug() {
1900 let chart = Chart::new().title("Debug Test");
1901 let debug_str = format!("{:?}", chart);
1902 assert!(debug_str.contains("Debug Test"));
1903 }
1904
1905 use presentar_core::RecordingCanvas;
1910
1911 #[test]
1912 fn test_chart_paint_empty() {
1913 let mut chart = Chart::new();
1914 chart.bounds = Rect::new(0.0, 0.0, 400.0, 300.0);
1915 let mut canvas = RecordingCanvas::new();
1916 chart.paint(&mut canvas);
1917 assert!(!canvas.commands().is_empty());
1919 }
1920
1921 #[test]
1922 fn test_chart_paint_with_title() {
1923 let mut chart = Chart::new().title("My Chart");
1924 chart.bounds = Rect::new(0.0, 0.0, 400.0, 300.0);
1925 let mut canvas = RecordingCanvas::new();
1926 chart.paint(&mut canvas);
1927 assert!(canvas.commands().len() >= 2);
1929 }
1930
1931 #[test]
1932 fn test_chart_paint_line_chart() {
1933 let mut chart = Chart::line().series(
1934 DataSeries::new("Data")
1935 .point(0.0, 0.0)
1936 .point(5.0, 10.0)
1937 .point(10.0, 5.0),
1938 );
1939 chart.bounds = Rect::new(0.0, 0.0, 400.0, 300.0);
1940 let mut canvas = RecordingCanvas::new();
1941 chart.paint(&mut canvas);
1942 assert!(canvas.commands().len() > 5);
1944 }
1945
1946 #[test]
1947 fn test_chart_paint_line_chart_no_points() {
1948 let mut chart = Chart::line().series(
1949 DataSeries::new("Data")
1950 .point(0.0, 0.0)
1951 .point(5.0, 10.0)
1952 .show_points(false),
1953 );
1954 chart.bounds = Rect::new(0.0, 0.0, 400.0, 300.0);
1955 let mut canvas = RecordingCanvas::new();
1956 chart.paint(&mut canvas);
1957 assert!(!canvas.commands().is_empty());
1958 }
1959
1960 #[test]
1961 fn test_chart_paint_area_chart() {
1962 let mut chart = Chart::area().series(
1963 DataSeries::new("Data")
1964 .point(0.0, 0.0)
1965 .point(5.0, 10.0)
1966 .point(10.0, 5.0)
1967 .fill(true),
1968 );
1969 chart.bounds = Rect::new(0.0, 0.0, 400.0, 300.0);
1970 let mut canvas = RecordingCanvas::new();
1971 chart.paint(&mut canvas);
1972 assert!(canvas.commands().len() > 5);
1973 }
1974
1975 #[test]
1976 fn test_chart_paint_bar_chart() {
1977 let mut chart = Chart::bar()
1978 .series(DataSeries::new("A").point(1.0, 10.0).point(2.0, 20.0))
1979 .series(DataSeries::new("B").point(1.0, 15.0).point(2.0, 25.0));
1980 chart.bounds = Rect::new(0.0, 0.0, 400.0, 300.0);
1981 let mut canvas = RecordingCanvas::new();
1982 chart.paint(&mut canvas);
1983 assert!(canvas.commands().len() > 5);
1984 }
1985
1986 #[test]
1987 fn test_chart_paint_bar_chart_empty_series() {
1988 let mut chart = Chart::bar();
1989 chart.bounds = Rect::new(0.0, 0.0, 400.0, 300.0);
1990 let mut canvas = RecordingCanvas::new();
1991 chart.paint(&mut canvas);
1992 assert!(!canvas.commands().is_empty());
1994 }
1995
1996 #[test]
1997 fn test_chart_paint_scatter_chart() {
1998 let mut chart = Chart::scatter().series(
1999 DataSeries::new("Points")
2000 .point(1.0, 2.0)
2001 .point(3.0, 4.0)
2002 .point(5.0, 6.0),
2003 );
2004 chart.bounds = Rect::new(0.0, 0.0, 400.0, 300.0);
2005 let mut canvas = RecordingCanvas::new();
2006 chart.paint(&mut canvas);
2007 assert!(canvas.commands().len() > 5);
2008 }
2009
2010 #[test]
2011 fn test_chart_paint_pie_chart() {
2012 let mut chart = Chart::pie()
2013 .series(DataSeries::new("Slice1").point(0.0, 30.0))
2014 .series(DataSeries::new("Slice2").point(0.0, 50.0))
2015 .series(DataSeries::new("Slice3").point(0.0, 20.0));
2016 chart.bounds = Rect::new(0.0, 0.0, 400.0, 300.0);
2017 let mut canvas = RecordingCanvas::new();
2018 chart.paint(&mut canvas);
2019 assert!(canvas.commands().len() > 3);
2020 }
2021
2022 #[test]
2023 fn test_chart_paint_pie_chart_zero_total() {
2024 let mut chart = Chart::pie().series(DataSeries::new("Zero").point(0.0, 0.0));
2025 chart.bounds = Rect::new(0.0, 0.0, 400.0, 300.0);
2026 let mut canvas = RecordingCanvas::new();
2027 chart.paint(&mut canvas);
2028 assert!(!canvas.commands().is_empty());
2030 }
2031
2032 #[test]
2033 fn test_chart_paint_histogram() {
2034 let mut chart = Chart::new().chart_type(ChartType::Histogram).series(
2035 DataSeries::new("Data")
2036 .point(1.0, 5.0)
2037 .point(2.0, 10.0)
2038 .point(3.0, 7.0),
2039 );
2040 chart.bounds = Rect::new(0.0, 0.0, 400.0, 300.0);
2041 let mut canvas = RecordingCanvas::new();
2042 chart.paint(&mut canvas);
2043 assert!(canvas.commands().len() > 5);
2044 }
2045
2046 #[test]
2047 fn test_chart_paint_heatmap() {
2048 let mut chart = Chart::heatmap()
2049 .series(
2050 DataSeries::new("Row1")
2051 .point(0.0, 10.0)
2052 .point(1.0, 20.0)
2053 .point(2.0, 30.0),
2054 )
2055 .series(
2056 DataSeries::new("Row2")
2057 .point(0.0, 15.0)
2058 .point(1.0, 25.0)
2059 .point(2.0, 35.0),
2060 );
2061 chart.bounds = Rect::new(0.0, 0.0, 400.0, 300.0);
2062 let mut canvas = RecordingCanvas::new();
2063 chart.paint(&mut canvas);
2064 assert!(canvas.commands().len() > 5);
2066 }
2067
2068 #[test]
2069 fn test_chart_paint_heatmap_empty() {
2070 let mut chart = Chart::heatmap();
2071 chart.bounds = Rect::new(0.0, 0.0, 400.0, 300.0);
2072 let mut canvas = RecordingCanvas::new();
2073 chart.paint(&mut canvas);
2074 assert!(!canvas.commands().is_empty());
2076 }
2077
2078 #[test]
2079 fn test_chart_paint_boxplot() {
2080 let mut chart = Chart::boxplot().series(
2081 DataSeries::new("Stats")
2082 .point(0.0, 1.0)
2083 .point(0.0, 2.0)
2084 .point(0.0, 3.0)
2085 .point(0.0, 4.0)
2086 .point(0.0, 5.0)
2087 .point(0.0, 6.0)
2088 .point(0.0, 7.0),
2089 );
2090 chart.bounds = Rect::new(0.0, 0.0, 400.0, 300.0);
2091 let mut canvas = RecordingCanvas::new();
2092 chart.paint(&mut canvas);
2093 assert!(canvas.commands().len() > 5);
2095 }
2096
2097 #[test]
2098 fn test_chart_paint_boxplot_insufficient_points() {
2099 let mut chart = Chart::boxplot().series(
2100 DataSeries::new("TooFew")
2101 .point(0.0, 1.0)
2102 .point(0.0, 2.0)
2103 .point(0.0, 3.0),
2104 );
2105 chart.bounds = Rect::new(0.0, 0.0, 400.0, 300.0);
2106 let mut canvas = RecordingCanvas::new();
2107 chart.paint(&mut canvas);
2108 assert!(!canvas.commands().is_empty());
2110 }
2111
2112 #[test]
2113 fn test_chart_paint_boxplot_empty_series() {
2114 let mut chart = Chart::boxplot();
2115 chart.bounds = Rect::new(0.0, 0.0, 400.0, 300.0);
2116 let mut canvas = RecordingCanvas::new();
2117 chart.paint(&mut canvas);
2118 assert!(!canvas.commands().is_empty());
2119 }
2120
2121 #[test]
2122 fn test_chart_paint_legend_top_right() {
2123 let mut chart = Chart::new()
2124 .legend(LegendPosition::TopRight)
2125 .series(DataSeries::new("Series A").point(1.0, 2.0))
2126 .series(DataSeries::new("Series B").point(2.0, 3.0));
2127 chart.bounds = Rect::new(0.0, 0.0, 400.0, 300.0);
2128 let mut canvas = RecordingCanvas::new();
2129 chart.paint(&mut canvas);
2130 assert!(canvas.commands().len() > 5);
2132 }
2133
2134 #[test]
2135 fn test_chart_paint_legend_top_left() {
2136 let mut chart = Chart::new()
2137 .legend(LegendPosition::TopLeft)
2138 .series(DataSeries::new("Data").point(1.0, 2.0));
2139 chart.bounds = Rect::new(0.0, 0.0, 400.0, 300.0);
2140 let mut canvas = RecordingCanvas::new();
2141 chart.paint(&mut canvas);
2142 assert!(canvas.commands().len() > 5);
2143 }
2144
2145 #[test]
2146 fn test_chart_paint_legend_bottom_right() {
2147 let mut chart = Chart::new()
2148 .legend(LegendPosition::BottomRight)
2149 .series(DataSeries::new("Data").point(1.0, 2.0));
2150 chart.bounds = Rect::new(0.0, 0.0, 400.0, 300.0);
2151 let mut canvas = RecordingCanvas::new();
2152 chart.paint(&mut canvas);
2153 assert!(canvas.commands().len() > 5);
2154 }
2155
2156 #[test]
2157 fn test_chart_paint_legend_bottom_left() {
2158 let mut chart = Chart::new()
2159 .legend(LegendPosition::BottomLeft)
2160 .series(DataSeries::new("Data").point(1.0, 2.0));
2161 chart.bounds = Rect::new(0.0, 0.0, 400.0, 300.0);
2162 let mut canvas = RecordingCanvas::new();
2163 chart.paint(&mut canvas);
2164 assert!(canvas.commands().len() > 5);
2165 }
2166
2167 #[test]
2168 fn test_chart_paint_legend_none() {
2169 let mut chart = Chart::new()
2170 .legend(LegendPosition::None)
2171 .series(DataSeries::new("Data").point(1.0, 2.0));
2172 chart.bounds = Rect::new(0.0, 0.0, 400.0, 300.0);
2173 let mut canvas = RecordingCanvas::new();
2174 chart.paint(&mut canvas);
2175 assert!(!canvas.commands().is_empty());
2177 }
2178
2179 #[test]
2180 fn test_chart_paint_legend_empty_series() {
2181 let mut chart = Chart::new().legend(LegendPosition::TopRight);
2182 chart.bounds = Rect::new(0.0, 0.0, 400.0, 300.0);
2183 let mut canvas = RecordingCanvas::new();
2184 chart.paint(&mut canvas);
2185 assert!(!canvas.commands().is_empty());
2187 }
2188
2189 #[test]
2190 fn test_chart_paint_grid_hidden() {
2191 let mut chart = Chart::new()
2192 .x_axis(Axis::new().show_grid(false))
2193 .y_axis(Axis::new().show_grid(false))
2194 .series(DataSeries::new("Data").point(0.0, 0.0).point(10.0, 10.0));
2195 chart.bounds = Rect::new(0.0, 0.0, 400.0, 300.0);
2196 let mut canvas = RecordingCanvas::new();
2197 chart.paint(&mut canvas);
2198 assert!(!canvas.commands().is_empty());
2200 }
2201
2202 #[test]
2203 fn test_chart_paint_line_single_point() {
2204 let mut chart = Chart::line().series(DataSeries::new("Single").point(5.0, 10.0));
2205 chart.bounds = Rect::new(0.0, 0.0, 400.0, 300.0);
2206 let mut canvas = RecordingCanvas::new();
2207 chart.paint(&mut canvas);
2208 assert!(!canvas.commands().is_empty());
2210 }
2211
2212 #[test]
2213 fn test_chart_paint_multiple_series_line() {
2214 let mut chart = Chart::line()
2215 .series(DataSeries::new("A").point(0.0, 0.0).point(5.0, 10.0))
2216 .series(DataSeries::new("B").point(0.0, 5.0).point(5.0, 15.0))
2217 .series(DataSeries::new("C").point(0.0, 10.0).point(5.0, 20.0));
2218 chart.bounds = Rect::new(0.0, 0.0, 400.0, 300.0);
2219 let mut canvas = RecordingCanvas::new();
2220 chart.paint(&mut canvas);
2221 assert!(canvas.commands().len() > 10);
2223 }
2224
2225 #[test]
2226 fn test_paint_grid_labels() {
2227 let mut chart = Chart::new()
2228 .x_axis(Axis::new().grid_lines(3))
2229 .y_axis(Axis::new().grid_lines(4))
2230 .series(DataSeries::new("Data").point(0.0, 0.0).point(10.0, 100.0));
2231 chart.bounds = Rect::new(0.0, 0.0, 400.0, 300.0);
2232 let mut canvas = RecordingCanvas::new();
2233 chart.paint(&mut canvas);
2234 assert!(canvas.commands().len() > 5);
2236 }
2237
2238 #[test]
2239 fn test_chart_paint_with_all_options() {
2240 let mut chart = Chart::new()
2241 .chart_type(ChartType::Line)
2242 .title("Full Chart")
2243 .series(
2244 DataSeries::new("Main")
2245 .point(0.0, 0.0)
2246 .point(5.0, 50.0)
2247 .point(10.0, 30.0)
2248 .color(Color::RED)
2249 .line_width(3.0)
2250 .point_size(6.0)
2251 .show_points(true)
2252 .fill(true),
2253 )
2254 .x_axis(Axis::new().label("X").min(-5.0).max(15.0).grid_lines(4))
2255 .y_axis(Axis::new().label("Y").min(-10.0).max(60.0).grid_lines(5))
2256 .legend(LegendPosition::TopRight)
2257 .background(Color::WHITE)
2258 .padding(50.0);
2259 chart.bounds = Rect::new(0.0, 0.0, 500.0, 400.0);
2260 let mut canvas = RecordingCanvas::new();
2261 chart.paint(&mut canvas);
2262 assert!(canvas.commands().len() > 15);
2264 }
2265}