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)]
1060#[allow(clippy::unwrap_used, clippy::disallowed_methods)]
1061#[path = "chart_tests.rs"]
1062mod tests;