presentar_widgets/
data_card.rs

1//! `DataCard` widget for displaying dataset metadata.
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;
9use std::collections::HashMap;
10
11/// Data quality indicator.
12#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
13pub enum DataQuality {
14    /// Unknown quality
15    #[default]
16    Unknown,
17    /// Poor quality (needs cleaning)
18    Poor,
19    /// Fair quality
20    Fair,
21    /// Good quality
22    Good,
23    /// Excellent quality (production ready)
24    Excellent,
25}
26
27impl DataQuality {
28    /// Get display color for quality.
29    #[must_use]
30    pub fn color(&self) -> Color {
31        match self {
32            Self::Unknown => Color::new(0.6, 0.6, 0.6, 1.0),
33            Self::Poor => Color::new(0.9, 0.3, 0.3, 1.0),
34            Self::Fair => Color::new(0.9, 0.7, 0.1, 1.0),
35            Self::Good => Color::new(0.4, 0.7, 0.3, 1.0),
36            Self::Excellent => Color::new(0.2, 0.7, 0.3, 1.0),
37        }
38    }
39
40    /// Get quality label.
41    #[must_use]
42    pub const fn label(&self) -> &'static str {
43        match self {
44            Self::Unknown => "Unknown",
45            Self::Poor => "Poor",
46            Self::Fair => "Fair",
47            Self::Good => "Good",
48            Self::Excellent => "Excellent",
49        }
50    }
51
52    /// Get quality score (0-100).
53    #[must_use]
54    pub const fn score(&self) -> u8 {
55        match self {
56            Self::Unknown => 0,
57            Self::Poor => 25,
58            Self::Fair => 50,
59            Self::Good => 75,
60            Self::Excellent => 100,
61        }
62    }
63}
64
65/// Data column/field schema.
66#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
67pub struct DataColumn {
68    /// Column name
69    pub name: String,
70    /// Data type
71    pub dtype: String,
72    /// Whether nullable
73    pub nullable: bool,
74    /// Description
75    pub description: Option<String>,
76}
77
78impl DataColumn {
79    /// Create a new column.
80    #[must_use]
81    pub fn new(name: impl Into<String>, dtype: impl Into<String>) -> Self {
82        Self {
83            name: name.into(),
84            dtype: dtype.into(),
85            nullable: false,
86            description: None,
87        }
88    }
89
90    /// Set nullable.
91    #[must_use]
92    pub const fn nullable(mut self) -> Self {
93        self.nullable = true;
94        self
95    }
96
97    /// Set description.
98    #[must_use]
99    pub fn description(mut self, desc: impl Into<String>) -> Self {
100        self.description = Some(desc.into());
101        self
102    }
103}
104
105/// Dataset statistics.
106#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)]
107pub struct DataStats {
108    /// Number of rows
109    pub rows: Option<u64>,
110    /// Number of columns
111    pub columns: Option<u32>,
112    /// Size in bytes
113    pub size_bytes: Option<u64>,
114    /// Null percentage
115    pub null_percentage: Option<f32>,
116    /// Duplicate percentage
117    pub duplicate_percentage: Option<f32>,
118}
119
120impl DataStats {
121    /// Create new stats.
122    #[must_use]
123    pub fn new() -> Self {
124        Self::default()
125    }
126
127    /// Set row count.
128    #[must_use]
129    pub const fn rows(mut self, count: u64) -> Self {
130        self.rows = Some(count);
131        self
132    }
133
134    /// Set column count.
135    #[must_use]
136    pub const fn columns(mut self, count: u32) -> Self {
137        self.columns = Some(count);
138        self
139    }
140
141    /// Set size in bytes.
142    #[must_use]
143    pub const fn size_bytes(mut self, bytes: u64) -> Self {
144        self.size_bytes = Some(bytes);
145        self
146    }
147
148    /// Set null percentage.
149    #[must_use]
150    pub fn null_percentage(mut self, pct: f32) -> Self {
151        self.null_percentage = Some(pct.clamp(0.0, 100.0));
152        self
153    }
154
155    /// Set duplicate percentage.
156    #[must_use]
157    pub fn duplicate_percentage(mut self, pct: f32) -> Self {
158        self.duplicate_percentage = Some(pct.clamp(0.0, 100.0));
159        self
160    }
161
162    /// Format size for display.
163    #[must_use]
164    pub fn formatted_size(&self) -> Option<String> {
165        self.size_bytes.map(|bytes| {
166            if bytes >= 1_000_000_000 {
167                format!("{:.1} GB", bytes as f64 / 1_000_000_000.0)
168            } else if bytes >= 1_000_000 {
169                format!("{:.1} MB", bytes as f64 / 1_000_000.0)
170            } else if bytes >= 1_000 {
171                format!("{:.1} KB", bytes as f64 / 1_000.0)
172            } else {
173                format!("{bytes} B")
174            }
175        })
176    }
177
178    /// Format row count for display.
179    #[must_use]
180    pub fn formatted_rows(&self) -> Option<String> {
181        self.rows.map(|r| {
182            if r >= 1_000_000 {
183                format!("{:.1}M rows", r as f64 / 1_000_000.0)
184            } else if r >= 1_000 {
185                format!("{:.1}K rows", r as f64 / 1_000.0)
186            } else {
187                format!("{r} rows")
188            }
189        })
190    }
191}
192
193/// `DataCard` widget for displaying dataset metadata.
194#[derive(Debug, Clone, Serialize, Deserialize)]
195pub struct DataCard {
196    /// Dataset name
197    name: String,
198    /// Dataset version
199    version: String,
200    /// Description
201    description: Option<String>,
202    /// Data quality
203    quality: DataQuality,
204    /// Data format (CSV, Parquet, JSON, etc.)
205    format: Option<String>,
206    /// Source URL or path
207    source: Option<String>,
208    /// Schema columns
209    schema: Vec<DataColumn>,
210    /// Statistics
211    stats: DataStats,
212    /// License
213    license: Option<String>,
214    /// Tags
215    tags: Vec<String>,
216    /// Custom metadata
217    metadata: HashMap<String, String>,
218    /// Card width
219    width: Option<f32>,
220    /// Card height
221    height: Option<f32>,
222    /// Background color
223    background: Color,
224    /// Border color
225    border_color: Color,
226    /// Corner radius
227    corner_radius: f32,
228    /// Show schema preview
229    show_schema: bool,
230    /// Accessible name
231    accessible_name_value: Option<String>,
232    /// Test ID
233    test_id_value: Option<String>,
234    /// Cached bounds
235    #[serde(skip)]
236    bounds: Rect,
237}
238
239impl Default for DataCard {
240    fn default() -> Self {
241        Self {
242            name: String::new(),
243            version: String::from("1.0.0"),
244            description: None,
245            quality: DataQuality::Unknown,
246            format: None,
247            source: None,
248            schema: Vec::new(),
249            stats: DataStats::default(),
250            license: None,
251            tags: Vec::new(),
252            metadata: HashMap::new(),
253            width: None,
254            height: None,
255            background: Color::WHITE,
256            border_color: Color::new(0.9, 0.9, 0.9, 1.0),
257            corner_radius: 8.0,
258            show_schema: true,
259            accessible_name_value: None,
260            test_id_value: None,
261            bounds: Rect::default(),
262        }
263    }
264}
265
266impl DataCard {
267    /// Create a new data card.
268    #[must_use]
269    pub fn new(name: impl Into<String>) -> Self {
270        Self {
271            name: name.into(),
272            ..Self::default()
273        }
274    }
275
276    /// Set name.
277    #[must_use]
278    pub fn name(mut self, name: impl Into<String>) -> Self {
279        self.name = name.into();
280        self
281    }
282
283    /// Set version.
284    #[must_use]
285    pub fn version(mut self, version: impl Into<String>) -> Self {
286        self.version = version.into();
287        self
288    }
289
290    /// Set description.
291    #[must_use]
292    pub fn description(mut self, desc: impl Into<String>) -> Self {
293        self.description = Some(desc.into());
294        self
295    }
296
297    /// Set quality.
298    #[must_use]
299    pub const fn quality(mut self, quality: DataQuality) -> Self {
300        self.quality = quality;
301        self
302    }
303
304    /// Set format.
305    #[must_use]
306    pub fn format(mut self, format: impl Into<String>) -> Self {
307        self.format = Some(format.into());
308        self
309    }
310
311    /// Set source.
312    #[must_use]
313    pub fn source(mut self, source: impl Into<String>) -> Self {
314        self.source = Some(source.into());
315        self
316    }
317
318    /// Add a schema column.
319    #[must_use]
320    pub fn column(mut self, col: DataColumn) -> Self {
321        self.schema.push(col);
322        self
323    }
324
325    /// Add multiple schema columns.
326    #[must_use]
327    pub fn columns(mut self, cols: impl IntoIterator<Item = DataColumn>) -> Self {
328        self.schema.extend(cols);
329        self
330    }
331
332    /// Set statistics.
333    #[must_use]
334    pub const fn stats(mut self, stats: DataStats) -> Self {
335        self.stats = stats;
336        self
337    }
338
339    /// Set license.
340    #[must_use]
341    pub fn license(mut self, license: impl Into<String>) -> Self {
342        self.license = Some(license.into());
343        self
344    }
345
346    /// Add a tag.
347    #[must_use]
348    pub fn tag(mut self, tag: impl Into<String>) -> Self {
349        self.tags.push(tag.into());
350        self
351    }
352
353    /// Add multiple tags.
354    #[must_use]
355    pub fn tags(mut self, tags: impl IntoIterator<Item = impl Into<String>>) -> Self {
356        self.tags.extend(tags.into_iter().map(Into::into));
357        self
358    }
359
360    /// Add custom metadata.
361    #[must_use]
362    pub fn metadata_entry(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
363        self.metadata.insert(key.into(), value.into());
364        self
365    }
366
367    /// Set width.
368    #[must_use]
369    pub fn width(mut self, width: f32) -> Self {
370        self.width = Some(width.max(200.0));
371        self
372    }
373
374    /// Set height.
375    #[must_use]
376    pub fn height(mut self, height: f32) -> Self {
377        self.height = Some(height.max(150.0));
378        self
379    }
380
381    /// Set background color.
382    #[must_use]
383    pub const fn background(mut self, color: Color) -> Self {
384        self.background = color;
385        self
386    }
387
388    /// Set border color.
389    #[must_use]
390    pub const fn border_color(mut self, color: Color) -> Self {
391        self.border_color = color;
392        self
393    }
394
395    /// Set corner radius.
396    #[must_use]
397    pub fn corner_radius(mut self, radius: f32) -> Self {
398        self.corner_radius = radius.max(0.0);
399        self
400    }
401
402    /// Set whether to show schema preview.
403    #[must_use]
404    pub const fn show_schema(mut self, show: bool) -> Self {
405        self.show_schema = show;
406        self
407    }
408
409    /// Set accessible name.
410    #[must_use]
411    pub fn accessible_name(mut self, name: impl Into<String>) -> Self {
412        self.accessible_name_value = Some(name.into());
413        self
414    }
415
416    /// Set test ID.
417    #[must_use]
418    pub fn test_id(mut self, id: impl Into<String>) -> Self {
419        self.test_id_value = Some(id.into());
420        self
421    }
422
423    // Getters
424
425    /// Get name.
426    #[must_use]
427    pub fn get_name(&self) -> &str {
428        &self.name
429    }
430
431    /// Get version.
432    #[must_use]
433    pub fn get_version(&self) -> &str {
434        &self.version
435    }
436
437    /// Get description.
438    #[must_use]
439    pub fn get_description(&self) -> Option<&str> {
440        self.description.as_deref()
441    }
442
443    /// Get quality.
444    #[must_use]
445    pub const fn get_quality(&self) -> DataQuality {
446        self.quality
447    }
448
449    /// Get format.
450    #[must_use]
451    pub fn get_format(&self) -> Option<&str> {
452        self.format.as_deref()
453    }
454
455    /// Get source.
456    #[must_use]
457    pub fn get_source(&self) -> Option<&str> {
458        self.source.as_deref()
459    }
460
461    /// Get schema.
462    #[must_use]
463    pub fn get_schema(&self) -> &[DataColumn] {
464        &self.schema
465    }
466
467    /// Get stats.
468    #[must_use]
469    pub const fn get_stats(&self) -> &DataStats {
470        &self.stats
471    }
472
473    /// Get license.
474    #[must_use]
475    pub fn get_license(&self) -> Option<&str> {
476        self.license.as_deref()
477    }
478
479    /// Get tags.
480    #[must_use]
481    pub fn get_tags(&self) -> &[String] {
482        &self.tags
483    }
484
485    /// Get custom metadata.
486    #[must_use]
487    pub fn get_metadata(&self, key: &str) -> Option<&str> {
488        self.metadata.get(key).map(String::as_str)
489    }
490
491    /// Check if has schema.
492    #[must_use]
493    pub fn has_schema(&self) -> bool {
494        !self.schema.is_empty()
495    }
496
497    /// Get column count.
498    #[must_use]
499    pub fn column_count(&self) -> usize {
500        self.schema.len()
501    }
502}
503
504impl Widget for DataCard {
505    fn type_id(&self) -> TypeId {
506        TypeId::of::<Self>()
507    }
508
509    fn measure(&self, constraints: Constraints) -> Size {
510        let width = self.width.unwrap_or(320.0);
511        let height = self.height.unwrap_or(220.0);
512        constraints.constrain(Size::new(width, height))
513    }
514
515    fn layout(&mut self, bounds: Rect) -> LayoutResult {
516        self.bounds = bounds;
517        LayoutResult {
518            size: bounds.size(),
519        }
520    }
521
522    #[allow(clippy::too_many_lines)]
523    fn paint(&self, canvas: &mut dyn Canvas) {
524        let padding = 16.0;
525
526        // Background
527        canvas.fill_rect(self.bounds, self.background);
528
529        // Border
530        canvas.stroke_rect(self.bounds, self.border_color, 1.0);
531
532        // Quality badge
533        let quality_color = self.quality.color();
534        let badge_rect = Rect::new(
535            self.bounds.x + self.bounds.width - 80.0,
536            self.bounds.y + padding,
537            70.0,
538            22.0,
539        );
540        canvas.fill_rect(badge_rect, quality_color);
541
542        let badge_style = TextStyle {
543            size: 10.0,
544            color: Color::WHITE,
545            ..TextStyle::default()
546        };
547        canvas.draw_text(
548            self.quality.label(),
549            Point::new(badge_rect.x + 10.0, badge_rect.y + 15.0),
550            &badge_style,
551        );
552
553        // Title
554        let title_style = TextStyle {
555            size: 18.0,
556            color: Color::new(0.1, 0.1, 0.1, 1.0),
557            ..TextStyle::default()
558        };
559        canvas.draw_text(
560            &self.name,
561            Point::new(self.bounds.x + padding, self.bounds.y + padding + 16.0),
562            &title_style,
563        );
564
565        // Version and format
566        let info_style = TextStyle {
567            size: 12.0,
568            color: Color::new(0.5, 0.5, 0.5, 1.0),
569            ..TextStyle::default()
570        };
571        let version_text = match &self.format {
572            Some(f) => format!("v{} • {}", self.version, f),
573            None => format!("v{}", self.version),
574        };
575        canvas.draw_text(
576            &version_text,
577            Point::new(self.bounds.x + padding, self.bounds.y + padding + 36.0),
578            &info_style,
579        );
580
581        let mut y_offset = padding + 50.0;
582
583        // Description
584        if let Some(ref desc) = self.description {
585            let desc_style = TextStyle {
586                size: 12.0,
587                color: Color::new(0.3, 0.3, 0.3, 1.0),
588                ..TextStyle::default()
589            };
590            canvas.draw_text(
591                desc,
592                Point::new(self.bounds.x + padding, self.bounds.y + y_offset + 12.0),
593                &desc_style,
594            );
595            y_offset += 24.0;
596        }
597
598        // Stats
599        let stats_style = TextStyle {
600            size: 11.0,
601            color: Color::new(0.4, 0.4, 0.4, 1.0),
602            ..TextStyle::default()
603        };
604        let value_style = TextStyle {
605            size: 14.0,
606            color: Color::new(0.2, 0.47, 0.96, 1.0),
607            ..TextStyle::default()
608        };
609
610        let mut sx = self.bounds.x + padding;
611        if let Some(rows) = self.stats.formatted_rows() {
612            canvas.draw_text(
613                "Rows",
614                Point::new(sx, self.bounds.y + y_offset + 12.0),
615                &stats_style,
616            );
617            canvas.draw_text(
618                &rows,
619                Point::new(sx, self.bounds.y + y_offset + 28.0),
620                &value_style,
621            );
622            sx += 80.0;
623        }
624        if let Some(cols) = self.stats.columns {
625            canvas.draw_text(
626                "Columns",
627                Point::new(sx, self.bounds.y + y_offset + 12.0),
628                &stats_style,
629            );
630            canvas.draw_text(
631                &cols.to_string(),
632                Point::new(sx, self.bounds.y + y_offset + 28.0),
633                &value_style,
634            );
635            sx += 70.0;
636        }
637        if let Some(size) = self.stats.formatted_size() {
638            canvas.draw_text(
639                "Size",
640                Point::new(sx, self.bounds.y + y_offset + 12.0),
641                &stats_style,
642            );
643            canvas.draw_text(
644                &size,
645                Point::new(sx, self.bounds.y + y_offset + 28.0),
646                &value_style,
647            );
648        }
649
650        if self.stats.rows.is_some()
651            || self.stats.columns.is_some()
652            || self.stats.size_bytes.is_some()
653        {
654            y_offset += 40.0;
655        }
656
657        // Schema preview
658        if self.show_schema && !self.schema.is_empty() {
659            let schema_style = TextStyle {
660                size: 10.0,
661                color: Color::new(0.4, 0.4, 0.4, 1.0),
662                ..TextStyle::default()
663            };
664            canvas.draw_text(
665                "Schema:",
666                Point::new(self.bounds.x + padding, self.bounds.y + y_offset + 12.0),
667                &schema_style,
668            );
669            y_offset += 18.0;
670
671            let col_style = TextStyle {
672                size: 10.0,
673                color: Color::new(0.2, 0.2, 0.2, 1.0),
674                ..TextStyle::default()
675            };
676            for col in self.schema.iter().take(4) {
677                let nullable = if col.nullable { "?" } else { "" };
678                let text = format!("{}: {}{}", col.name, col.dtype, nullable);
679                canvas.draw_text(
680                    &text,
681                    Point::new(
682                        self.bounds.x + padding + 8.0,
683                        self.bounds.y + y_offset + 12.0,
684                    ),
685                    &col_style,
686                );
687                y_offset += 14.0;
688            }
689            if self.schema.len() > 4 {
690                canvas.draw_text(
691                    &format!("... +{} more", self.schema.len() - 4),
692                    Point::new(
693                        self.bounds.x + padding + 8.0,
694                        self.bounds.y + y_offset + 12.0,
695                    ),
696                    &schema_style,
697                );
698                y_offset += 14.0;
699            }
700        }
701
702        // Tags
703        if !self.tags.is_empty() {
704            let tag_style = TextStyle {
705                size: 10.0,
706                color: Color::new(0.3, 0.3, 0.3, 1.0),
707                ..TextStyle::default()
708            };
709            let tag_bg = Color::new(0.95, 0.95, 0.95, 1.0);
710
711            let mut tx = self.bounds.x + padding;
712            for tag in self.tags.iter().take(5) {
713                let tag_width = (tag.len() as f32).mul_add(6.0, 12.0);
714                canvas.fill_rect(
715                    Rect::new(tx, self.bounds.y + y_offset + 4.0, tag_width, 18.0),
716                    tag_bg,
717                );
718                canvas.draw_text(
719                    tag,
720                    Point::new(tx + 6.0, self.bounds.y + y_offset + 17.0),
721                    &tag_style,
722                );
723                tx += tag_width + 6.0;
724            }
725        }
726    }
727
728    fn event(&mut self, _event: &presentar_core::Event) -> Option<Box<dyn Any + Send>> {
729        None
730    }
731
732    fn children(&self) -> &[Box<dyn Widget>] {
733        &[]
734    }
735
736    fn children_mut(&mut self) -> &mut [Box<dyn Widget>] {
737        &mut []
738    }
739
740    fn is_interactive(&self) -> bool {
741        false
742    }
743
744    fn is_focusable(&self) -> bool {
745        false
746    }
747
748    fn accessible_name(&self) -> Option<&str> {
749        self.accessible_name_value.as_deref().or(Some(&self.name))
750    }
751
752    fn accessible_role(&self) -> AccessibleRole {
753        AccessibleRole::Generic
754    }
755
756    fn test_id(&self) -> Option<&str> {
757        self.test_id_value.as_deref()
758    }
759}
760
761#[cfg(test)]
762mod tests {
763    use super::*;
764
765    // ===== DataQuality Tests =====
766
767    #[test]
768    fn test_data_quality_default() {
769        assert_eq!(DataQuality::default(), DataQuality::Unknown);
770    }
771
772    #[test]
773    fn test_data_quality_color() {
774        let excellent = DataQuality::Excellent;
775        let color = excellent.color();
776        assert!(color.g > color.r); // Green-ish
777    }
778
779    #[test]
780    fn test_data_quality_label() {
781        assert_eq!(DataQuality::Unknown.label(), "Unknown");
782        assert_eq!(DataQuality::Poor.label(), "Poor");
783        assert_eq!(DataQuality::Fair.label(), "Fair");
784        assert_eq!(DataQuality::Good.label(), "Good");
785        assert_eq!(DataQuality::Excellent.label(), "Excellent");
786    }
787
788    #[test]
789    fn test_data_quality_score() {
790        assert_eq!(DataQuality::Unknown.score(), 0);
791        assert_eq!(DataQuality::Poor.score(), 25);
792        assert_eq!(DataQuality::Fair.score(), 50);
793        assert_eq!(DataQuality::Good.score(), 75);
794        assert_eq!(DataQuality::Excellent.score(), 100);
795    }
796
797    // ===== DataColumn Tests =====
798
799    #[test]
800    fn test_data_column_new() {
801        let col = DataColumn::new("age", "int64");
802        assert_eq!(col.name, "age");
803        assert_eq!(col.dtype, "int64");
804        assert!(!col.nullable);
805        assert!(col.description.is_none());
806    }
807
808    #[test]
809    fn test_data_column_nullable() {
810        let col = DataColumn::new("email", "string").nullable();
811        assert!(col.nullable);
812    }
813
814    #[test]
815    fn test_data_column_description() {
816        let col = DataColumn::new("id", "uuid").description("Primary key");
817        assert_eq!(col.description, Some("Primary key".to_string()));
818    }
819
820    // ===== DataStats Tests =====
821
822    #[test]
823    fn test_data_stats_new() {
824        let stats = DataStats::new();
825        assert!(stats.rows.is_none());
826        assert!(stats.columns.is_none());
827    }
828
829    #[test]
830    fn test_data_stats_builder() {
831        let stats = DataStats::new()
832            .rows(1_000_000)
833            .columns(50)
834            .size_bytes(500_000_000)
835            .null_percentage(2.5)
836            .duplicate_percentage(0.1);
837
838        assert_eq!(stats.rows, Some(1_000_000));
839        assert_eq!(stats.columns, Some(50));
840        assert_eq!(stats.size_bytes, Some(500_000_000));
841        assert_eq!(stats.null_percentage, Some(2.5));
842        assert_eq!(stats.duplicate_percentage, Some(0.1));
843    }
844
845    #[test]
846    fn test_data_stats_null_percentage_clamped() {
847        let stats = DataStats::new().null_percentage(150.0);
848        assert_eq!(stats.null_percentage, Some(100.0));
849
850        let stats = DataStats::new().null_percentage(-10.0);
851        assert_eq!(stats.null_percentage, Some(0.0));
852    }
853
854    #[test]
855    fn test_data_stats_formatted_size_bytes() {
856        let stats = DataStats::new().size_bytes(500);
857        assert_eq!(stats.formatted_size(), Some("500 B".to_string()));
858    }
859
860    #[test]
861    fn test_data_stats_formatted_size_kb() {
862        let stats = DataStats::new().size_bytes(5_000);
863        assert_eq!(stats.formatted_size(), Some("5.0 KB".to_string()));
864    }
865
866    #[test]
867    fn test_data_stats_formatted_size_mb() {
868        let stats = DataStats::new().size_bytes(50_000_000);
869        assert_eq!(stats.formatted_size(), Some("50.0 MB".to_string()));
870    }
871
872    #[test]
873    fn test_data_stats_formatted_size_gb() {
874        let stats = DataStats::new().size_bytes(5_000_000_000);
875        assert_eq!(stats.formatted_size(), Some("5.0 GB".to_string()));
876    }
877
878    #[test]
879    fn test_data_stats_formatted_rows_small() {
880        let stats = DataStats::new().rows(500);
881        assert_eq!(stats.formatted_rows(), Some("500 rows".to_string()));
882    }
883
884    #[test]
885    fn test_data_stats_formatted_rows_thousands() {
886        let stats = DataStats::new().rows(50_000);
887        assert_eq!(stats.formatted_rows(), Some("50.0K rows".to_string()));
888    }
889
890    #[test]
891    fn test_data_stats_formatted_rows_millions() {
892        let stats = DataStats::new().rows(5_000_000);
893        assert_eq!(stats.formatted_rows(), Some("5.0M rows".to_string()));
894    }
895
896    // ===== DataCard Construction Tests =====
897
898    #[test]
899    fn test_data_card_new() {
900        let card = DataCard::new("customers");
901        assert_eq!(card.get_name(), "customers");
902        assert_eq!(card.get_version(), "1.0.0");
903        assert_eq!(card.get_quality(), DataQuality::Unknown);
904    }
905
906    #[test]
907    fn test_data_card_default() {
908        let card = DataCard::default();
909        assert!(card.name.is_empty());
910        assert_eq!(card.version, "1.0.0");
911    }
912
913    #[test]
914    fn test_data_card_builder() {
915        let card = DataCard::new("sales_data")
916            .version("2.0.0")
917            .description("Quarterly sales data")
918            .quality(DataQuality::Excellent)
919            .format("Parquet")
920            .source("s3://bucket/sales/")
921            .column(DataColumn::new("id", "int64"))
922            .column(DataColumn::new("amount", "float64"))
923            .stats(DataStats::new().rows(1_000_000).columns(20))
924            .license("MIT")
925            .tag("sales")
926            .tag("finance")
927            .metadata_entry("owner", "analytics-team")
928            .width(400.0)
929            .height(300.0)
930            .background(Color::WHITE)
931            .border_color(Color::new(0.8, 0.8, 0.8, 1.0))
932            .corner_radius(12.0)
933            .show_schema(true)
934            .accessible_name("Sales data card")
935            .test_id("sales-card");
936
937        assert_eq!(card.get_name(), "sales_data");
938        assert_eq!(card.get_version(), "2.0.0");
939        assert_eq!(card.get_description(), Some("Quarterly sales data"));
940        assert_eq!(card.get_quality(), DataQuality::Excellent);
941        assert_eq!(card.get_format(), Some("Parquet"));
942        assert_eq!(card.get_source(), Some("s3://bucket/sales/"));
943        assert_eq!(card.get_schema().len(), 2);
944        assert_eq!(card.get_stats().rows, Some(1_000_000));
945        assert_eq!(card.get_license(), Some("MIT"));
946        assert_eq!(card.get_tags().len(), 2);
947        assert_eq!(card.get_metadata("owner"), Some("analytics-team"));
948        assert_eq!(Widget::accessible_name(&card), Some("Sales data card"));
949        assert_eq!(Widget::test_id(&card), Some("sales-card"));
950    }
951
952    #[test]
953    fn test_data_card_columns() {
954        let cols = vec![DataColumn::new("a", "int"), DataColumn::new("b", "string")];
955        let card = DataCard::new("data").columns(cols);
956        assert_eq!(card.column_count(), 2);
957        assert!(card.has_schema());
958    }
959
960    #[test]
961    fn test_data_card_tags() {
962        let card = DataCard::new("data").tags(["raw", "cleaned", "normalized"]);
963        assert_eq!(card.get_tags().len(), 3);
964    }
965
966    // ===== Dimension Tests =====
967
968    #[test]
969    fn test_data_card_width_min() {
970        let card = DataCard::new("data").width(100.0);
971        assert_eq!(card.width, Some(200.0));
972    }
973
974    #[test]
975    fn test_data_card_height_min() {
976        let card = DataCard::new("data").height(50.0);
977        assert_eq!(card.height, Some(150.0));
978    }
979
980    #[test]
981    fn test_data_card_corner_radius_min() {
982        let card = DataCard::new("data").corner_radius(-5.0);
983        assert_eq!(card.corner_radius, 0.0);
984    }
985
986    // ===== Widget Trait Tests =====
987
988    #[test]
989    fn test_data_card_type_id() {
990        let card = DataCard::new("data");
991        assert_eq!(Widget::type_id(&card), TypeId::of::<DataCard>());
992    }
993
994    #[test]
995    fn test_data_card_measure_default() {
996        let card = DataCard::new("data");
997        let size = card.measure(Constraints::loose(Size::new(1000.0, 1000.0)));
998        assert_eq!(size.width, 320.0);
999        assert_eq!(size.height, 220.0);
1000    }
1001
1002    #[test]
1003    fn test_data_card_measure_custom() {
1004        let card = DataCard::new("data").width(400.0).height(300.0);
1005        let size = card.measure(Constraints::loose(Size::new(1000.0, 1000.0)));
1006        assert_eq!(size.width, 400.0);
1007        assert_eq!(size.height, 300.0);
1008    }
1009
1010    #[test]
1011    fn test_data_card_layout() {
1012        let mut card = DataCard::new("data");
1013        let bounds = Rect::new(10.0, 20.0, 320.0, 220.0);
1014        let result = card.layout(bounds);
1015        assert_eq!(result.size, Size::new(320.0, 220.0));
1016        assert_eq!(card.bounds, bounds);
1017    }
1018
1019    #[test]
1020    fn test_data_card_children() {
1021        let card = DataCard::new("data");
1022        assert!(card.children().is_empty());
1023    }
1024
1025    #[test]
1026    fn test_data_card_is_interactive() {
1027        let card = DataCard::new("data");
1028        assert!(!card.is_interactive());
1029    }
1030
1031    #[test]
1032    fn test_data_card_is_focusable() {
1033        let card = DataCard::new("data");
1034        assert!(!card.is_focusable());
1035    }
1036
1037    #[test]
1038    fn test_data_card_accessible_role() {
1039        let card = DataCard::new("data");
1040        assert_eq!(card.accessible_role(), AccessibleRole::Generic);
1041    }
1042
1043    #[test]
1044    fn test_data_card_accessible_name_from_name() {
1045        let card = DataCard::new("customers");
1046        assert_eq!(Widget::accessible_name(&card), Some("customers"));
1047    }
1048
1049    #[test]
1050    fn test_data_card_accessible_name_explicit() {
1051        let card = DataCard::new("customers").accessible_name("Customer dataset");
1052        assert_eq!(Widget::accessible_name(&card), Some("Customer dataset"));
1053    }
1054
1055    #[test]
1056    fn test_data_card_test_id() {
1057        let card = DataCard::new("data").test_id("data-card");
1058        assert_eq!(Widget::test_id(&card), Some("data-card"));
1059    }
1060
1061    // ===== Has Schema Tests =====
1062
1063    #[test]
1064    fn test_data_card_has_schema_false() {
1065        let card = DataCard::new("data");
1066        assert!(!card.has_schema());
1067    }
1068
1069    #[test]
1070    fn test_data_card_has_schema_true() {
1071        let card = DataCard::new("data").column(DataColumn::new("id", "int"));
1072        assert!(card.has_schema());
1073    }
1074
1075    // =========================================================================
1076    // Additional Coverage Tests
1077    // =========================================================================
1078
1079    #[test]
1080    fn test_data_quality_color_all_variants() {
1081        let _ = DataQuality::Unknown.color();
1082        let _ = DataQuality::Poor.color();
1083        let _ = DataQuality::Fair.color();
1084        let _ = DataQuality::Good.color();
1085        let _ = DataQuality::Excellent.color();
1086    }
1087
1088    #[test]
1089    fn test_data_stats_formatted_rows_none() {
1090        let stats = DataStats::new();
1091        assert!(stats.formatted_rows().is_none());
1092    }
1093
1094    #[test]
1095    fn test_data_stats_formatted_size_none() {
1096        let stats = DataStats::new();
1097        assert!(stats.formatted_size().is_none());
1098    }
1099
1100    #[test]
1101    fn test_data_card_children_mut() {
1102        let mut card = DataCard::new("data");
1103        assert!(card.children_mut().is_empty());
1104    }
1105
1106    #[test]
1107    fn test_data_card_event_returns_none() {
1108        let mut card = DataCard::new("data");
1109        let result = card.event(&presentar_core::Event::KeyDown {
1110            key: presentar_core::Key::Down,
1111        });
1112        assert!(result.is_none());
1113    }
1114
1115    #[test]
1116    fn test_data_card_test_id_none() {
1117        let card = DataCard::new("data");
1118        assert!(Widget::test_id(&card).is_none());
1119    }
1120
1121    #[test]
1122    fn test_data_stats_duplicate_percentage_clamped() {
1123        let stats = DataStats::new().duplicate_percentage(150.0);
1124        assert_eq!(stats.duplicate_percentage, Some(100.0));
1125
1126        let stats = DataStats::new().duplicate_percentage(-10.0);
1127        assert_eq!(stats.duplicate_percentage, Some(0.0));
1128    }
1129
1130    #[test]
1131    fn test_data_column_eq() {
1132        let col1 = DataColumn::new("id", "int64");
1133        let col2 = DataColumn::new("id", "int64");
1134        assert_eq!(col1.name, col2.name);
1135        assert_eq!(col1.dtype, col2.dtype);
1136    }
1137}