Skip to main content

dumap_core/
html.rs

1use crate::category::FileCategory;
2use crate::scan::format_size;
3
4/// Build the legend HTML from `FileCategory::ALL` so it stays in sync
5/// with category colors and labels automatically.
6fn build_legend_html() -> String {
7    FileCategory::ALL
8        .iter()
9        .map(|cat| {
10            format!(
11                r#"<span class="item"><span class="swatch" style="background:{}"></span>{}</span>"#,
12                cat.color(),
13                cat.label()
14            )
15        })
16        .collect::<Vec<_>>()
17        .join("\n      ")
18}
19
20/// Generate an interactive HTML treemap using ECharts.
21///
22/// The generated HTML is self-contained (loads ECharts from CDN) with:
23/// - Dark theme styling
24/// - Breadcrumb navigation for drill-down
25/// - `leafDepth: 3` — shows 3 levels at a time
26/// - Tooltips with full path and formatted size
27/// - Hierarchical borders (thicker at top levels)
28/// - Responsive resize
29pub fn generate_html(
30    tree_json: &str,
31    total_size: u64,
32    file_count: usize,
33    scan_path: &str,
34    leaf_depth: u16,
35) -> String {
36    let total_size_str = format_size(total_size);
37    let legend_html = build_legend_html();
38    format!(
39        r##"<!DOCTYPE html>
40<html lang="en">
41<head>
42<meta charset="utf-8">
43<title>dumap — {scan_path}</title>
44<style>
45  * {{ margin: 0; padding: 0; box-sizing: border-box; }}
46  body {{ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; background: #1a1a2e; color: #e0e0e0; }}
47  #header {{ padding: 12px 24px; background: #16213e; border-bottom: 1px solid #0f3460; display: flex; justify-content: space-between; align-items: center; flex-wrap: wrap; gap: 8px; }}
48  #header h1 {{ font-size: 18px; font-weight: 600; }}
49  #header .stats {{ font-size: 14px; color: #a0a0a0; }}
50  #legend {{ display: flex; gap: 12px; flex-wrap: wrap; align-items: center; }}
51  #legend .item {{ display: flex; align-items: center; gap: 4px; font-size: 11px; color: #b0b0b0; }}
52  #legend .swatch {{ width: 10px; height: 10px; border-radius: 2px; display: inline-block; }}
53  #chart {{ width: 100%; height: calc(100vh - 80px); }}
54</style>
55</head>
56<body>
57<div id="header">
58  <div style="display:flex;align-items:center;gap:24px;">
59    <h1>dumap — {scan_path}</h1>
60    <div id="legend">
61      {legend_html}
62    </div>
63  </div>
64  <div class="stats">{file_count} files &middot; {total_size_str} total</div>
65</div>
66<div id="chart"></div>
67<script src="https://cdn.jsdelivr.net/npm/echarts@5/dist/echarts.min.js"></script>
68<script>
69var chart = echarts.init(document.getElementById('chart'));
70var data = {tree_json};
71
72function formatBytes(b) {{
73  if (b >= 1099511627776) return (b/1099511627776).toFixed(1)+' TB';
74  if (b >= 1073741824) return (b/1073741824).toFixed(1)+' GB';
75  if (b >= 1048576) return (b/1048576).toFixed(1)+' MB';
76  if (b >= 1024) return (b/1024).toFixed(1)+' KB';
77  return b+' B';
78}}
79
80chart.setOption({{
81  tooltip: {{
82    formatter: function(info) {{
83      var val = info.value;
84      var path = info.treePathInfo.map(function(n){{ return n.name; }}).join('/');
85      return '<b>' + echarts.format.encodeHTML(path) + '</b><br/>' + formatBytes(val);
86    }}
87  }},
88  series: [{{
89    type: 'treemap',
90    data: data,
91    leafDepth: {leaf_depth},
92    roam: false,
93    breadcrumb: {{
94      top: 4,
95      left: 10,
96      itemStyle: {{ color: '#16213e', borderColor: '#0f3460' }},
97      textStyle: {{ color: '#e0e0e0', fontSize: 13 }}
98    }},
99    label: {{
100      show: true,
101      formatter: function(p) {{
102        return p.name + '\n' + formatBytes(p.value);
103      }},
104      fontSize: 12,
105      color: '#fff'
106    }},
107    upperLabel: {{
108      show: true,
109      height: 24,
110      color: '#fff',
111      fontSize: 13,
112      backgroundColor: 'transparent'
113    }},
114    itemStyle: {{
115      borderColor: '#1a1a2e',
116      borderWidth: 2,
117      gapWidth: 1
118    }},
119    levels: [
120      {{ itemStyle: {{ borderColor: '#555', borderWidth: 4, gapWidth: 4 }}, upperLabel: {{ show: false }} }},
121      {{ itemStyle: {{ borderColor: '#444', borderWidth: 2, gapWidth: 2 }}, upperLabel: {{ show: true }} }},
122      {{ itemStyle: {{ borderColor: '#333', borderWidth: 1, gapWidth: 1 }}, upperLabel: {{ show: true }} }},
123      {{ colorSaturation: [0.4, 0.8], itemStyle: {{ borderColor: '#2a2a3e', borderWidth: 1, gapWidth: 1 }} }}
124    ]
125  }}]
126}});
127
128window.addEventListener('resize', function() {{ chart.resize(); }});
129</script>
130</body>
131</html>"##
132    )
133}
134
135#[cfg(test)]
136#[path = "html_tests.rs"]
137mod html_tests;