Skip to main content

presentar_terminal/widgets/
feature_importance.rs

1//! Feature importance plot widget.
2//!
3//! Implements SPEC-024 Section 26.5.3.
4
5use presentar_core::{
6    Brick, BrickAssertion, BrickBudget, BrickVerification, Canvas, Color, Constraints, Event,
7    LayoutResult, Point, Rect, Size, TextStyle, TypeId, Widget,
8};
9use std::any::Any;
10use std::time::Duration;
11
12/// Feature importance plot widget.
13#[derive(Debug, Clone)]
14pub struct FeatureImportance {
15    /// Feature names.
16    features: Vec<String>,
17    /// Importance values.
18    importances: Vec<f64>,
19    /// Sort by importance.
20    sorted: bool,
21    /// Show values.
22    show_values: bool,
23    /// Bar color.
24    bar_color: Color,
25    /// Maximum features to display.
26    max_features: usize,
27    /// Cached bounds.
28    bounds: Rect,
29}
30
31impl FeatureImportance {
32    /// Create a new feature importance plot.
33    #[must_use]
34    pub fn new(features: Vec<String>, importances: Vec<f64>) -> Self {
35        Self {
36            features,
37            importances,
38            sorted: true,
39            show_values: true,
40            bar_color: Color::new(0.2, 0.6, 0.9, 1.0),
41            max_features: 20,
42            bounds: Rect::default(),
43        }
44    }
45
46    /// Toggle sorting.
47    #[must_use]
48    pub fn with_sorted(mut self, sorted: bool) -> Self {
49        self.sorted = sorted;
50        self
51    }
52
53    /// Toggle value display.
54    #[must_use]
55    pub fn with_show_values(mut self, show: bool) -> Self {
56        self.show_values = show;
57        self
58    }
59
60    /// Set bar color.
61    #[must_use]
62    pub fn with_color(mut self, color: Color) -> Self {
63        self.bar_color = color;
64        self
65    }
66
67    /// Set maximum features to display.
68    #[must_use]
69    pub fn with_max_features(mut self, max: usize) -> Self {
70        self.max_features = max;
71        self
72    }
73
74    /// Get sorted indices.
75    fn sorted_indices(&self) -> Vec<usize> {
76        let mut indices: Vec<usize> = (0..self.importances.len()).collect();
77        if self.sorted {
78            indices.sort_by(|&a, &b| {
79                self.importances[b]
80                    .partial_cmp(&self.importances[a])
81                    .unwrap_or(std::cmp::Ordering::Equal)
82            });
83        }
84        indices.truncate(self.max_features);
85        indices
86    }
87
88    /// Get maximum importance.
89    fn max_importance(&self) -> f64 {
90        self.importances
91            .iter()
92            .copied()
93            .filter(|v| v.is_finite())
94            .fold(0.0f64, f64::max)
95            .max(1e-10)
96    }
97}
98
99impl Default for FeatureImportance {
100    fn default() -> Self {
101        Self::new(Vec::new(), Vec::new())
102    }
103}
104
105impl Widget for FeatureImportance {
106    fn type_id(&self) -> TypeId {
107        TypeId::of::<Self>()
108    }
109
110    fn measure(&self, constraints: Constraints) -> Size {
111        let height = (self.features.len().min(self.max_features) + 2) as f32;
112        Size::new(
113            constraints.max_width.min(60.0),
114            constraints.max_height.min(height),
115        )
116    }
117
118    fn layout(&mut self, bounds: Rect) -> LayoutResult {
119        self.bounds = bounds;
120        LayoutResult {
121            size: Size::new(bounds.width, bounds.height),
122        }
123    }
124
125    fn paint(&self, canvas: &mut dyn Canvas) {
126        if self.bounds.width < 20.0 || self.bounds.height < 3.0 || self.features.is_empty() {
127            return;
128        }
129
130        let indices = self.sorted_indices();
131        let max_imp = self.max_importance();
132
133        // Calculate layout
134        let label_width = 15.0f32;
135        let value_width = if self.show_values { 8.0 } else { 0.0 };
136        let bar_start = self.bounds.x + label_width;
137        let bar_max_width = self.bounds.width - label_width - value_width - 1.0;
138
139        let label_style = TextStyle {
140            color: Color::new(0.7, 0.7, 0.7, 1.0),
141            ..Default::default()
142        };
143
144        let bar_style = TextStyle {
145            color: self.bar_color,
146            ..Default::default()
147        };
148
149        let value_style = TextStyle {
150            color: Color::new(0.5, 0.5, 0.5, 1.0),
151            ..Default::default()
152        };
153
154        // Title
155        canvas.draw_text(
156            "Feature Importance",
157            Point::new(self.bounds.x, self.bounds.y),
158            &label_style,
159        );
160
161        // Draw bars
162        let available_rows = (self.bounds.height as usize).saturating_sub(2);
163        for (row, &idx) in indices.iter().enumerate().take(available_rows) {
164            let y = self.bounds.y + row as f32 + 1.0;
165
166            // Feature name (truncated)
167            let name: String = self.features[idx].chars().take(14).collect();
168            canvas.draw_text(
169                &format!("{name:>14}"),
170                Point::new(self.bounds.x, y),
171                &label_style,
172            );
173
174            // Bar
175            let importance = self.importances[idx].max(0.0);
176            let bar_width = ((importance / max_imp) * bar_max_width as f64) as usize;
177
178            if bar_width > 0 {
179                let bar_str: String = "█".repeat(bar_width);
180                canvas.draw_text(&bar_str, Point::new(bar_start, y), &bar_style);
181            }
182
183            // Value
184            if self.show_values {
185                let value_x = bar_start + bar_max_width + 1.0;
186                canvas.draw_text(
187                    &format!("{importance:.3}"),
188                    Point::new(value_x, y),
189                    &value_style,
190                );
191            }
192        }
193
194        // Show "..." if truncated
195        if indices.len() > available_rows {
196            let y = self.bounds.y + self.bounds.height - 1.0;
197            canvas.draw_text(
198                &format!("... and {} more", indices.len() - available_rows),
199                Point::new(self.bounds.x, y),
200                &label_style,
201            );
202        }
203    }
204
205    fn event(&mut self, _event: &Event) -> Option<Box<dyn Any + Send>> {
206        None
207    }
208
209    fn children(&self) -> &[Box<dyn Widget>] {
210        &[]
211    }
212
213    fn children_mut(&mut self) -> &mut [Box<dyn Widget>] {
214        &mut []
215    }
216}
217
218impl Brick for FeatureImportance {
219    fn brick_name(&self) -> &'static str {
220        "FeatureImportance"
221    }
222
223    fn assertions(&self) -> &[BrickAssertion] {
224        static ASSERTIONS: &[BrickAssertion] = &[BrickAssertion::max_latency_ms(16)];
225        ASSERTIONS
226    }
227
228    fn budget(&self) -> BrickBudget {
229        BrickBudget::uniform(16)
230    }
231
232    fn verify(&self) -> BrickVerification {
233        let mut passed = Vec::new();
234        let mut failed = Vec::new();
235
236        if self.bounds.width >= 20.0 && self.bounds.height >= 3.0 {
237            passed.push(BrickAssertion::max_latency_ms(16));
238        } else {
239            failed.push((
240                BrickAssertion::max_latency_ms(16),
241                "Size too small".to_string(),
242            ));
243        }
244
245        // Check length consistency
246        if self.features.len() != self.importances.len() {
247            failed.push((
248                BrickAssertion::max_latency_ms(16),
249                "Features and importances length mismatch".to_string(),
250            ));
251        }
252
253        BrickVerification {
254            passed,
255            failed,
256            verification_time: Duration::from_micros(5),
257        }
258    }
259
260    fn to_html(&self) -> String {
261        String::new()
262    }
263
264    fn to_css(&self) -> String {
265        String::new()
266    }
267}
268
269#[cfg(test)]
270mod tests {
271    use super::*;
272    use crate::direct::{CellBuffer, DirectTerminalCanvas};
273
274    #[test]
275    fn test_feature_importance_new() {
276        let features = vec!["A".to_string(), "B".to_string(), "C".to_string()];
277        let importances = vec![0.5, 0.3, 0.2];
278        let plot = FeatureImportance::new(features, importances);
279        assert_eq!(plot.features.len(), 3);
280        assert_eq!(plot.importances.len(), 3);
281    }
282
283    #[test]
284    fn test_feature_importance_empty() {
285        let plot = FeatureImportance::default();
286        assert!(plot.features.is_empty());
287    }
288
289    #[test]
290    fn test_feature_importance_sorted_indices() {
291        let features = vec!["A".to_string(), "B".to_string(), "C".to_string()];
292        let importances = vec![0.2, 0.5, 0.3];
293        let plot = FeatureImportance::new(features, importances);
294        let indices = plot.sorted_indices();
295        assert_eq!(indices[0], 1); // B has highest importance
296    }
297
298    #[test]
299    fn test_feature_importance_max() {
300        let features = vec!["A".to_string(), "B".to_string()];
301        let importances = vec![0.2, 0.8];
302        let plot = FeatureImportance::new(features, importances);
303        assert!((plot.max_importance() - 0.8).abs() < 0.01);
304    }
305
306    #[test]
307    fn test_feature_importance_paint() {
308        let features = vec![
309            "feature_a".to_string(),
310            "feature_b".to_string(),
311            "feature_c".to_string(),
312            "feature_d".to_string(),
313            "feature_e".to_string(),
314        ];
315        let importances = vec![0.35, 0.25, 0.2, 0.12, 0.08];
316
317        let mut plot = FeatureImportance::new(features, importances);
318        let bounds = Rect::new(0.0, 0.0, 60.0, 10.0);
319        plot.layout(bounds);
320
321        let mut buffer = CellBuffer::new(60, 10);
322        let mut canvas = DirectTerminalCanvas::new(&mut buffer);
323        plot.paint(&mut canvas);
324    }
325
326    #[test]
327    fn test_feature_importance_with_options() {
328        let features = vec!["A".to_string()];
329        let importances = vec![0.5];
330        let plot = FeatureImportance::new(features, importances)
331            .with_sorted(false)
332            .with_show_values(false)
333            .with_color(Color::RED)
334            .with_max_features(10);
335
336        assert!(!plot.sorted);
337        assert!(!plot.show_values);
338        assert_eq!(plot.max_features, 10);
339    }
340
341    #[test]
342    fn test_feature_importance_verify() {
343        let features = vec!["A".to_string(), "B".to_string()];
344        let importances = vec![0.5, 0.3];
345        let mut plot = FeatureImportance::new(features, importances);
346        plot.bounds = Rect::new(0.0, 0.0, 60.0, 10.0);
347        assert!(plot.verify().is_valid());
348    }
349
350    #[test]
351    fn test_feature_importance_verify_mismatch() {
352        let features = vec!["A".to_string(), "B".to_string()];
353        let importances = vec![0.5]; // Length mismatch
354        let mut plot = FeatureImportance::new(features, importances);
355        plot.bounds = Rect::new(0.0, 0.0, 60.0, 10.0);
356        assert!(!plot.verify().is_valid());
357    }
358
359    #[test]
360    fn test_feature_importance_brick_name() {
361        let plot = FeatureImportance::default();
362        assert_eq!(plot.brick_name(), "FeatureImportance");
363    }
364
365    // =========================================================================
366    // Additional coverage tests
367    // =========================================================================
368
369    #[test]
370    fn test_sorted_indices_unsorted() {
371        let features = vec!["A".to_string(), "B".to_string(), "C".to_string()];
372        let importances = vec![0.2, 0.5, 0.3];
373        let plot = FeatureImportance::new(features, importances).with_sorted(false);
374        let indices = plot.sorted_indices();
375        // Should maintain original order
376        assert_eq!(indices[0], 0);
377        assert_eq!(indices[1], 1);
378        assert_eq!(indices[2], 2);
379    }
380
381    #[test]
382    fn test_sorted_indices_with_nan() {
383        let features = vec!["A".to_string(), "B".to_string(), "C".to_string()];
384        let importances = vec![0.2, f64::NAN, 0.5];
385        let plot = FeatureImportance::new(features, importances);
386        let indices = plot.sorted_indices();
387        // NaN comparison should not panic
388        assert_eq!(indices.len(), 3);
389    }
390
391    #[test]
392    fn test_sorted_indices_truncates() {
393        let features: Vec<String> = (0..30).map(|i| format!("F{i}")).collect();
394        let importances: Vec<f64> = (0..30).map(|i| i as f64 / 30.0).collect();
395        let plot = FeatureImportance::new(features, importances).with_max_features(5);
396        let indices = plot.sorted_indices();
397        assert_eq!(indices.len(), 5);
398    }
399
400    #[test]
401    fn test_max_importance_empty() {
402        let plot = FeatureImportance::default();
403        let max = plot.max_importance();
404        // Should return minimum value to avoid division by zero
405        assert!(max > 0.0);
406    }
407
408    #[test]
409    fn test_max_importance_all_nan() {
410        let features = vec!["A".to_string(), "B".to_string()];
411        let importances = vec![f64::NAN, f64::NAN];
412        let plot = FeatureImportance::new(features, importances);
413        let max = plot.max_importance();
414        // Should return minimum to avoid div by zero
415        assert!(max > 0.0);
416    }
417
418    #[test]
419    fn test_max_importance_with_infinity() {
420        let features = vec!["A".to_string(), "B".to_string()];
421        let importances = vec![f64::INFINITY, 0.5];
422        let plot = FeatureImportance::new(features, importances);
423        let max = plot.max_importance();
424        // Should filter infinity
425        assert!(max < f64::INFINITY);
426    }
427
428    #[test]
429    fn test_max_importance_negative() {
430        let features = vec!["A".to_string(), "B".to_string()];
431        let importances = vec![-0.5, -0.3];
432        let plot = FeatureImportance::new(features, importances);
433        let max = plot.max_importance();
434        // max should at least be the min threshold
435        assert!(max > 0.0);
436    }
437
438    #[test]
439    fn test_paint_too_small_width() {
440        let features = vec!["A".to_string()];
441        let importances = vec![0.5];
442        let mut plot = FeatureImportance::new(features, importances);
443        plot.bounds = Rect::new(0.0, 0.0, 10.0, 10.0); // Too narrow
444
445        let mut buffer = CellBuffer::new(10, 10);
446        let mut canvas = DirectTerminalCanvas::new(&mut buffer);
447        plot.paint(&mut canvas); // Should return early
448    }
449
450    #[test]
451    fn test_paint_too_small_height() {
452        let features = vec!["A".to_string()];
453        let importances = vec![0.5];
454        let mut plot = FeatureImportance::new(features, importances);
455        plot.bounds = Rect::new(0.0, 0.0, 60.0, 2.0); // Too short
456
457        let mut buffer = CellBuffer::new(60, 2);
458        let mut canvas = DirectTerminalCanvas::new(&mut buffer);
459        plot.paint(&mut canvas); // Should return early
460    }
461
462    #[test]
463    fn test_paint_empty_features() {
464        let plot = FeatureImportance::default();
465        let mut buffer = CellBuffer::new(60, 10);
466        let mut canvas = DirectTerminalCanvas::new(&mut buffer);
467        plot.paint(&mut canvas); // Should return early
468    }
469
470    #[test]
471    fn test_paint_without_values() {
472        let features = vec!["feature_a".to_string(), "feature_b".to_string()];
473        let importances = vec![0.5, 0.3];
474        let mut plot = FeatureImportance::new(features, importances).with_show_values(false);
475        plot.bounds = Rect::new(0.0, 0.0, 60.0, 10.0);
476
477        let mut buffer = CellBuffer::new(60, 10);
478        let mut canvas = DirectTerminalCanvas::new(&mut buffer);
479        plot.paint(&mut canvas);
480    }
481
482    #[test]
483    fn test_paint_with_truncation() {
484        // More features than available rows
485        let features: Vec<String> = (0..20).map(|i| format!("F{i}")).collect();
486        let importances: Vec<f64> = (0..20).map(|i| i as f64 / 20.0).collect();
487        let mut plot = FeatureImportance::new(features, importances);
488        plot.bounds = Rect::new(0.0, 0.0, 60.0, 8.0); // Only ~6 rows for features
489
490        let mut buffer = CellBuffer::new(60, 8);
491        let mut canvas = DirectTerminalCanvas::new(&mut buffer);
492        plot.paint(&mut canvas); // Should show "... and X more"
493    }
494
495    #[test]
496    fn test_paint_long_feature_names() {
497        let features = vec!["very_long_feature_name_that_exceeds_14_chars".to_string()];
498        let importances = vec![0.5];
499        let mut plot = FeatureImportance::new(features, importances);
500        plot.bounds = Rect::new(0.0, 0.0, 60.0, 10.0);
501
502        let mut buffer = CellBuffer::new(60, 10);
503        let mut canvas = DirectTerminalCanvas::new(&mut buffer);
504        plot.paint(&mut canvas); // Should truncate name
505    }
506
507    #[test]
508    fn test_paint_zero_bar_width() {
509        let features = vec!["A".to_string()];
510        let importances = vec![0.0]; // Zero importance
511        let mut plot = FeatureImportance::new(features, importances);
512        plot.bounds = Rect::new(0.0, 0.0, 60.0, 10.0);
513
514        let mut buffer = CellBuffer::new(60, 10);
515        let mut canvas = DirectTerminalCanvas::new(&mut buffer);
516        plot.paint(&mut canvas);
517    }
518
519    #[test]
520    fn test_paint_negative_importance() {
521        let features = vec!["A".to_string()];
522        let importances = vec![-0.5]; // Negative importance
523        let mut plot = FeatureImportance::new(features, importances);
524        plot.bounds = Rect::new(0.0, 0.0, 60.0, 10.0);
525
526        let mut buffer = CellBuffer::new(60, 10);
527        let mut canvas = DirectTerminalCanvas::new(&mut buffer);
528        plot.paint(&mut canvas); // Should handle gracefully (clamp to 0)
529    }
530
531    #[test]
532    fn test_verify_too_small() {
533        let features = vec!["A".to_string()];
534        let importances = vec![0.5];
535        let mut plot = FeatureImportance::new(features, importances);
536        plot.bounds = Rect::new(0.0, 0.0, 10.0, 2.0); // Too small
537        let result = plot.verify();
538        assert!(!result.failed.is_empty());
539    }
540
541    #[test]
542    fn test_widget_type_id() {
543        let plot = FeatureImportance::default();
544        let id = Widget::type_id(&plot);
545        assert_eq!(id, TypeId::of::<FeatureImportance>());
546    }
547
548    #[test]
549    fn test_widget_measure() {
550        let features = vec!["A".to_string(), "B".to_string(), "C".to_string()];
551        let importances = vec![0.5, 0.3, 0.2];
552        let plot = FeatureImportance::new(features, importances);
553        let constraints = Constraints::tight(Size::new(100.0, 50.0));
554        let size = plot.measure(constraints);
555        assert!(size.width <= 60.0);
556        assert!(size.height <= 50.0);
557    }
558
559    #[test]
560    fn test_widget_layout() {
561        let mut plot = FeatureImportance::default();
562        let bounds = Rect::new(10.0, 20.0, 40.0, 15.0);
563        let result = plot.layout(bounds);
564        assert_eq!(result.size.width, 40.0);
565        assert_eq!(result.size.height, 15.0);
566        assert_eq!(plot.bounds, bounds);
567    }
568
569    #[test]
570    fn test_widget_event() {
571        let mut plot = FeatureImportance::default();
572        let result = plot.event(&Event::FocusIn);
573        assert!(result.is_none());
574    }
575
576    #[test]
577    fn test_widget_children() {
578        let plot = FeatureImportance::default();
579        assert!(plot.children().is_empty());
580    }
581
582    #[test]
583    fn test_widget_children_mut() {
584        let mut plot = FeatureImportance::default();
585        assert!(plot.children_mut().is_empty());
586    }
587
588    #[test]
589    fn test_brick_assertions() {
590        let plot = FeatureImportance::default();
591        let assertions = plot.assertions();
592        assert!(!assertions.is_empty());
593    }
594
595    #[test]
596    fn test_brick_budget() {
597        let plot = FeatureImportance::default();
598        let budget = plot.budget();
599        assert!(budget.total_ms > 0);
600    }
601
602    #[test]
603    fn test_brick_to_html() {
604        let plot = FeatureImportance::default();
605        assert!(plot.to_html().is_empty());
606    }
607
608    #[test]
609    fn test_brick_to_css() {
610        let plot = FeatureImportance::default();
611        assert!(plot.to_css().is_empty());
612    }
613
614    #[test]
615    fn test_clone() {
616        let features = vec!["A".to_string(), "B".to_string()];
617        let importances = vec![0.5, 0.3];
618        let plot = FeatureImportance::new(features, importances)
619            .with_sorted(true)
620            .with_show_values(false)
621            .with_color(Color::GREEN)
622            .with_max_features(5);
623
624        let cloned = plot;
625        assert_eq!(cloned.features.len(), 2);
626        assert!(cloned.sorted);
627        assert!(!cloned.show_values);
628        assert_eq!(cloned.max_features, 5);
629    }
630
631    #[test]
632    fn test_debug() {
633        let plot = FeatureImportance::default();
634        let debug_str = format!("{:?}", plot);
635        assert!(debug_str.contains("FeatureImportance"));
636    }
637
638    #[test]
639    fn test_default_values() {
640        let features = vec!["A".to_string()];
641        let importances = vec![0.5];
642        let plot = FeatureImportance::new(features, importances);
643        assert!(plot.sorted);
644        assert!(plot.show_values);
645        assert_eq!(plot.max_features, 20);
646    }
647}