Skip to main content

dumap_core/tree/
node.rs

1use crate::category::FileCategory;
2use serde::Serialize;
3use std::collections::HashMap;
4use std::path::Path;
5
6/// Intermediate tree node for building the hierarchy from filesystem paths.
7///
8/// Each node represents either a file (leaf with `file_size > 0` and no children)
9/// or a directory (with children). Sizes aggregate upward via `total_size()`.
10#[derive(Debug)]
11pub struct DirNode {
12    pub children: HashMap<String, DirNode>,
13    /// Total size of files directly in this node (leaf files)
14    pub file_size: u64,
15    /// Number of files directly in this node
16    pub file_count: usize,
17}
18
19/// ECharts itemStyle for setting per-node colors.
20#[derive(Debug, Serialize)]
21pub struct EChartsItemStyle {
22    pub color: FileCategory,
23}
24
25/// JSON-serializable tree node for ECharts treemap visualization.
26#[derive(Debug, Serialize)]
27pub struct EChartsNode {
28    pub name: String,
29    #[serde(skip_serializing_if = "Option::is_none")]
30    pub value: Option<u64>,
31    #[serde(skip_serializing_if = "Vec::is_empty")]
32    pub children: Vec<EChartsNode>,
33    #[serde(rename = "itemStyle", skip_serializing_if = "Option::is_none")]
34    pub item_style: Option<EChartsItemStyle>,
35}
36
37impl DirNode {
38    pub fn new() -> Self {
39        Self {
40            children: HashMap::new(),
41            file_size: 0,
42            file_count: 0,
43        }
44    }
45
46    /// Insert a file path into the tree, splitting on path separators.
47    pub fn insert(&mut self, path_components: &[&str], size: u64) {
48        if path_components.is_empty() {
49            return;
50        }
51
52        if path_components.len() == 1 {
53            // Leaf file — store as a child with no further children
54            let leaf = self
55                .children
56                .entry(path_components[0].to_string())
57                .or_default();
58            leaf.file_size += size;
59            leaf.file_count += 1;
60        } else {
61            // Intermediate directory
62            let child = self
63                .children
64                .entry(path_components[0].to_string())
65                .or_default();
66            child.insert(&path_components[1..], size);
67        }
68    }
69
70    /// Compute total size of this subtree.
71    pub fn total_size(&self) -> u64 {
72        let children_size: u64 = self.children.values().map(|c| c.total_size()).sum();
73        self.file_size + children_size
74    }
75
76    /// Total number of files in this subtree.
77    pub fn total_file_count(&self) -> usize {
78        let children_count: usize = self.children.values().map(|c| c.total_file_count()).sum();
79        self.file_count + children_count
80    }
81
82    /// Convert to ECharts JSON tree, collapsing single-child directories.
83    ///
84    /// Single-child directory chains are collapsed (e.g., `a/b/c` becomes
85    /// a single node named `"a/b/c"`) to reduce visual clutter.
86    pub fn to_echarts(&self, name: &str) -> EChartsNode {
87        if self.children.is_empty() {
88            // Leaf — color by file category
89            let category = FileCategory::from_path(Path::new(name));
90            return EChartsNode {
91                name: name.to_string(),
92                value: Some(self.file_size),
93                children: Vec::new(),
94                item_style: Some(EChartsItemStyle { color: category }),
95            };
96        }
97
98        let mut children: Vec<EChartsNode> = self
99            .children
100            .iter()
101            .map(|(child_name, child_node)| child_node.to_echarts(child_name))
102            .collect();
103
104        // Sort children by value descending (largest first) for better treemap layout
105        children.sort_by(|a, b| {
106            let size_a = echarts_subtree_size(a);
107            let size_b = echarts_subtree_size(b);
108            size_b.cmp(&size_a)
109        });
110
111        // Collapse single-child directory chains: /a/b/c -> "a/b/c"
112        if children.len() == 1 && !children[0].children.is_empty() {
113            let child = children.remove(0);
114            let collapsed_name = format!("{}/{}", name, child.name);
115            return EChartsNode {
116                name: collapsed_name,
117                value: child.value,
118                children: child.children,
119                item_style: None,
120            };
121        }
122
123        EChartsNode {
124            name: name.to_string(),
125            value: None,
126            children,
127            item_style: None,
128        }
129    }
130}
131
132impl Default for DirNode {
133    fn default() -> Self {
134        Self::new()
135    }
136}
137
138/// Compute the total size of an EChartsNode subtree for sorting.
139fn echarts_subtree_size(node: &EChartsNode) -> u64 {
140    if let Some(val) = node.value {
141        val
142    } else {
143        node.children.iter().map(echarts_subtree_size).sum()
144    }
145}
146
147/// Split a file path into components, handling both Unix and Windows separators.
148pub fn split_path(path: &str) -> Vec<&str> {
149    path.split(['/', '\\']).filter(|s| !s.is_empty()).collect()
150}
151
152/// Build a tree from a list of (path, size) pairs.
153pub fn build_tree(files: &[(&str, u64)]) -> DirNode {
154    let mut root = DirNode::new();
155    for (path, size) in files {
156        let components = split_path(path);
157        if !components.is_empty() {
158            root.insert(&components, *size);
159        }
160    }
161    root
162}