presentar_widgets/
chart.rs

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