Skip to main content

presentar_terminal/widgets/
treemap.rs

1//! Treemap widget for hierarchical space-filling visualization.
2//!
3//! Implements P207 from SPEC-024 Section 15.2.
4//! Uses squarify algorithm for optimal aspect ratios.
5
6use crate::theme::Gradient;
7use presentar_core::{
8    Brick, BrickAssertion, BrickBudget, BrickVerification, Canvas, Color, Constraints, Event,
9    LayoutResult, Point, Rect, Size, TextStyle, TypeId, Widget,
10};
11use std::any::Any;
12use std::time::Duration;
13
14/// A node in the treemap hierarchy.
15#[derive(Debug, Clone)]
16pub struct TreemapNode {
17    /// Node label.
18    pub label: String,
19    /// Node value (size).
20    pub value: f64,
21    /// Optional color override.
22    pub color: Option<Color>,
23    /// Child nodes.
24    pub children: Vec<Self>,
25    /// Flash intensity (0.0-1.0) for change indication (GAP-TREE-001).
26    /// Decays over time to create visual flash effect.
27    pub flash_intensity: f32,
28    /// Previous value for change detection.
29    pub previous_value: Option<f64>,
30}
31
32impl TreemapNode {
33    /// Create a leaf node.
34    #[must_use]
35    pub fn leaf(label: &str, value: f64) -> Self {
36        Self {
37            label: label.to_string(),
38            value,
39            color: None,
40            children: Vec::new(),
41            flash_intensity: 0.0,
42            previous_value: None,
43        }
44    }
45
46    /// Create a leaf node with color.
47    #[must_use]
48    pub fn leaf_colored(label: &str, value: f64, color: Color) -> Self {
49        Self {
50            label: label.to_string(),
51            value,
52            color: Some(color),
53            children: Vec::new(),
54            flash_intensity: 0.0,
55            previous_value: None,
56        }
57    }
58
59    /// Create a branch node.
60    #[must_use]
61    pub fn branch(label: &str, children: Vec<Self>) -> Self {
62        let value = children.iter().map(Self::total_value).sum();
63        Self {
64            label: label.to_string(),
65            value,
66            color: None,
67            children,
68            flash_intensity: 0.0,
69            previous_value: None,
70        }
71    }
72
73    /// Update value and detect change for flash effect (GAP-TREE-001).
74    ///
75    /// Returns true if the value changed significantly.
76    pub fn update_value(&mut self, new_value: f64) -> bool {
77        let threshold = 0.01; // 1% change threshold
78        let changed = self.previous_value.map_or(true, |prev| {
79            let delta = (new_value - prev).abs();
80            let relative = delta / prev.max(1.0);
81            relative > threshold
82        });
83
84        if changed {
85            self.flash_intensity = 1.0; // Full flash on change
86        }
87
88        self.previous_value = Some(self.value);
89        self.value = new_value;
90        changed
91    }
92
93    /// Decay flash intensity over time.
94    ///
95    /// Call this each frame to animate the flash effect.
96    pub fn decay_flash(&mut self, decay_rate: f32) {
97        self.flash_intensity = (self.flash_intensity - decay_rate).max(0.0);
98        for child in &mut self.children {
99            child.decay_flash(decay_rate);
100        }
101    }
102
103    /// Check if this node is currently flashing.
104    #[must_use]
105    pub fn is_flashing(&self) -> bool {
106        self.flash_intensity > 0.01
107    }
108
109    /// Get flash color (white overlay with intensity).
110    #[must_use]
111    pub fn flash_color(&self) -> Color {
112        Color::new(1.0, 1.0, 1.0, self.flash_intensity * 0.5)
113    }
114
115    /// Get total value including children.
116    #[must_use]
117    pub fn total_value(&self) -> f64 {
118        if self.children.is_empty() {
119            self.value
120        } else {
121            self.children.iter().map(Self::total_value).sum()
122        }
123    }
124
125    /// Check if this is a leaf node.
126    #[must_use]
127    pub fn is_leaf(&self) -> bool {
128        self.children.is_empty()
129    }
130}
131
132/// Layout algorithm for treemap.
133#[derive(Debug, Clone, Copy, Default)]
134pub enum TreemapLayout {
135    /// Squarify algorithm (default, best aspect ratios).
136    #[default]
137    Squarify,
138    /// Slice and dice (alternating horizontal/vertical).
139    SliceAndDice,
140    /// Binary tree layout.
141    Binary,
142}
143
144/// Computed rectangle for a node.
145#[derive(Debug, Clone)]
146struct ComputedRect {
147    rect: Rect,
148    node_idx: usize,
149    depth: usize,
150}
151
152/// Treemap widget.
153#[derive(Debug, Clone)]
154pub struct Treemap {
155    root: Option<TreemapNode>,
156    layout: TreemapLayout,
157    gradient: Gradient,
158    show_labels: bool,
159    max_depth: usize,
160    border_width: f32,
161    bounds: Rect,
162    // Cached layout
163    computed_rects: Vec<ComputedRect>,
164    flat_nodes: Vec<(TreemapNode, usize)>, // (node, depth)
165}
166
167impl Treemap {
168    /// Create a new treemap widget.
169    #[must_use]
170    pub fn new() -> Self {
171        Self {
172            root: None,
173            layout: TreemapLayout::default(),
174            gradient: Gradient::three(
175                Color::new(0.2, 0.4, 0.8, 1.0), // Blue at 0.0
176                Color::new(0.4, 0.8, 0.4, 1.0), // Green at 0.5
177                Color::new(0.8, 0.4, 0.2, 1.0), // Orange at 1.0
178            ),
179            show_labels: true,
180            max_depth: 3,
181            border_width: 0.0,
182            bounds: Rect::default(),
183            computed_rects: Vec::new(),
184            flat_nodes: Vec::new(),
185        }
186    }
187
188    /// Set the root node.
189    #[must_use]
190    pub fn with_root(mut self, root: TreemapNode) -> Self {
191        self.root = Some(root);
192        self.invalidate_layout();
193        self
194    }
195
196    /// Set layout algorithm.
197    #[must_use]
198    pub fn with_layout(mut self, layout: TreemapLayout) -> Self {
199        self.layout = layout;
200        self.invalidate_layout();
201        self
202    }
203
204    /// Set color gradient.
205    #[must_use]
206    pub fn with_gradient(mut self, gradient: Gradient) -> Self {
207        self.gradient = gradient;
208        self
209    }
210
211    /// Toggle labels.
212    #[must_use]
213    pub fn with_labels(mut self, show: bool) -> Self {
214        self.show_labels = show;
215        self
216    }
217
218    /// Set maximum depth to render.
219    #[must_use]
220    pub fn with_max_depth(mut self, depth: usize) -> Self {
221        self.max_depth = depth;
222        self.invalidate_layout();
223        self
224    }
225
226    /// Invalidate cached layout.
227    fn invalidate_layout(&mut self) {
228        self.computed_rects.clear();
229        self.flat_nodes.clear();
230    }
231
232    /// Flatten nodes with depth tracking.
233    fn flatten_nodes_static(node: &TreemapNode, depth: usize, out: &mut Vec<(TreemapNode, usize)>) {
234        out.push((node.clone(), depth));
235        // Use a reasonable max depth (3) to avoid infinite recursion
236        if depth < 3 {
237            for child in &node.children {
238                Self::flatten_nodes_static(child, depth + 1, out);
239            }
240        }
241    }
242
243    /// Compute layout using squarify algorithm.
244    fn compute_layout(&mut self) {
245        self.computed_rects.clear();
246        self.flat_nodes.clear();
247
248        let Some(root) = self.root.clone() else {
249            return;
250        };
251
252        // Flatten tree for rendering
253        let mut flat_nodes = Vec::new();
254        Self::flatten_nodes_static(&root, 0, &mut flat_nodes);
255        self.flat_nodes = flat_nodes;
256
257        // Compute rectangles
258        let bounds = self.bounds;
259        self.squarify_layout(&root, bounds, 0, &mut 0);
260    }
261
262    /// Squarify algorithm implementation.
263    fn squarify_layout(
264        &mut self,
265        node: &TreemapNode,
266        rect: Rect,
267        depth: usize,
268        node_idx: &mut usize,
269    ) {
270        let current_idx = *node_idx;
271        *node_idx += 1;
272
273        // Store this node's rect
274        self.computed_rects.push(ComputedRect {
275            rect,
276            node_idx: current_idx,
277            depth,
278        });
279
280        if node.children.is_empty()
281            || depth >= self.max_depth
282            || rect.width < 2.0
283            || rect.height < 2.0
284        {
285            return;
286        }
287
288        // Get sorted children by value (descending)
289        let mut children: Vec<_> = node.children.iter().collect();
290        children.sort_by(|a, b| {
291            b.total_value()
292                .partial_cmp(&a.total_value())
293                .unwrap_or(std::cmp::Ordering::Equal)
294        });
295
296        let total_value: f64 = children.iter().map(|c| c.total_value()).sum();
297        if total_value <= 0.0 {
298            return;
299        }
300
301        // Squarify layout
302        let mut remaining_rect = Rect::new(
303            rect.x + self.border_width,
304            rect.y + self.border_width,
305            (rect.width - 2.0 * self.border_width).max(0.0),
306            (rect.height - 2.0 * self.border_width).max(0.0),
307        );
308
309        let mut row: Vec<(usize, f64)> = Vec::new();
310        let mut row_sum = 0.0;
311
312        for (i, child) in children.iter().enumerate() {
313            let child_value = child.total_value();
314            if child_value <= 0.0 {
315                continue;
316            }
317
318            // Try adding to current row
319            row.push((i, child_value));
320            row_sum += child_value;
321
322            // Check if we should finalize this row
323            let worst_current = self.worst_ratio(&row, row_sum, remaining_rect, total_value);
324
325            if i + 1 < children.len() {
326                let next_value = children[i + 1].total_value();
327                let mut test_row = row.clone();
328                test_row.push((i + 1, next_value));
329                let worst_with_next =
330                    self.worst_ratio(&test_row, row_sum + next_value, remaining_rect, total_value);
331
332                if worst_with_next > worst_current {
333                    // Finalize current row
334                    remaining_rect = self.layout_row(
335                        &children,
336                        &row,
337                        row_sum,
338                        remaining_rect,
339                        total_value,
340                        depth,
341                        node_idx,
342                    );
343                    row.clear();
344                    row_sum = 0.0;
345                }
346            }
347        }
348
349        // Layout final row
350        if !row.is_empty() {
351            self.layout_row(
352                &children,
353                &row,
354                row_sum,
355                remaining_rect,
356                total_value,
357                depth,
358                node_idx,
359            );
360        }
361    }
362
363    /// Calculate worst aspect ratio in a row.
364    fn worst_ratio(&self, row: &[(usize, f64)], row_sum: f64, rect: Rect, total: f64) -> f64 {
365        if row.is_empty() || row_sum <= 0.0 || total <= 0.0 {
366            return f64::INFINITY;
367        }
368
369        let area = rect.width as f64 * rect.height as f64;
370        let row_area = area * (row_sum / total);
371
372        let is_horizontal = rect.width >= rect.height;
373        let side = if is_horizontal {
374            rect.height
375        } else {
376            rect.width
377        } as f64;
378
379        if side <= 0.0 {
380            return f64::INFINITY;
381        }
382
383        let side_sq = side * side;
384        let row_sum_sq = row_sum * row_sum;
385
386        row.iter()
387            .map(|(_, v)| {
388                let ratio = (row_area * v) / (side_sq * row_sum_sq / row_area);
389                ratio.max(1.0 / ratio)
390            })
391            .fold(0.0f64, f64::max)
392    }
393
394    /// Layout a row of nodes and return remaining rect.
395    #[allow(clippy::too_many_arguments)]
396    fn layout_row(
397        &mut self,
398        children: &[&TreemapNode],
399        row: &[(usize, f64)],
400        row_sum: f64,
401        rect: Rect,
402        total: f64,
403        depth: usize,
404        node_idx: &mut usize,
405    ) -> Rect {
406        if row.is_empty() || row_sum <= 0.0 || total <= 0.0 {
407            return rect;
408        }
409
410        let is_horizontal = rect.width >= rect.height;
411        let row_fraction = row_sum / total;
412
413        let (row_rect, remaining) = if is_horizontal {
414            let row_height = rect.height * row_fraction as f32;
415            (
416                Rect::new(rect.x, rect.y, rect.width, row_height),
417                Rect::new(
418                    rect.x,
419                    rect.y + row_height,
420                    rect.width,
421                    rect.height - row_height,
422                ),
423            )
424        } else {
425            let row_width = rect.width * row_fraction as f32;
426            (
427                Rect::new(rect.x, rect.y, row_width, rect.height),
428                Rect::new(
429                    rect.x + row_width,
430                    rect.y,
431                    rect.width - row_width,
432                    rect.height,
433                ),
434            )
435        };
436
437        // Layout each child in the row
438        let mut offset = 0.0f32;
439        for &(child_idx, child_value) in row {
440            let child_fraction = child_value / row_sum;
441
442            let child_rect = if is_horizontal {
443                let w = row_rect.width * child_fraction as f32;
444                let r = Rect::new(row_rect.x + offset, row_rect.y, w, row_rect.height);
445                offset += w;
446                r
447            } else {
448                let h = row_rect.height * child_fraction as f32;
449                let r = Rect::new(row_rect.x, row_rect.y + offset, row_rect.width, h);
450                offset += h;
451                r
452            };
453
454            self.squarify_layout(children[child_idx], child_rect, depth + 1, node_idx);
455        }
456
457        remaining
458    }
459}
460
461impl Default for Treemap {
462    fn default() -> Self {
463        Self::new()
464    }
465}
466
467impl Widget for Treemap {
468    fn type_id(&self) -> TypeId {
469        TypeId::of::<Self>()
470    }
471
472    fn measure(&self, constraints: Constraints) -> Size {
473        Size::new(
474            constraints.max_width.min(80.0),
475            constraints.max_height.min(40.0),
476        )
477    }
478
479    fn layout(&mut self, bounds: Rect) -> LayoutResult {
480        self.bounds = bounds;
481        self.compute_layout();
482        LayoutResult {
483            size: Size::new(bounds.width, bounds.height),
484        }
485    }
486
487    fn paint(&self, canvas: &mut dyn Canvas) {
488        if self.bounds.width < 4.0 || self.bounds.height < 2.0 {
489            return;
490        }
491
492        #[allow(clippy::redundant_closure_for_method_calls)]
493        let total_value = self.root.as_ref().map_or(1.0, |r| r.total_value());
494
495        // Draw rectangles from deepest to shallowest (painter's algorithm)
496        let mut sorted_rects: Vec<_> = self.computed_rects.iter().collect();
497        sorted_rects.sort_by(|a, b| b.depth.cmp(&a.depth));
498
499        for computed in sorted_rects {
500            if computed.rect.width < 1.0 || computed.rect.height < 1.0 {
501                continue;
502            }
503
504            let node = if computed.node_idx < self.flat_nodes.len() {
505                &self.flat_nodes[computed.node_idx].0
506            } else {
507                continue;
508            };
509
510            // Determine color
511            let color = node.color.unwrap_or_else(|| {
512                let t = (node.total_value() / total_value).clamp(0.0, 1.0);
513                self.gradient.sample(t)
514            });
515
516            // Fill rectangle
517            let fill_char = if computed.depth == 0 { ' ' } else { '░' };
518            let style = TextStyle {
519                color,
520                ..Default::default()
521            };
522
523            for y in 0..(computed.rect.height as usize).max(1) {
524                let row: String = (0..(computed.rect.width as usize).max(1))
525                    .map(|_| fill_char)
526                    .collect();
527                canvas.draw_text(
528                    &row,
529                    Point::new(computed.rect.x, computed.rect.y + y as f32),
530                    &style,
531                );
532            }
533
534            // Draw label if enabled and there's space
535            if self.show_labels && computed.rect.width >= 3.0 && computed.rect.height >= 1.0 {
536                let label = if node.label.len() > computed.rect.width as usize - 1 {
537                    format!("{}…", &node.label[..computed.rect.width as usize - 2])
538                } else {
539                    node.label.clone()
540                };
541
542                let label_style = TextStyle {
543                    color: Color::new(1.0, 1.0, 1.0, 1.0),
544                    ..Default::default()
545                };
546
547                canvas.draw_text(
548                    &label,
549                    Point::new(computed.rect.x + 1.0, computed.rect.y),
550                    &label_style,
551                );
552            }
553        }
554    }
555
556    fn event(&mut self, _event: &Event) -> Option<Box<dyn Any + Send>> {
557        None
558    }
559
560    fn children(&self) -> &[Box<dyn Widget>] {
561        &[]
562    }
563
564    fn children_mut(&mut self) -> &mut [Box<dyn Widget>] {
565        &mut []
566    }
567}
568
569impl Brick for Treemap {
570    fn brick_name(&self) -> &'static str {
571        "Treemap"
572    }
573
574    fn assertions(&self) -> &[BrickAssertion] {
575        static ASSERTIONS: &[BrickAssertion] = &[BrickAssertion::max_latency_ms(16)];
576        ASSERTIONS
577    }
578
579    fn budget(&self) -> BrickBudget {
580        BrickBudget::uniform(16)
581    }
582
583    fn verify(&self) -> BrickVerification {
584        let mut passed = Vec::new();
585        let mut failed = Vec::new();
586
587        if self.bounds.width >= 4.0 && self.bounds.height >= 2.0 {
588            passed.push(BrickAssertion::max_latency_ms(16));
589        } else {
590            failed.push((
591                BrickAssertion::max_latency_ms(16),
592                "Size too small".to_string(),
593            ));
594        }
595
596        BrickVerification {
597            passed,
598            failed,
599            verification_time: Duration::from_micros(10),
600        }
601    }
602
603    fn to_html(&self) -> String {
604        String::new()
605    }
606
607    fn to_css(&self) -> String {
608        String::new()
609    }
610}
611
612#[cfg(test)]
613mod tests {
614    use super::*;
615    use crate::{CellBuffer, DirectTerminalCanvas};
616
617    #[test]
618    fn test_treemap_creation() {
619        let treemap = Treemap::new();
620        assert!(treemap.root.is_none());
621    }
622
623    #[test]
624    fn test_leaf_node() {
625        let node = TreemapNode::leaf("test", 100.0);
626        assert_eq!(node.label, "test");
627        assert_eq!(node.value, 100.0);
628        assert!(node.is_leaf());
629    }
630
631    #[test]
632    fn test_leaf_node_colored() {
633        let color = Color::new(0.5, 0.6, 0.7, 1.0);
634        let node = TreemapNode::leaf_colored("colored", 50.0, color);
635        assert_eq!(node.label, "colored");
636        assert_eq!(node.value, 50.0);
637        assert!(node.color.is_some());
638        assert!(node.is_leaf());
639    }
640
641    #[test]
642    fn test_branch_node() {
643        let branch = TreemapNode::branch(
644            "parent",
645            vec![
646                TreemapNode::leaf("child1", 50.0),
647                TreemapNode::leaf("child2", 30.0),
648            ],
649        );
650        assert!(!branch.is_leaf());
651        assert_eq!(branch.total_value(), 80.0);
652    }
653
654    #[test]
655    fn test_nested_branch_total_value() {
656        let root = TreemapNode::branch(
657            "root",
658            vec![
659                TreemapNode::branch(
660                    "sub1",
661                    vec![TreemapNode::leaf("a", 10.0), TreemapNode::leaf("b", 20.0)],
662                ),
663                TreemapNode::leaf("c", 30.0),
664            ],
665        );
666        assert_eq!(root.total_value(), 60.0);
667    }
668
669    #[test]
670    fn test_treemap_with_root() {
671        let root = TreemapNode::branch(
672            "root",
673            vec![TreemapNode::leaf("a", 100.0), TreemapNode::leaf("b", 50.0)],
674        );
675        let treemap = Treemap::new().with_root(root);
676        assert!(treemap.root.is_some());
677    }
678
679    #[test]
680    fn test_treemap_with_layout() {
681        let treemap = Treemap::new().with_layout(TreemapLayout::SliceAndDice);
682        assert!(matches!(treemap.layout, TreemapLayout::SliceAndDice));
683
684        let treemap2 = Treemap::new().with_layout(TreemapLayout::Binary);
685        assert!(matches!(treemap2.layout, TreemapLayout::Binary));
686    }
687
688    #[test]
689    fn test_treemap_with_gradient() {
690        let gradient = Gradient::two(
691            Color::new(1.0, 0.0, 0.0, 1.0),
692            Color::new(0.0, 0.0, 1.0, 1.0),
693        );
694        let treemap = Treemap::new().with_gradient(gradient);
695        // Verify gradient is set (sample a point)
696        let sample = treemap.gradient.sample(0.5);
697        assert!(sample.r > 0.0);
698    }
699
700    #[test]
701    fn test_treemap_with_labels() {
702        let treemap = Treemap::new().with_labels(false);
703        assert!(!treemap.show_labels);
704
705        let treemap2 = Treemap::new().with_labels(true);
706        assert!(treemap2.show_labels);
707    }
708
709    #[test]
710    fn test_treemap_with_max_depth() {
711        let treemap = Treemap::new().with_max_depth(5);
712        assert_eq!(treemap.max_depth, 5);
713    }
714
715    #[test]
716    fn test_treemap_measure() {
717        let treemap = Treemap::new();
718        let constraints = Constraints::new(0.0, 100.0, 0.0, 50.0);
719        let size = treemap.measure(constraints);
720        assert_eq!(size.width, 80.0); // min(100, 80)
721        assert_eq!(size.height, 40.0); // min(50, 40)
722    }
723
724    #[test]
725    fn test_treemap_measure_small_constraints() {
726        let treemap = Treemap::new();
727        let constraints = Constraints::new(0.0, 30.0, 0.0, 20.0);
728        let size = treemap.measure(constraints);
729        assert_eq!(size.width, 30.0);
730        assert_eq!(size.height, 20.0);
731    }
732
733    #[test]
734    fn test_treemap_layout_and_paint() {
735        let root = TreemapNode::branch(
736            "root",
737            vec![
738                TreemapNode::leaf_colored("big", 100.0, Color::new(0.8, 0.2, 0.2, 1.0)),
739                TreemapNode::leaf_colored("small", 50.0, Color::new(0.2, 0.8, 0.2, 1.0)),
740            ],
741        );
742        let mut treemap = Treemap::new().with_root(root);
743
744        let mut buffer = CellBuffer::new(40, 20);
745        let mut canvas = DirectTerminalCanvas::new(&mut buffer);
746
747        let result = treemap.layout(Rect::new(0.0, 0.0, 40.0, 20.0));
748        assert_eq!(result.size.width, 40.0);
749        assert_eq!(result.size.height, 20.0);
750
751        treemap.paint(&mut canvas);
752
753        // Verify something was rendered
754        let cells = buffer.cells();
755        let non_empty = cells
756            .iter()
757            .filter(|c| !c.symbol.is_empty() && c.symbol != " ")
758            .count();
759        assert!(non_empty > 0, "Treemap should render some content");
760    }
761
762    #[test]
763    fn test_treemap_paint_too_small() {
764        let root = TreemapNode::leaf("tiny", 10.0);
765        let mut treemap = Treemap::new().with_root(root);
766
767        let mut buffer = CellBuffer::new(2, 1);
768        let mut canvas = DirectTerminalCanvas::new(&mut buffer);
769
770        treemap.layout(Rect::new(0.0, 0.0, 2.0, 1.0));
771        treemap.paint(&mut canvas);
772        // Should not crash with tiny bounds
773    }
774
775    #[test]
776    fn test_treemap_paint_no_root() {
777        let mut treemap = Treemap::new();
778
779        let mut buffer = CellBuffer::new(40, 20);
780        let mut canvas = DirectTerminalCanvas::new(&mut buffer);
781
782        treemap.layout(Rect::new(0.0, 0.0, 40.0, 20.0));
783        treemap.paint(&mut canvas);
784        // Should not crash with no root
785    }
786
787    #[test]
788    fn test_treemap_deep_hierarchy() {
789        let root = TreemapNode::branch(
790            "level0",
791            vec![TreemapNode::branch(
792                "level1",
793                vec![TreemapNode::branch(
794                    "level2",
795                    vec![TreemapNode::branch(
796                        "level3",
797                        vec![TreemapNode::leaf("deep", 100.0)],
798                    )],
799                )],
800            )],
801        );
802        let mut treemap = Treemap::new().with_root(root).with_max_depth(4);
803
804        let mut buffer = CellBuffer::new(60, 30);
805        let mut canvas = DirectTerminalCanvas::new(&mut buffer);
806
807        treemap.layout(Rect::new(0.0, 0.0, 60.0, 30.0));
808        treemap.paint(&mut canvas);
809
810        // Should render without crashing
811        let cells = buffer.cells();
812        assert!(!cells.is_empty());
813    }
814
815    #[test]
816    fn test_treemap_many_children() {
817        let children: Vec<TreemapNode> = (0..10)
818            .map(|i| TreemapNode::leaf(&format!("node{i}"), (i + 1) as f64 * 10.0))
819            .collect();
820        let root = TreemapNode::branch("root", children);
821
822        let mut treemap = Treemap::new().with_root(root);
823
824        let mut buffer = CellBuffer::new(80, 40);
825        let mut canvas = DirectTerminalCanvas::new(&mut buffer);
826
827        treemap.layout(Rect::new(0.0, 0.0, 80.0, 40.0));
828        treemap.paint(&mut canvas);
829    }
830
831    #[test]
832    fn test_treemap_zero_value_children() {
833        let root = TreemapNode::branch(
834            "root",
835            vec![
836                TreemapNode::leaf("valid", 100.0),
837                TreemapNode::leaf("zero", 0.0),
838            ],
839        );
840        let mut treemap = Treemap::new().with_root(root);
841
842        let mut buffer = CellBuffer::new(40, 20);
843        let mut canvas = DirectTerminalCanvas::new(&mut buffer);
844
845        treemap.layout(Rect::new(0.0, 0.0, 40.0, 20.0));
846        treemap.paint(&mut canvas);
847        // Should handle zero-value nodes gracefully
848    }
849
850    #[test]
851    fn test_treemap_long_labels() {
852        let root = TreemapNode::branch(
853            "root",
854            vec![TreemapNode::leaf(
855                "this_is_a_very_long_label_that_should_be_truncated",
856                100.0,
857            )],
858        );
859        let mut treemap = Treemap::new().with_root(root).with_labels(true);
860
861        let mut buffer = CellBuffer::new(20, 10);
862        let mut canvas = DirectTerminalCanvas::new(&mut buffer);
863
864        treemap.layout(Rect::new(0.0, 0.0, 20.0, 10.0));
865        treemap.paint(&mut canvas);
866    }
867
868    #[test]
869    fn test_treemap_labels_disabled() {
870        let root = TreemapNode::leaf("test", 100.0);
871        let mut treemap = Treemap::new().with_root(root).with_labels(false);
872
873        let mut buffer = CellBuffer::new(40, 20);
874        let mut canvas = DirectTerminalCanvas::new(&mut buffer);
875
876        treemap.layout(Rect::new(0.0, 0.0, 40.0, 20.0));
877        treemap.paint(&mut canvas);
878    }
879
880    #[test]
881    fn test_treemap_assertions() {
882        let treemap = Treemap::default();
883        assert!(!treemap.assertions().is_empty());
884    }
885
886    #[test]
887    fn test_treemap_verify_valid() {
888        let mut treemap = Treemap::default();
889        treemap.bounds = Rect::new(0.0, 0.0, 80.0, 40.0);
890        assert!(treemap.verify().is_valid());
891    }
892
893    #[test]
894    fn test_treemap_verify_invalid_small() {
895        let mut treemap = Treemap::default();
896        treemap.bounds = Rect::new(0.0, 0.0, 2.0, 1.0);
897        assert!(!treemap.verify().is_valid());
898    }
899
900    #[test]
901    fn test_treemap_children() {
902        let treemap = Treemap::default();
903        assert!(treemap.children().is_empty());
904    }
905
906    #[test]
907    fn test_treemap_children_mut() {
908        let mut treemap = Treemap::default();
909        assert!(treemap.children_mut().is_empty());
910    }
911
912    #[test]
913    fn test_treemap_brick_name() {
914        let treemap = Treemap::new();
915        assert_eq!(treemap.brick_name(), "Treemap");
916    }
917
918    #[test]
919    fn test_treemap_budget() {
920        let treemap = Treemap::new();
921        let budget = treemap.budget();
922        assert!(budget.layout_ms > 0);
923        assert!(budget.paint_ms > 0);
924    }
925
926    #[test]
927    fn test_treemap_to_html() {
928        let treemap = Treemap::new();
929        assert!(treemap.to_html().is_empty());
930    }
931
932    #[test]
933    fn test_treemap_to_css() {
934        let treemap = Treemap::new();
935        assert!(treemap.to_css().is_empty());
936    }
937
938    #[test]
939    fn test_treemap_type_id() {
940        let treemap = Treemap::new();
941        let type_id = Widget::type_id(&treemap);
942        assert_eq!(type_id, TypeId::of::<Treemap>());
943    }
944
945    #[test]
946    fn test_treemap_event() {
947        let mut treemap = Treemap::new();
948        let event = Event::Resize {
949            width: 80.0,
950            height: 24.0,
951        };
952        assert!(treemap.event(&event).is_none());
953    }
954
955    #[test]
956    fn test_treemap_vertical_layout() {
957        // Create a tall, narrow treemap to test vertical layout path
958        let root = TreemapNode::branch(
959            "root",
960            vec![TreemapNode::leaf("a", 100.0), TreemapNode::leaf("b", 100.0)],
961        );
962        let mut treemap = Treemap::new().with_root(root);
963
964        let mut buffer = CellBuffer::new(10, 40); // Tall and narrow
965        let mut canvas = DirectTerminalCanvas::new(&mut buffer);
966
967        treemap.layout(Rect::new(0.0, 0.0, 10.0, 40.0));
968        treemap.paint(&mut canvas);
969    }
970
971    #[test]
972    fn test_treemap_layout_default() {
973        let layout = TreemapLayout::default();
974        assert!(matches!(layout, TreemapLayout::Squarify));
975    }
976
977    // =========================================================================
978    // GAP-TREE-001: Flash effect tests
979    // =========================================================================
980
981    #[test]
982    fn test_treemap_node_flash_intensity_default() {
983        let node = TreemapNode::leaf("test", 100.0);
984        assert_eq!(node.flash_intensity, 0.0);
985        assert!(node.previous_value.is_none());
986    }
987
988    #[test]
989    fn test_treemap_node_flash_intensity_colored() {
990        let node = TreemapNode::leaf_colored("test", 100.0, Color::new(1.0, 0.0, 0.0, 1.0));
991        assert_eq!(node.flash_intensity, 0.0);
992        assert!(node.previous_value.is_none());
993    }
994
995    #[test]
996    fn test_treemap_node_flash_intensity_branch() {
997        let node = TreemapNode::branch("root", vec![TreemapNode::leaf("child", 50.0)]);
998        assert_eq!(node.flash_intensity, 0.0);
999        assert!(node.previous_value.is_none());
1000    }
1001
1002    #[test]
1003    fn test_treemap_node_update_value_first_call() {
1004        let mut node = TreemapNode::leaf("test", 100.0);
1005        let changed = node.update_value(150.0);
1006        assert!(changed, "First update should always report change");
1007        assert_eq!(node.flash_intensity, 1.0);
1008        assert_eq!(node.value, 150.0);
1009        assert_eq!(node.previous_value, Some(100.0));
1010    }
1011
1012    #[test]
1013    fn test_treemap_node_update_value_significant_change() {
1014        let mut node = TreemapNode::leaf("test", 100.0);
1015        node.previous_value = Some(100.0);
1016        node.flash_intensity = 0.0;
1017
1018        let changed = node.update_value(150.0); // 50% change
1019        assert!(changed, "50% change should trigger flash");
1020        assert_eq!(node.flash_intensity, 1.0);
1021    }
1022
1023    #[test]
1024    fn test_treemap_node_update_value_small_change() {
1025        let mut node = TreemapNode::leaf("test", 100.0);
1026        node.previous_value = Some(100.0);
1027        node.flash_intensity = 0.0;
1028
1029        let changed = node.update_value(100.5); // 0.5% change
1030        assert!(!changed, "0.5% change should not trigger flash");
1031        assert_eq!(node.flash_intensity, 0.0);
1032    }
1033
1034    #[test]
1035    fn test_treemap_node_decay_flash() {
1036        let mut node = TreemapNode::leaf("test", 100.0);
1037        node.flash_intensity = 1.0;
1038
1039        node.decay_flash(0.1);
1040        assert!((node.flash_intensity - 0.9).abs() < 0.001);
1041
1042        node.decay_flash(0.5);
1043        assert!((node.flash_intensity - 0.4).abs() < 0.001);
1044    }
1045
1046    #[test]
1047    fn test_treemap_node_decay_flash_clamps_zero() {
1048        let mut node = TreemapNode::leaf("test", 100.0);
1049        node.flash_intensity = 0.1;
1050
1051        node.decay_flash(0.5); // Would go negative
1052        assert_eq!(node.flash_intensity, 0.0);
1053    }
1054
1055    #[test]
1056    fn test_treemap_node_decay_flash_recursive() {
1057        let mut root = TreemapNode::branch(
1058            "root",
1059            vec![TreemapNode::leaf("a", 50.0), TreemapNode::leaf("b", 50.0)],
1060        );
1061        root.flash_intensity = 1.0;
1062        root.children[0].flash_intensity = 0.8;
1063        root.children[1].flash_intensity = 0.5;
1064
1065        root.decay_flash(0.2);
1066
1067        assert!((root.flash_intensity - 0.8).abs() < 0.001);
1068        assert!((root.children[0].flash_intensity - 0.6).abs() < 0.001);
1069        assert!((root.children[1].flash_intensity - 0.3).abs() < 0.001);
1070    }
1071
1072    #[test]
1073    fn test_treemap_node_is_flashing() {
1074        let mut node = TreemapNode::leaf("test", 100.0);
1075        assert!(!node.is_flashing());
1076
1077        node.flash_intensity = 1.0;
1078        assert!(node.is_flashing());
1079
1080        node.flash_intensity = 0.02;
1081        assert!(node.is_flashing());
1082
1083        node.flash_intensity = 0.005; // Below threshold
1084        assert!(!node.is_flashing());
1085    }
1086
1087    #[test]
1088    fn test_treemap_node_flash_color() {
1089        let mut node = TreemapNode::leaf("test", 100.0);
1090        node.flash_intensity = 1.0;
1091
1092        let color = node.flash_color();
1093        assert_eq!(color.r, 1.0);
1094        assert_eq!(color.g, 1.0);
1095        assert_eq!(color.b, 1.0);
1096        assert!((color.a - 0.5).abs() < 0.001); // 1.0 * 0.5
1097
1098        node.flash_intensity = 0.5;
1099        let color2 = node.flash_color();
1100        assert!((color2.a - 0.25).abs() < 0.001); // 0.5 * 0.5
1101    }
1102
1103    #[test]
1104    fn test_treemap_node_flash_color_zero_intensity() {
1105        let node = TreemapNode::leaf("test", 100.0);
1106        let color = node.flash_color();
1107        assert_eq!(color.a, 0.0);
1108    }
1109}