1use 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#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
13pub enum DataQuality {
14 #[default]
16 Unknown,
17 Poor,
19 Fair,
21 Good,
23 Excellent,
25}
26
27impl DataQuality {
28 #[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 #[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 #[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#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
67pub struct DataColumn {
68 pub name: String,
70 pub dtype: String,
72 pub nullable: bool,
74 pub description: Option<String>,
76}
77
78impl DataColumn {
79 #[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 #[must_use]
92 pub const fn nullable(mut self) -> Self {
93 self.nullable = true;
94 self
95 }
96
97 #[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#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)]
107pub struct DataStats {
108 pub rows: Option<u64>,
110 pub columns: Option<u32>,
112 pub size_bytes: Option<u64>,
114 pub null_percentage: Option<f32>,
116 pub duplicate_percentage: Option<f32>,
118}
119
120impl DataStats {
121 #[must_use]
123 pub fn new() -> Self {
124 Self::default()
125 }
126
127 #[must_use]
129 pub const fn rows(mut self, count: u64) -> Self {
130 self.rows = Some(count);
131 self
132 }
133
134 #[must_use]
136 pub const fn columns(mut self, count: u32) -> Self {
137 self.columns = Some(count);
138 self
139 }
140
141 #[must_use]
143 pub const fn size_bytes(mut self, bytes: u64) -> Self {
144 self.size_bytes = Some(bytes);
145 self
146 }
147
148 #[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 #[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 #[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 #[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#[derive(Debug, Clone, Serialize, Deserialize)]
195pub struct DataCard {
196 name: String,
198 version: String,
200 description: Option<String>,
202 quality: DataQuality,
204 format: Option<String>,
206 source: Option<String>,
208 schema: Vec<DataColumn>,
210 stats: DataStats,
212 license: Option<String>,
214 tags: Vec<String>,
216 metadata: HashMap<String, String>,
218 width: Option<f32>,
220 height: Option<f32>,
222 background: Color,
224 border_color: Color,
226 corner_radius: f32,
228 show_schema: bool,
230 accessible_name_value: Option<String>,
232 test_id_value: Option<String>,
234 #[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 #[must_use]
269 pub fn new(name: impl Into<String>) -> Self {
270 Self {
271 name: name.into(),
272 ..Self::default()
273 }
274 }
275
276 #[must_use]
278 pub fn name(mut self, name: impl Into<String>) -> Self {
279 self.name = name.into();
280 self
281 }
282
283 #[must_use]
285 pub fn version(mut self, version: impl Into<String>) -> Self {
286 self.version = version.into();
287 self
288 }
289
290 #[must_use]
292 pub fn description(mut self, desc: impl Into<String>) -> Self {
293 self.description = Some(desc.into());
294 self
295 }
296
297 #[must_use]
299 pub const fn quality(mut self, quality: DataQuality) -> Self {
300 self.quality = quality;
301 self
302 }
303
304 #[must_use]
306 pub fn format(mut self, format: impl Into<String>) -> Self {
307 self.format = Some(format.into());
308 self
309 }
310
311 #[must_use]
313 pub fn source(mut self, source: impl Into<String>) -> Self {
314 self.source = Some(source.into());
315 self
316 }
317
318 #[must_use]
320 pub fn column(mut self, col: DataColumn) -> Self {
321 self.schema.push(col);
322 self
323 }
324
325 #[must_use]
327 pub fn columns(mut self, cols: impl IntoIterator<Item = DataColumn>) -> Self {
328 self.schema.extend(cols);
329 self
330 }
331
332 #[must_use]
334 pub const fn stats(mut self, stats: DataStats) -> Self {
335 self.stats = stats;
336 self
337 }
338
339 #[must_use]
341 pub fn license(mut self, license: impl Into<String>) -> Self {
342 self.license = Some(license.into());
343 self
344 }
345
346 #[must_use]
348 pub fn tag(mut self, tag: impl Into<String>) -> Self {
349 self.tags.push(tag.into());
350 self
351 }
352
353 #[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 #[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 #[must_use]
369 pub fn width(mut self, width: f32) -> Self {
370 self.width = Some(width.max(200.0));
371 self
372 }
373
374 #[must_use]
376 pub fn height(mut self, height: f32) -> Self {
377 self.height = Some(height.max(150.0));
378 self
379 }
380
381 #[must_use]
383 pub const fn background(mut self, color: Color) -> Self {
384 self.background = color;
385 self
386 }
387
388 #[must_use]
390 pub const fn border_color(mut self, color: Color) -> Self {
391 self.border_color = color;
392 self
393 }
394
395 #[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 #[must_use]
404 pub const fn show_schema(mut self, show: bool) -> Self {
405 self.show_schema = show;
406 self
407 }
408
409 #[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 #[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 #[must_use]
427 pub fn get_name(&self) -> &str {
428 &self.name
429 }
430
431 #[must_use]
433 pub fn get_version(&self) -> &str {
434 &self.version
435 }
436
437 #[must_use]
439 pub fn get_description(&self) -> Option<&str> {
440 self.description.as_deref()
441 }
442
443 #[must_use]
445 pub const fn get_quality(&self) -> DataQuality {
446 self.quality
447 }
448
449 #[must_use]
451 pub fn get_format(&self) -> Option<&str> {
452 self.format.as_deref()
453 }
454
455 #[must_use]
457 pub fn get_source(&self) -> Option<&str> {
458 self.source.as_deref()
459 }
460
461 #[must_use]
463 pub fn get_schema(&self) -> &[DataColumn] {
464 &self.schema
465 }
466
467 #[must_use]
469 pub const fn get_stats(&self) -> &DataStats {
470 &self.stats
471 }
472
473 #[must_use]
475 pub fn get_license(&self) -> Option<&str> {
476 self.license.as_deref()
477 }
478
479 #[must_use]
481 pub fn get_tags(&self) -> &[String] {
482 &self.tags
483 }
484
485 #[must_use]
487 pub fn get_metadata(&self, key: &str) -> Option<&str> {
488 self.metadata.get(key).map(String::as_str)
489 }
490
491 #[must_use]
493 pub fn has_schema(&self) -> bool {
494 !self.schema.is_empty()
495 }
496
497 #[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 canvas.fill_rect(self.bounds, self.background);
528
529 canvas.stroke_rect(self.bounds, self.border_color, 1.0);
531
532 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 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 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 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 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 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 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 #[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); }
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 #[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 #[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 #[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 #[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 #[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 #[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 #[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}