1use 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#[derive(Debug, Clone)]
14pub struct FeatureImportance {
15 features: Vec<String>,
17 importances: Vec<f64>,
19 sorted: bool,
21 show_values: bool,
23 bar_color: Color,
25 max_features: usize,
27 bounds: Rect,
29}
30
31impl FeatureImportance {
32 #[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 #[must_use]
48 pub fn with_sorted(mut self, sorted: bool) -> Self {
49 self.sorted = sorted;
50 self
51 }
52
53 #[must_use]
55 pub fn with_show_values(mut self, show: bool) -> Self {
56 self.show_values = show;
57 self
58 }
59
60 #[must_use]
62 pub fn with_color(mut self, color: Color) -> Self {
63 self.bar_color = color;
64 self
65 }
66
67 #[must_use]
69 pub fn with_max_features(mut self, max: usize) -> Self {
70 self.max_features = max;
71 self
72 }
73
74 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 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 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 canvas.draw_text(
156 "Feature Importance",
157 Point::new(self.bounds.x, self.bounds.y),
158 &label_style,
159 );
160
161 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 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 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 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 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 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); }
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]; 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 #[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 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 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 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 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 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 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); let mut buffer = CellBuffer::new(10, 10);
446 let mut canvas = DirectTerminalCanvas::new(&mut buffer);
447 plot.paint(&mut canvas); }
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); let mut buffer = CellBuffer::new(60, 2);
458 let mut canvas = DirectTerminalCanvas::new(&mut buffer);
459 plot.paint(&mut canvas); }
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); }
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 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); let mut buffer = CellBuffer::new(60, 8);
491 let mut canvas = DirectTerminalCanvas::new(&mut buffer);
492 plot.paint(&mut canvas); }
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); }
506
507 #[test]
508 fn test_paint_zero_bar_width() {
509 let features = vec!["A".to_string()];
510 let importances = vec![0.0]; 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]; 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); }
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); 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}