presentar_widgets/
chart.rs

1//! `Chart` widget for data visualization.
2
3use 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/// Chart type variants.
13#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
14pub enum ChartType {
15    /// Line chart
16    #[default]
17    Line,
18    /// Bar chart
19    Bar,
20    /// Scatter plot
21    Scatter,
22    /// Area chart
23    Area,
24    /// Pie chart
25    Pie,
26    /// Histogram
27    Histogram,
28    /// Heatmap - displays matrix data with color encoding
29    Heatmap,
30    /// Box plot - displays statistical distributions
31    BoxPlot,
32}
33
34/// A single data series for the chart.
35#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
36pub struct DataSeries {
37    /// Series name/label
38    pub name: String,
39    /// Data points (x, y)
40    pub points: Vec<(f64, f64)>,
41    /// Series color
42    pub color: Color,
43    /// Line width (for line/area charts)
44    pub line_width: f32,
45    /// Point size (for scatter/line charts)
46    pub point_size: f32,
47    /// Whether to show points
48    pub show_points: bool,
49    /// Whether to fill area under line
50    pub fill: bool,
51}
52
53impl DataSeries {
54    /// Create a new data series.
55    #[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    /// Add a data point.
69    #[must_use]
70    pub fn point(mut self, x: f64, y: f64) -> Self {
71        self.points.push((x, y));
72        self
73    }
74
75    /// Add multiple data points.
76    #[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    /// Set series color.
83    #[must_use]
84    pub const fn color(mut self, color: Color) -> Self {
85        self.color = color;
86        self
87    }
88
89    /// Set line width.
90    #[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    /// Set point size.
97    #[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    /// Set whether to show points.
104    #[must_use]
105    pub const fn show_points(mut self, show: bool) -> Self {
106        self.show_points = show;
107        self
108    }
109
110    /// Set whether to fill area.
111    #[must_use]
112    pub const fn fill(mut self, fill: bool) -> Self {
113        self.fill = fill;
114        self
115    }
116
117    /// Get min/max X values.
118    #[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    /// Get min/max Y values.
137    #[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/// Axis configuration.
157#[derive(Debug, Clone, Serialize, Deserialize)]
158pub struct Axis {
159    /// Axis label
160    pub label: Option<String>,
161    /// Minimum value (auto if None)
162    pub min: Option<f64>,
163    /// Maximum value (auto if None)
164    pub max: Option<f64>,
165    /// Number of grid lines
166    pub grid_lines: usize,
167    /// Show grid
168    pub show_grid: bool,
169    /// Axis color
170    pub color: Color,
171    /// Grid color
172    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    /// Create a new axis.
191    #[must_use]
192    pub fn new() -> Self {
193        Self::default()
194    }
195
196    /// Set axis label.
197    #[must_use]
198    pub fn label(mut self, label: impl Into<String>) -> Self {
199        self.label = Some(label.into());
200        self
201    }
202
203    /// Set minimum value.
204    #[must_use]
205    pub const fn min(mut self, min: f64) -> Self {
206        self.min = Some(min);
207        self
208    }
209
210    /// Set maximum value.
211    #[must_use]
212    pub const fn max(mut self, max: f64) -> Self {
213        self.max = Some(max);
214        self
215    }
216
217    /// Set range.
218    #[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    /// Set number of grid lines.
226    #[must_use]
227    pub fn grid_lines(mut self, count: usize) -> Self {
228        self.grid_lines = count.max(2);
229        self
230    }
231
232    /// Set whether to show grid.
233    #[must_use]
234    pub const fn show_grid(mut self, show: bool) -> Self {
235        self.show_grid = show;
236        self
237    }
238
239    /// Set axis color.
240    #[must_use]
241    pub const fn color(mut self, color: Color) -> Self {
242        self.color = color;
243        self
244    }
245
246    /// Set grid color.
247    #[must_use]
248    pub const fn grid_color(mut self, color: Color) -> Self {
249        self.grid_color = color;
250        self
251    }
252}
253
254/// Legend position.
255#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
256pub enum LegendPosition {
257    /// No legend
258    None,
259    /// Top right (default)
260    #[default]
261    TopRight,
262    /// Top left
263    TopLeft,
264    /// Bottom right
265    BottomRight,
266    /// Bottom left
267    BottomLeft,
268}
269
270/// `Chart` widget for data visualization.
271#[derive(Debug, Clone, Serialize, Deserialize)]
272pub struct Chart {
273    /// Chart type
274    kind: ChartType,
275    /// Data series
276    series: Vec<DataSeries>,
277    /// Chart title
278    title: Option<String>,
279    /// X axis configuration
280    x_axis: Axis,
281    /// Y axis configuration
282    y_axis: Axis,
283    /// Legend position
284    legend: LegendPosition,
285    /// Background color
286    background: Color,
287    /// Padding around chart area
288    padding: f32,
289    /// Width
290    width: Option<f32>,
291    /// Height
292    height: Option<f32>,
293    /// Accessible name
294    accessible_name_value: Option<String>,
295    /// Test ID
296    test_id_value: Option<String>,
297    /// Cached bounds
298    #[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    /// Create a new chart.
324    #[must_use]
325    pub fn new() -> Self {
326        Self::default()
327    }
328
329    /// Create a line chart.
330    #[must_use]
331    pub fn line() -> Self {
332        Self::new().chart_type(ChartType::Line)
333    }
334
335    /// Create a bar chart.
336    #[must_use]
337    pub fn bar() -> Self {
338        Self::new().chart_type(ChartType::Bar)
339    }
340
341    /// Create a scatter chart.
342    #[must_use]
343    pub fn scatter() -> Self {
344        Self::new().chart_type(ChartType::Scatter)
345    }
346
347    /// Create an area chart.
348    #[must_use]
349    pub fn area() -> Self {
350        Self::new().chart_type(ChartType::Area)
351    }
352
353    /// Create a pie chart.
354    #[must_use]
355    pub fn pie() -> Self {
356        Self::new().chart_type(ChartType::Pie)
357    }
358
359    /// Create a heatmap chart.
360    #[must_use]
361    pub fn heatmap() -> Self {
362        Self::new().chart_type(ChartType::Heatmap)
363    }
364
365    /// Create a box plot chart.
366    #[must_use]
367    pub fn boxplot() -> Self {
368        Self::new().chart_type(ChartType::BoxPlot)
369    }
370
371    /// Set chart type.
372    #[must_use]
373    pub const fn chart_type(mut self, chart_type: ChartType) -> Self {
374        self.kind = chart_type;
375        self
376    }
377
378    /// Add a data series.
379    #[must_use]
380    pub fn series(mut self, series: DataSeries) -> Self {
381        self.series.push(series);
382        self
383    }
384
385    /// Add multiple data series.
386    #[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    /// Set chart title.
393    #[must_use]
394    pub fn title(mut self, title: impl Into<String>) -> Self {
395        self.title = Some(title.into());
396        self
397    }
398
399    /// Set X axis.
400    #[must_use]
401    pub fn x_axis(mut self, axis: Axis) -> Self {
402        self.x_axis = axis;
403        self
404    }
405
406    /// Set Y axis.
407    #[must_use]
408    pub fn y_axis(mut self, axis: Axis) -> Self {
409        self.y_axis = axis;
410        self
411    }
412
413    /// Set legend position.
414    #[must_use]
415    pub const fn legend(mut self, position: LegendPosition) -> Self {
416        self.legend = position;
417        self
418    }
419
420    /// Set background color.
421    #[must_use]
422    pub const fn background(mut self, color: Color) -> Self {
423        self.background = color;
424        self
425    }
426
427    /// Set padding.
428    #[must_use]
429    pub fn padding(mut self, padding: f32) -> Self {
430        self.padding = padding.max(0.0);
431        self
432    }
433
434    /// Set width.
435    #[must_use]
436    pub fn width(mut self, width: f32) -> Self {
437        self.width = Some(width.max(100.0));
438        self
439    }
440
441    /// Set height.
442    #[must_use]
443    pub fn height(mut self, height: f32) -> Self {
444        self.height = Some(height.max(100.0));
445        self
446    }
447
448    /// Set accessible name.
449    #[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    /// Set test ID.
456    #[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    /// Get chart type.
463    #[must_use]
464    pub const fn get_chart_type(&self) -> ChartType {
465        self.kind
466    }
467
468    /// Get data series.
469    #[must_use]
470    pub fn get_series(&self) -> &[DataSeries] {
471        &self.series
472    }
473
474    /// Get series count.
475    #[must_use]
476    pub fn series_count(&self) -> usize {
477        self.series.len()
478    }
479
480    /// Check if chart has data.
481    #[must_use]
482    pub fn has_data(&self) -> bool {
483        self.series.iter().any(|s| !s.points.is_empty())
484    }
485
486    /// Get title.
487    #[must_use]
488    pub fn get_title(&self) -> Option<&str> {
489        self.title.as_deref()
490    }
491
492    /// Compute data bounds across all series.
493    #[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        // Apply axis overrides
516        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    /// Get plot area (excluding padding and labels).
533    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    /// Map data point to screen coordinates.
544    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    /// Paint grid lines.
556    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        // Vertical grid lines
560        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        // Horizontal grid lines
574        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        // Axis labels
588        let text_style = TextStyle {
589            size: 10.0,
590            color: self.x_axis.color,
591            ..TextStyle::default()
592        };
593
594        // X axis labels
595        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        // Y axis labels
607        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    /// Paint line/area chart.
620    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            // Collect points for the path
627            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            // Draw line using proper path
634            canvas.draw_path(&path_points, series.color, series.line_width);
635
636            // For area charts, fill the area under the line
637            if series.fill {
638                let mut fill_points = path_points.clone();
639                // Add bottom corners
640                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; // Semi-transparent fill
646                canvas.fill_polygon(&fill_points, fill_color);
647            }
648
649            // Draw points as circles
650            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    /// Paint bar chart.
660    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        // Calculate bar width based on number of points
670        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    /// Paint scatter chart.
697    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    /// Paint pie chart.
707    fn paint_pie(&self, canvas: &mut dyn Canvas, plot: &Rect) {
708        // Sum all Y values across series
709        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        // Draw pie segments as arcs
725        let mut start_angle: f32 = -std::f32::consts::FRAC_PI_2; // Start from top
726        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    /// Paint heatmap chart - displays matrix data with color encoding.
740    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        // For heatmap, we treat each series as a row and each point as a cell
745        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                // Map value to color intensity (blue to red)
763                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    /// Paint box plot - displays statistical distributions.
778    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; // Need at least 5 points for box plot (min, q1, median, q3, max)
793            }
794
795            // Sort points by y value for quartile calculation
796            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            // Map y values to screen coordinates
808            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            // Draw whiskers (vertical lines from min to q1 and q3 to max)
820            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            // Draw box (from q1 to q3)
834            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            // Draw median line
839            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            // Draw caps (horizontal lines at min and max)
847            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    /// Paint legend.
864    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        // Legend background
894        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        // Legend entries
905        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            // Color box
914            canvas.fill_rect(Rect::new(lx + 5.0, ey + 4.0, 12.0, 12.0), series.color);
915            // Label
916            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        // Background
941        canvas.fill_rect(self.bounds, self.background);
942
943        // Title
944        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        // Get data bounds
963        let Some(bounds) = self.data_bounds() else {
964            return;
965        };
966
967        // Draw grid
968        self.paint_grid(canvas, &plot, &bounds);
969
970        // Draw chart based on type
971        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        // Draw legend
981        self.paint_legend(canvas);
982    }
983
984    fn event(&mut self, _event: &presentar_core::Event) -> Option<Box<dyn Any + Send>> {
985        // Charts are currently view-only
986        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 // Charts are treated as images for accessibility
1013    }
1014
1015    fn test_id(&self) -> Option<&str> {
1016        self.test_id_value.as_deref()
1017    }
1018}
1019
1020// PROBAR-SPEC-009: Brick Architecture - Tests define interface
1021impl 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    // ===== ChartType Tests =====
1064
1065    #[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    // ===== DataSeries Tests =====
1098
1099    #[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    // ===== Axis Tests =====
1198
1199    #[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    // ===== LegendPosition Tests =====
1255
1256    #[test]
1257    fn test_legend_position_default() {
1258        assert_eq!(LegendPosition::default(), LegendPosition::TopRight);
1259    }
1260
1261    // ===== Chart Construction Tests =====
1262
1263    #[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    // ===== Data Bounds Tests =====
1334
1335    #[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); // x_min
1343        assert_eq!(bounds.1, 5.0); // x_max
1344        assert_eq!(bounds.2, 5.0); // y_min
1345        assert_eq!(bounds.3, 25.0); // y_max
1346    }
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); // x_min (overridden)
1357        assert_eq!(bounds.1, 10.0); // x_max (overridden)
1358        assert_eq!(bounds.2, 0.0); // y_min (overridden)
1359        assert_eq!(bounds.3, 50.0); // y_max (overridden)
1360    }
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    // ===== Dimension Tests =====
1369
1370    #[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    // ===== Widget Trait Tests =====
1389
1390    #[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    // ===== Plot Area Tests =====
1466
1467    #[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); // 40 + 30 for title
1484    }
1485
1486    // ===== Map Point Tests =====
1487
1488    #[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); // Y is flipped
1508    }
1509
1510    // ===== Has Data Tests =====
1511
1512    #[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    // =========================================================================
1525    // Additional Coverage Tests
1526    // =========================================================================
1527
1528    #[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); // x_min
1595        assert_eq!(bounds.1, 5.0); // x_max (same as min for single point)
1596    }
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    // =========================================================================
1636    // Brick Trait Tests
1637    // =========================================================================
1638
1639    #[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        // Verify budget has reasonable values
1658        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    // =========================================================================
1708    // Chart Type Constructor Tests
1709    // =========================================================================
1710
1711    #[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    // =========================================================================
1724    // Additional Edge Case Tests
1725    // =========================================================================
1726
1727    #[test]
1728    fn test_chart_data_bounds_with_partial_axis_override() {
1729        // Only x_min overridden
1730        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); // x_min overridden
1736        assert_eq!(bounds.1, 5.0); // x_max from data
1737    }
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); // y_max overridden
1747    }
1748
1749    #[test]
1750    fn test_chart_map_point_with_zero_range() {
1751        let chart = Chart::new();
1752        // Zero range should be clamped to 1e-10
1753        let bounds = (5.0, 5.0, 10.0, 10.0); // Same values = zero range
1754        let plot = Rect::new(0.0, 0.0, 100.0, 100.0);
1755
1756        // Should not panic, uses 1e-10 as min range
1757        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        // Constrain to smaller size
1766        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()); // Second series has data
1815        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); // min x
1826        assert_eq!(bounds.1, 10.0); // max x
1827        assert_eq!(bounds.2, -10.0); // min y
1828        assert_eq!(bounds.3, 50.0); // max y
1829    }
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    // =========================================================================
1906    // Paint Method Tests (coverage for rendering code)
1907    // =========================================================================
1908
1909    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        // Should paint background only (no data)
1918        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        // Should paint background and title
1928        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        // Should have multiple draw commands for grid, line, and points
1943        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        // Only background painted (no data)
1993        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        // Pie with zero total skips rendering segments
2029        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        // Should paint cells for heatmap
2065        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        // Empty heatmap still paints background
2075        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        // Boxplot paints whiskers, box, and median
2094        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        // Should skip boxplot for < 5 points
2109        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        // Legend should add fill_rect and stroke_rect commands
2131        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        // No legend commands
2176        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        // No series = no legend
2186        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        // Grid hidden but axis labels still drawn
2199        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        // Single point - line skipped but point drawn
2209        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        // Multiple lines + points + grid
2222        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        // Grid lines + labels
2235        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        // Should have many commands for all elements
2263        assert!(canvas.commands().len() > 15);
2264    }
2265}