awacke1 commited on
Commit
5d74c00
·
verified ·
1 Parent(s): 7fa4ce0

Update index.html.v2

Browse files
Files changed (1) hide show
  1. index.html.v2 +375 -900
index.html.v2 CHANGED
@@ -3,992 +3,467 @@
3
  <head>
4
  <meta charset="UTF-8">
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
- <title>Squarified Treemap Explorer</title>
7
  <style>
8
- * {
9
- margin: 0;
10
- padding: 0;
11
- box-sizing: border-box;
12
- }
13
-
 
 
 
 
 
 
 
 
 
 
 
14
  body {
15
- font-family: 'Arial', sans-serif;
16
- background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
17
- min-height: 100vh;
18
- padding: 20px;
19
- }
20
-
21
- .container {
22
- max-width: 1400px;
23
- margin: 0 auto;
24
- background: white;
25
- border-radius: 15px;
26
- box-shadow: 0 20px 40px rgba(0,0,0,0.1);
27
- overflow: hidden;
28
- }
29
-
30
- .header {
31
- background: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%);
32
- color: white;
33
- padding: 30px;
34
  text-align: center;
35
- }
36
-
37
- .header h1 {
38
- font-size: 2.5em;
39
- margin-bottom: 10px;
40
- text-shadow: 0 2px 4px rgba(0,0,0,0.3);
41
- }
42
-
43
- .header p {
44
- opacity: 0.9;
45
- font-size: 1.1em;
46
- }
47
-
48
- .controls {
49
- padding: 30px;
50
- background: #f8f9fa;
51
- border-bottom: 1px solid #e9ecef;
52
- }
53
-
54
- .file-input-wrapper {
55
  display: flex;
56
  flex-direction: column;
57
  align-items: center;
58
- gap: 15px;
 
59
  }
60
 
61
- .file-input {
62
- display: none;
 
 
 
 
 
 
63
  }
64
 
65
- .file-input-button {
66
- background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
 
 
 
67
  color: white;
68
- padding: 15px 30px;
69
- border: none;
70
- border-radius: 50px;
71
- font-size: 1.1em;
72
  cursor: pointer;
73
- transition: transform 0.2s, box-shadow 0.2s;
74
- box-shadow: 0 4px 15px rgba(102, 126, 234, 0.4);
75
- position: relative;
76
- overflow: hidden;
77
  }
78
 
79
- .file-input-button:hover {
 
80
  transform: translateY(-2px);
81
- box-shadow: 0 6px 20px rgba(102, 126, 234, 0.6);
82
  }
83
 
84
- .demo-button {
85
- background: linear-gradient(135deg, #00b894 0%, #00a085 100%);
86
- margin-left: 15px;
87
  }
88
 
89
- .button-group {
90
- display: flex;
91
- flex-wrap: wrap;
92
- gap: 15px;
93
- justify-content: center;
94
- }
95
-
96
- .stats {
97
- display: flex;
98
- justify-content: center;
99
- gap: 30px;
100
- margin-top: 20px;
101
- flex-wrap: wrap;
102
- }
103
-
104
- .stat-item {
105
- background: white;
106
- padding: 15px 25px;
107
- border-radius: 10px;
108
- box-shadow: 0 2px 10px rgba(0,0,0,0.1);
109
- text-align: center;
110
  }
111
 
112
- .stat-number {
113
- font-size: 1.5em;
114
- font-weight: bold;
115
- color: #667eea;
116
  }
117
 
118
- .stat-label {
119
- color: #666;
120
- font-size: 0.9em;
 
 
 
 
 
121
  }
122
 
123
- .visualization-area {
124
- padding: 30px;
125
- min-height: 600px;
126
  }
127
 
128
- .treemap-container {
129
- margin-bottom: 40px;
130
- border-radius: 10px;
 
 
 
 
 
 
131
  overflow: hidden;
132
- box-shadow: 0 5px 15px rgba(0,0,0,0.1);
 
 
133
  }
134
-
135
- .treemap-header {
136
- background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
137
  color: white;
138
- padding: 15px 20px;
139
- font-weight: bold;
140
- font-size: 1.1em;
 
 
 
 
 
141
  }
142
 
143
- .treemap {
144
- position: relative;
145
- background: #f8f9fa;
146
- min-height: 400px;
147
- border: 1px solid #e9ecef;
 
 
 
 
 
148
  }
149
 
150
- .treemap-node {
151
- position: absolute;
152
- border: 1px solid #fff;
 
 
 
 
153
  cursor: pointer;
154
- transition: all 0.3s ease;
155
- display: flex;
156
- align-items: center;
157
- justify-content: center;
158
- font-size: 12px;
159
- font-weight: 500;
160
- color: #333;
161
- overflow: hidden;
162
- }
163
-
164
- .treemap-node:hover {
165
- border-color: #667eea;
166
- border-width: 2px;
167
- transform: scale(1.02);
168
- z-index: 100;
169
- box-shadow: 0 5px 15px rgba(0,0,0,0.3);
170
- }
171
-
172
- .treemap-node.file {
173
- color: white;
174
  }
175
 
176
- .treemap-node.folder {
177
- background: linear-gradient(135deg, #fd79a8 0%, #e84393 100%);
178
  color: white;
179
  }
180
 
181
- /* File type specific colors */
182
- .treemap-node.file-image {
183
- background: linear-gradient(135deg, #00cec9 0%, #00b894 100%);
184
- }
185
-
186
- .treemap-node.file-video {
187
- background: linear-gradient(135deg, #e17055 0%, #d63031 100%);
188
- }
189
-
190
- .treemap-node.file-audio {
191
- background: linear-gradient(135deg, #a29bfe 0%, #6c5ce7 100%);
192
- }
193
-
194
- .treemap-node.file-document {
195
- background: linear-gradient(135deg, #fdcb6e 0%, #e17055 100%);
196
- }
197
-
198
- .treemap-node.file-office {
199
- background: linear-gradient(135deg, #00b894 0%, #00a085 100%);
200
- }
201
-
202
- .treemap-node.file-code {
203
- background: linear-gradient(135deg, #81ecec 0%, #00cec9 100%);
204
- }
205
-
206
- .legend {
207
- display: flex;
208
  justify-content: center;
209
- flex-wrap: wrap;
210
- gap: 15px;
211
- margin-top: 20px;
212
- padding: 20px;
213
- background: white;
214
- border-radius: 10px;
215
- box-shadow: 0 2px 10px rgba(0,0,0,0.1);
216
  }
217
-
218
- .legend-item {
219
- display: flex;
220
- align-items: center;
221
- gap: 8px;
222
- font-size: 0.9em;
223
  }
224
-
225
- .legend-color {
226
- width: 16px;
227
- height: 16px;
228
- border-radius: 3px;
229
- border: 1px solid rgba(0,0,0,0.1);
230
  }
231
-
232
- .legend-color.folder { background: linear-gradient(135deg, #fd79a8 0%, #e84393 100%); }
233
- .legend-color.image { background: linear-gradient(135deg, #00cec9 0%, #00b894 100%); }
234
- .legend-color.video { background: linear-gradient(135deg, #e17055 0%, #d63031 100%); }
235
- .legend-color.audio { background: linear-gradient(135deg, #a29bfe 0%, #6c5ce7 100%); }
236
- .legend-color.document { background: linear-gradient(135deg, #fdcb6e 0%, #e17055 100%); }
237
- .legend-color.office { background: linear-gradient(135deg, #00b894 0%, #00a085 100%); }
238
- .legend-color.code { background: linear-gradient(135deg, #81ecec 0%, #00cec9 100%); }
239
- .legend-color.other { background: linear-gradient(135deg, #74b9ff 0%, #0984e3 100%); }
240
-
241
- .tooltip {
242
- position: absolute;
243
- background: rgba(0, 0, 0, 0.9);
244
- color: white;
245
- padding: 10px 15px;
246
  border-radius: 5px;
247
- font-size: 12px;
248
- pointer-events: none;
249
- z-index: 1000;
250
- opacity: 0;
251
- transition: opacity 0.3s;
252
- max-width: 250px;
253
- line-height: 1.4;
254
- }
255
-
256
- .tooltip.visible {
257
- opacity: 1;
258
- }
259
-
260
- .loading {
261
- text-align: center;
262
- padding: 60px;
263
- color: #666;
264
- }
265
-
266
- .loading-spinner {
267
- width: 50px;
268
- height: 50px;
269
- border: 3px solid #f3f3f3;
270
- border-top: 3px solid #667eea;
271
- border-radius: 50%;
272
- animation: spin 1s linear infinite;
273
- margin: 0 auto 20px;
274
  }
275
-
276
- @keyframes spin {
277
- 0% { transform: rotate(0deg); }
278
- 100% { transform: rotate(360deg); }
279
  }
280
-
281
- .breadcrumb {
282
- padding: 10px 20px;
283
- background: #e9ecef;
284
- font-size: 14px;
285
- color: #666;
286
  }
287
 
288
- @media (max-width: 768px) {
289
- .header h1 {
290
- font-size: 1.8em;
291
- }
292
-
293
- .stats {
294
- gap: 15px;
295
- }
296
-
297
- .stat-item {
298
- padding: 10px 15px;
299
- font-size: 0.9em;
300
- }
301
-
302
- .visualization-area {
303
- padding: 15px;
304
- }
305
- }
306
  </style>
307
  </head>
308
  <body>
309
- <div class="container">
310
- <div class="header">
311
- <h1>🗂️ Squarified Treemap Explorer</h1>
312
- <p>Visualize hierarchical file structures using advanced treemap algorithms</p>
313
- </div>
314
 
315
- <div class="controls">
316
- <div class="file-input-wrapper">
317
- <input type="file" id="folderInput" class="file-input" webkitdirectory multiple>
318
- <div class="button-group">
319
- <button class="file-input-button" onclick="selectFolder()">
320
- 📁 Select Folder to Explore
321
- </button>
322
- <button class="file-input-button demo-button" onclick="generateDemoData()">
323
- 🎮 Try Demo Data
324
- </button>
325
- </div>
326
- <div class="stats" id="stats" style="display: none;">
327
- <div class="stat-item">
328
- <div class="stat-number" id="totalFiles">0</div>
329
- <div class="stat-label">Files</div>
330
- </div>
331
- <div class="stat-item">
332
- <div class="stat-number" id="totalFolders">0</div>
333
- <div class="stat-label">Folders</div>
334
- </div>
335
- <div class="stat-item">
336
- <div class="stat-number" id="totalSize">0 MB</div>
337
- <div class="stat-label">Total Size</div>
338
- </div>
339
- <div class="stat-item">
340
- <div class="stat-number" id="maxDepth">0</div>
341
- <div class="stat-label">Max Depth</div>
342
- </div>
343
- </div>
344
- <div class="legend" id="legend" style="display: none;">
345
- <div class="legend-item">
346
- <div class="legend-color folder"></div>
347
- <span>📁 Folders</span>
348
- </div>
349
- <div class="legend-item">
350
- <div class="legend-color image"></div>
351
- <span>🖼️ Images</span>
352
- </div>
353
- <div class="legend-item">
354
- <div class="legend-color video"></div>
355
- <span>🎥 Video</span>
356
- </div>
357
- <div class="legend-item">
358
- <div class="legend-color audio"></div>
359
- <span>🎵 Audio</span>
360
- </div>
361
- <div class="legend-item">
362
- <div class="legend-color document"></div>
363
- <span>📄 Documents</span>
364
- </div>
365
- <div class="legend-item">
366
- <div class="legend-color office"></div>
367
- <span>📊 Office</span>
368
- </div>
369
- <div class="legend-item">
370
- <div class="legend-color code"></div>
371
- <span>💻 Code</span>
372
- </div>
373
- <div class="legend-item">
374
- <div class="legend-color other"></div>
375
- <span>📦 Other</span>
376
- </div>
377
- </div>
378
- </div>
379
- </div>
380
 
381
- <div class="visualization-area" id="visualizationArea">
382
- <div style="text-align: center; padding: 60px; color: #999;">
383
- <h3>🎯 Ready to Explore</h3>
384
- <p>Select a folder above to visualize its structure, or try the demo data to see how it works!</p>
385
- </div>
386
- </div>
387
- </div>
388
 
389
- <div class="tooltip" id="tooltip"></div>
 
390
 
391
- <script>
392
- class SquarifiedTreemapExplorer {
393
- constructor() {
394
- this.tooltip = document.getElementById('tooltip');
395
- this.visualizationArea = document.getElementById('visualizationArea');
396
- this.folderInput = document.getElementById('folderInput');
397
- this.fileData = null;
398
- this.setupEventListeners();
399
- }
400
 
401
- setupEventListeners() {
402
- document.addEventListener('mousemove', (e) => {
403
- this.tooltip.style.left = e.pageX + 10 + 'px';
404
- this.tooltip.style.top = e.pageY + 10 + 'px';
405
- });
 
 
 
406
 
407
- // Listen for file input changes
408
- this.folderInput.addEventListener('change', (e) => {
409
- if (e.target.files.length > 0) {
410
- this.processFiles(e.target.files);
411
- }
412
- });
413
- }
414
 
415
- async selectFolder() {
416
- try {
417
- // Try modern File System Access API first
418
- if ('showDirectoryPicker' in window && window.location.protocol === 'https:') {
419
- const directoryHandle = await window.showDirectoryPicker();
420
- await this.processDirectory(directoryHandle);
421
- } else {
422
- // Fall back to file input
423
- this.folderInput.click();
424
- }
425
- } catch (error) {
426
- if (error.name !== 'AbortError') {
427
- console.log('File System Access API not available, using fallback');
428
- this.folderInput.click();
429
- }
430
- }
431
- }
432
 
433
- async processFiles(files) {
434
- this.showLoading();
435
-
436
- try {
437
- const fileTree = this.buildFileTreeFromFiles(files);
438
- this.fileData = fileTree;
439
- this.updateStats(fileTree);
440
- this.generateTreemaps(fileTree);
441
- } catch (error) {
442
- console.error('Error processing files:', error);
443
- this.showError('Error processing file structure.');
 
 
 
 
 
 
 
 
 
444
  }
 
 
445
  }
446
 
447
- buildFileTreeFromFiles(files) {
448
- const root = {
449
- name: 'Selected Folder',
450
- path: '',
451
- type: 'directory',
452
- size: 0,
453
- children: []
454
- };
455
-
456
- const pathMap = new Map();
457
- pathMap.set('', root);
458
-
459
- // Sort files by path to ensure directories are created before their contents
460
- const sortedFiles = Array.from(files).sort((a, b) => a.webkitRelativePath.localeCompare(b.webkitRelativePath));
461
-
462
- for (const file of sortedFiles) {
463
  const pathParts = file.webkitRelativePath.split('/');
 
464
  let currentPath = '';
465
-
466
- // Create directory structure
467
- for (let i = 0; i < pathParts.length - 1; i++) {
468
- const parentPath = currentPath;
469
- currentPath = currentPath ? `${currentPath}/${pathParts[i]}` : pathParts[i];
470
-
471
- if (!pathMap.has(currentPath)) {
472
- const dirNode = {
473
- name: pathParts[i],
474
- path: currentPath,
475
- type: 'directory',
476
- size: 0,
477
- children: []
478
- };
479
- pathMap.set(currentPath, dirNode);
480
- pathMap.get(parentPath).children.push(dirNode);
481
- }
482
- }
483
-
484
- // Add file
485
- const fileName = pathParts[pathParts.length - 1];
486
- const filePath = file.webkitRelativePath;
487
- const parentPath = pathParts.slice(0, -1).join('/');
488
-
489
- const fileNode = {
490
- name: fileName,
491
- path: filePath,
492
- type: 'file',
493
- size: file.size,
494
- lastModified: file.lastModified
495
- };
496
-
497
- pathMap.get(parentPath).children.push(fileNode);
498
- }
499
 
500
- // Calculate directory sizes and sort children
501
- this.calculateDirectorySizes(root);
502
- this.sortChildrenBySize(root);
503
-
504
- return root;
505
- }
506
-
507
- calculateDirectorySizes(node) {
508
- if (node.type === 'file') {
509
- return node.size;
510
- }
511
-
512
- let totalSize = 0;
513
- for (const child of node.children || []) {
514
- totalSize += this.calculateDirectorySizes(child);
515
- }
516
- node.size = totalSize;
517
- return totalSize;
518
- }
519
-
520
- sortChildrenBySize(node) {
521
- if (node.children) {
522
- node.children.sort((a, b) => b.size - a.size);
523
- node.children.forEach(child => this.sortChildrenBySize(child));
524
- }
525
- }
526
-
527
- generateDemoData() {
528
- const demoData = {
529
- name: 'Demo Project',
530
- path: '',
531
- type: 'directory',
532
- size: 78650000,
533
- children: [
534
- {
535
- name: 'src',
536
- path: 'src',
537
- type: 'directory',
538
- size: 28500000,
539
- children: [
540
- { name: 'main.js', path: 'src/main.js', type: 'file', size: 15200000 },
541
- { name: 'utils.py', path: 'src/utils.py', type: 'file', size: 8500000 },
542
- { name: 'config.json', path: 'src/config.json', type: 'file', size: 3200000 },
543
- { name: 'index.html', path: 'src/index.html', type: 'file', size: 1600000 }
544
- ]
545
- },
546
- {
547
- name: 'assets',
548
- path: 'assets',
549
- type: 'directory',
550
- size: 25800000,
551
- children: [
552
- { name: 'hero-video.mp4', path: 'assets/hero-video.mp4', type: 'file', size: 12400000 },
553
- { name: 'logo.png', path: 'assets/logo.png', type: 'file', size: 5600000 },
554
- { name: 'background.jpg', path: 'assets/background.jpg', type: 'file', size: 4200000 },
555
- { name: 'theme-song.mp3', path: 'assets/theme-song.mp3', type: 'file', size: 3600000 }
556
- ]
557
- },
558
- {
559
- name: 'docs',
560
- path: 'docs',
561
- type: 'directory',
562
- size: 8900000,
563
- children: [
564
- { name: 'manual.pdf', path: 'docs/manual.pdf', type: 'file', size: 5200000 },
565
- { name: 'README.md', path: 'docs/README.md', type: 'file', size: 1800000 },
566
- { name: 'API.txt', path: 'docs/API.txt', type: 'file', size: 1200000 },
567
- { name: 'CHANGELOG.md', path: 'docs/CHANGELOG.md', type: 'file', size: 700000 }
568
- ]
569
- },
570
- {
571
- name: 'data',
572
- path: 'data',
573
- type: 'directory',
574
- size: 11200000,
575
- children: [
576
- { name: 'report.xlsx', path: 'data/report.xlsx', type: 'file', size: 6800000 },
577
- { name: 'presentation.pptx', path: 'data/presentation.pptx', type: 'file', size: 4400000 }
578
- ]
579
- },
580
- {
581
- name: 'tests',
582
- path: 'tests',
583
- type: 'directory',
584
- size: 1320000,
585
- children: [
586
- { name: 'main.test.js', path: 'tests/main.test.js', type: 'file', size: 680000 },
587
- { name: 'utils.test.py', path: 'tests/utils.test.py', type: 'file', size: 440000 },
588
- { name: 'config.test.json', path: 'tests/config.test.json', type: 'file', size: 200000 }
589
- ]
590
- },
591
- { name: 'package.json', path: 'package.json', type: 'file', size: 180000 },
592
- { name: 'webpack.config.js', path: 'webpack.config.js', type: 'file', size: 120000 },
593
- { name: 'archive.zip', path: 'archive.zip', type: 'file', size: 2630000 }
594
- ]
595
- };
596
-
597
- this.fileData = demoData;
598
- this.updateStats(demoData);
599
- this.generateTreemaps(demoData);
600
- }
601
-
602
- async processDirectory(directoryHandle) {
603
- this.showLoading();
604
-
605
- try {
606
- const fileTree = await this.buildFileTree(directoryHandle);
607
- this.fileData = fileTree;
608
- this.updateStats(fileTree);
609
- this.generateTreemaps(fileTree);
610
- } catch (error) {
611
- console.error('Error processing directory:', error);
612
- this.showError('Error processing directory structure.');
613
- }
614
- }
615
-
616
- async buildFileTree(directoryHandle, path = '') {
617
- const node = {
618
- name: directoryHandle.name || 'Root',
619
- path: path,
620
- type: 'directory',
621
- size: 0,
622
- children: []
623
- };
624
-
625
- for await (const [name, handle] of directoryHandle.entries()) {
626
- try {
627
- const childPath = path ? `${path}/${name}` : name;
628
-
629
- if (handle.kind === 'file') {
630
- const file = await handle.getFile();
631
- node.children.push({
632
- name: name,
633
- path: childPath,
634
- type: 'file',
635
- size: file.size,
636
- lastModified: file.lastModified
637
- });
638
- node.size += file.size;
639
- } else if (handle.kind === 'directory') {
640
- const subDir = await this.buildFileTree(handle, childPath);
641
- node.children.push(subDir);
642
- node.size += subDir.size;
643
  }
644
- } catch (error) {
645
- console.warn(`Skipping ${name}:`, error);
646
  }
647
  }
648
-
649
- // Sort children by size (descending) for better treemap layout
650
- node.children.sort((a, b) => b.size - a.size);
651
- return node;
652
- }
653
-
654
- updateStats(fileTree) {
655
- const stats = this.calculateStats(fileTree);
656
-
657
- document.getElementById('totalFiles').textContent = stats.files.toLocaleString();
658
- document.getElementById('totalFolders').textContent = stats.folders.toLocaleString();
659
- document.getElementById('totalSize').textContent = this.formatFileSize(stats.size);
660
- document.getElementById('maxDepth').textContent = stats.depth;
661
- document.getElementById('stats').style.display = 'flex';
662
- document.getElementById('legend').style.display = 'flex';
663
  }
664
 
665
- getFileTypeCategory(filename) {
666
- if (!filename || filename.indexOf('.') === -1) return 'other';
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
667
 
668
- const extension = filename.toLowerCase().split('.').pop();
 
669
 
670
- const categories = {
671
- image: ['jpg', 'jpeg', 'png', 'webp', 'gif', 'bmp', 'ico'],
672
- video: ['mp4', 'avi', 'mov', 'mkv', 'wmv', 'flv', 'webm'],
673
- audio: ['mp3', 'wav', 'flac', 'aac', 'ogg', 'm4a'],
674
- document: ['pdf', 'md', 'txt', 'rtf', 'doc', 'docx'],
675
- office: ['xlsx', 'xls', 'pptx', 'ppt', 'csv'],
676
- code: ['html', 'htm', 'css', 'js', 'ts', 'py', 'java', 'cpp', 'c', 'php', 'rb', 'go', 'rs', 'swift', 'kt', 'json', 'xml', 'yml', 'yaml', 'svg']
677
- };
678
-
679
- for (const [category, extensions] of Object.entries(categories)) {
680
- if (extensions.includes(extension)) {
681
- return category;
682
- }
683
- }
684
-
685
- return 'other';
686
- }
687
-
688
- calculateStats(node, depth = 0) {
689
- let stats = {
690
- files: node.type === 'file' ? 1 : 0,
691
- folders: node.type === 'directory' ? 1 : 0,
692
- size: node.size || 0,
693
- depth: depth
694
- };
695
-
696
- if (node.children) {
697
- for (const child of node.children) {
698
- const childStats = this.calculateStats(child, depth + 1);
699
- stats.files += childStats.files;
700
- stats.folders += childStats.folders;
701
- stats.size += childStats.size;
702
- stats.depth = Math.max(stats.depth, childStats.depth);
703
- }
704
- }
705
-
706
- return stats;
707
- }
708
-
709
- generateTreemaps(fileTree) {
710
- this.visualizationArea.innerHTML = '';
711
-
712
- // Create main treemap
713
- this.createTreemapContainer(fileTree, 'Root Directory', 0);
714
-
715
- // Create treemaps for major subdirectories
716
- if (fileTree.children) {
717
- const majorFolders = fileTree.children
718
- .filter(child => child.type === 'directory' && child.children && child.children.length > 0)
719
- .slice(0, 5); // Show top 5 subdirectories
720
-
721
- majorFolders.forEach((folder, index) => {
722
- this.createTreemapContainer(folder, folder.name, index + 1);
723
  });
724
- }
725
- }
726
-
727
- createTreemapContainer(data, title, level) {
728
- const container = document.createElement('div');
729
- container.className = 'treemap-container';
730
-
731
- const header = document.createElement('div');
732
- header.className = 'treemap-header';
733
- header.textContent = `${title} (${this.formatFileSize(data.size)})`;
734
 
735
- const breadcrumb = document.createElement('div');
736
- breadcrumb.className = 'breadcrumb';
737
- breadcrumb.textContent = data.path || '/';
738
-
739
- const treemap = document.createElement('div');
740
- treemap.className = 'treemap';
741
- treemap.style.height = level === 0 ? '500px' : '400px';
742
-
743
- container.appendChild(header);
744
- container.appendChild(breadcrumb);
745
- container.appendChild(treemap);
746
-
747
- this.visualizationArea.appendChild(container);
748
-
749
- // Generate squarified treemap layout
750
- this.renderSquarifiedTreemap(treemap, data);
751
- }
752
-
753
- renderSquarifiedTreemap(container, data) {
754
- if (!data.children || data.children.length === 0) return;
755
-
756
- const rect = container.getBoundingClientRect();
757
- const width = rect.width || 800;
758
- const height = rect.height || 400;
759
-
760
- const totalSize = data.size;
761
- const children = data.children.filter(child => child.size > 0);
762
-
763
- if (children.length === 0) return;
764
-
765
- // Scale areas to fit container
766
- const scaledChildren = children.map(child => ({
767
- ...child,
768
- scaledSize: (child.size / totalSize) * (width * height)
769
- }));
770
 
771
- const layout = this.squarify(scaledChildren, [], width, { x: 0, y: 0, width, height });
772
- this.renderLayout(container, layout);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
773
  }
774
-
775
- squarify(children, row, w, container) {
776
- if (children.length === 0) {
777
- if (row.length > 0) {
778
- return this.layoutRow(row, container);
779
- }
780
- return [];
 
 
 
 
 
 
781
  }
 
782
 
783
- const c = children[0];
784
- const newRow = [...row, c];
785
-
786
- if (row.length === 0 || this.worst(newRow, w) <= this.worst(row, w)) {
787
- return this.squarify(children.slice(1), newRow, w, container);
788
- } else {
789
- const rowLayout = this.layoutRow(row, container);
790
- const remaining = this.shrinkContainer(container, row, w);
791
- const restLayout = this.squarify(children, [], this.getShortSide(remaining), remaining);
792
- return [...rowLayout, ...restLayout];
793
  }
794
- }
795
-
796
- worst(row, w) {
797
- if (row.length === 0) return Infinity;
798
-
799
- const areas = row.map(r => r.scaledSize);
800
- const sum = areas.reduce((a, b) => a + b, 0);
801
- const max = Math.max(...areas);
802
- const min = Math.min(...areas);
803
-
804
- const term1 = (w * w * max) / (sum * sum);
805
- const term2 = (sum * sum) / (w * w * min);
806
-
807
- return Math.max(term1, term2);
808
- }
809
-
810
- layoutRow(row, container) {
811
- if (row.length === 0) return [];
812
-
813
- const sum = row.reduce((acc, r) => acc + r.scaledSize, 0);
814
- const isVertical = container.width >= container.height;
815
-
816
- let layouts = [];
817
- let offset = 0;
818
-
819
- for (const item of row) {
820
- let rect;
821
- if (isVertical) {
822
- const height = container.height;
823
- const width = (item.scaledSize / sum) * (sum / height);
824
- rect = {
825
- x: container.x + offset,
826
- y: container.y,
827
- width: width,
828
- height: height,
829
- data: item
830
- };
831
- offset += width;
832
- } else {
833
- const width = container.width;
834
- const height = (item.scaledSize / sum) * (sum / width);
835
- rect = {
836
- x: container.x,
837
- y: container.y + offset,
838
- width: width,
839
- height: height,
840
- data: item
841
- };
842
- offset += height;
843
  }
844
- layouts.push(rect);
845
  }
846
-
847
- return layouts;
848
- }
849
-
850
- shrinkContainer(container, row, w) {
851
- const sum = row.reduce((acc, r) => acc + r.scaledSize, 0);
852
- const isVertical = container.width >= container.height;
853
-
854
- if (isVertical) {
855
- const usedWidth = sum / container.height;
856
- return {
857
- x: container.x + usedWidth,
858
- y: container.y,
859
- width: container.width - usedWidth,
860
- height: container.height
861
- };
862
- } else {
863
- const usedHeight = sum / container.width;
864
- return {
865
- x: container.x,
866
- y: container.y + usedHeight,
867
- width: container.width,
868
- height: container.height - usedHeight
869
- };
870
  }
871
- }
872
-
873
- getShortSide(container) {
874
- return Math.min(container.width, container.height);
875
- }
876
 
877
- renderLayout(container, layout) {
878
- container.innerHTML = '';
879
-
880
- layout.forEach(rect => {
881
- const element = document.createElement('div');
882
- let cssClass = `treemap-node ${rect.data.type}`;
883
-
884
- // Add file type specific class for files
885
- if (rect.data.type === 'file') {
886
- const fileType = this.getFileTypeCategory(rect.data.name);
887
- cssClass = `treemap-node file-${fileType}`;
888
- }
889
-
890
- element.className = cssClass;
891
-
892
- element.style.left = `${rect.x}px`;
893
- element.style.top = `${rect.y}px`;
894
- element.style.width = `${rect.width}px`;
895
- element.style.height = `${rect.height}px`;
896
-
897
- // Show name only if rectangle is large enough
898
- if (rect.width > 60 && rect.height > 20) {
899
- element.textContent = rect.data.name;
900
- }
901
-
902
- this.addTooltip(element, rect.data);
903
- container.appendChild(element);
904
- });
905
- }
906
-
907
- addTooltip(element, data) {
908
- element.addEventListener('mouseenter', () => {
909
- const tooltipContent = this.createTooltipContent(data);
910
- this.tooltip.innerHTML = tooltipContent;
911
- this.tooltip.classList.add('visible');
912
- });
913
-
914
- element.addEventListener('mouseleave', () => {
915
- this.tooltip.classList.remove('visible');
916
- });
917
- }
918
-
919
- createTooltipContent(data) {
920
- let content = `<strong>${data.name}</strong><br>`;
921
- content += `Type: ${data.type}`;
922
-
923
- if (data.type === 'file') {
924
- const fileType = this.getFileTypeCategory(data.name);
925
- const typeLabels = {
926
- image: '🖼️ Image',
927
- video: '🎥 Video',
928
- audio: '🎵 Audio',
929
- document: '📄 Document',
930
- office: '📊 Office',
931
- code: '💻 Code',
932
- other: '📦 Other'
933
- };
934
- content += ` (${typeLabels[fileType]})`;
935
- }
936
-
937
- content += `<br>Size: ${this.formatFileSize(data.size)}<br>`;
938
- content += `Path: ${data.path}<br>`;
939
-
940
- if (data.type === 'file' && data.lastModified) {
941
- content += `Modified: ${new Date(data.lastModified).toLocaleDateString()}<br>`;
942
- }
943
-
944
- if (data.children) {
945
- content += `Items: ${data.children.length}`;
946
- }
947
-
948
- return content;
949
- }
950
-
951
- formatFileSize(bytes) {
952
- if (bytes === 0) return '0 B';
953
-
954
  const k = 1024;
955
- const sizes = ['B', 'KB', 'MB', 'GB', 'TB'];
956
  const i = Math.floor(Math.log(bytes) / Math.log(k));
957
-
958
- return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
959
- }
960
-
961
- showLoading() {
962
- this.visualizationArea.innerHTML = `
963
- <div class="loading">
964
- <div class="loading-spinner"></div>
965
- <h3>📊 Processing Directory Structure</h3>
966
- <p>Analyzing files and building treemap visualization...</p>
967
- </div>
968
- `;
969
- }
970
-
971
- showError(message) {
972
- this.visualizationArea.innerHTML = `
973
- <div style="text-align: center; padding: 60px; color: #e74c3c;">
974
- <h3>❌ Error</h3>
975
- <p>${message}</p>
976
- </div>
977
- `;
978
  }
979
- }
980
-
981
- // Initialize the application
982
- const app = new SquarifiedTreemapExplorer();
983
-
984
- // Global functions for button clicks
985
- function selectFolder() {
986
- app.selectFolder();
987
- }
988
-
989
- function generateDemoData() {
990
- app.generateDemoData();
991
- }
992
  </script>
 
993
  </body>
994
- </html>
 
3
  <head>
4
  <meta charset="UTF-8">
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>Squarified Treemap - Folder Explorer</title>
7
  <style>
8
+ /*
9
+ This space solves for Treemap visualization of the file space produced by folders, directories, paths, files, and properties of files and directories such as their accumulated size, create date, modified date, file type, words mentioned in filename, groups that are emergent on multivariate file spaces with omni modality.
10
+
11
+ Papers that represent ideas implemented:
12
+
13
+ Inspiration:
14
+ A heuristic extending the Squarified treemapping algorithm. abstract: https://arxiv.org/abs/1609.00754
15
+ A heuristic extending the Squarified treemapping algorithm. pdf: https://arxiv.org/pdf/1609.00754
16
+ Squarified Treemaps: https://vanwijk.win.tue.nl/stm.pdf
17
+ Treemaps with Bounded Aspect Ratio: https://arxiv.org/abs/1012.1749 https://arxiv.org/pdf/1012.1749
18
+ Interactive Visualisation of Hierarchical Quantitative Data: an Evaluation https://www.arxiv.org/abs/1908.01277v1 https://www.arxiv.org/pdf/1908.01277v1
19
+ https://en.wikipedia.org/wiki/Treemapping
20
+ A Novel Algorithm for Real-time Procedural Generation of Building Floor Plans https://ar5iv.labs.arxiv.org/html/1211.5842
21
+ Fat Polygonal Partitions with Applications to Visualization and Embeddings https://arxiv.org/abs/1009.1866 https://arxiv.org/pdf/1009.1866
22
+ Tiling heuristics and evaluation metrics for treemaps with a target node aspect ratio: https://www.diva-portal.org/smash/get/diva2:1129639/FULLTEXT01.pdf
23
+ */
24
+ /* General Styling */
25
  body {
26
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
27
+ margin: 0;
28
+ padding: 2rem;
29
+ background-color: #f4f4f9;
30
+ color: #333;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
31
  text-align: center;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
32
  display: flex;
33
  flex-direction: column;
34
  align-items: center;
35
+ justify-content: flex-start;
36
+ min-height: 100vh;
37
  }
38
 
39
+ h1 {
40
+ color: #2c3e50;
41
+ }
42
+
43
+ p {
44
+ color: #555;
45
+ margin-bottom: 2rem;
46
+ max-width: 600px;
47
  }
48
 
49
+ /* Input Button Styling */
50
+ .folder-picker-label {
51
+ display: inline-block;
52
+ padding: 12px 24px;
53
+ background-color: #3498db;
54
  color: white;
55
+ border-radius: 8px;
 
 
 
56
  cursor: pointer;
57
+ font-weight: bold;
58
+ transition: background-color 0.3s ease, transform 0.2s ease;
 
 
59
  }
60
 
61
+ .folder-picker-label:hover {
62
+ background-color: #2980b9;
63
  transform: translateY(-2px);
 
64
  }
65
 
66
+ #folder-picker {
67
+ display: none; /* Hide the default file input */
 
68
  }
69
 
70
+ /* Treemap Container */
71
+ #treemap-container {
72
+ position: relative;
73
+ width: 90vw;
74
+ max-width: 1200px;
75
+ height: 75vh;
76
+ margin: 2rem auto;
77
+ border: 1px solid #ccc;
78
+ box-shadow: 0 4px 12px rgba(0,0,0,0.1);
79
+ background-color: #fff;
80
+ border-radius: 8px;
81
+ overflow: hidden; /* Ensures nodes don't spill out */
82
+ }
83
+
84
+ /* Styling for node groups (both leaves and internal directories) */
85
+ .node-group {
86
+ position: absolute;
87
+ box-sizing: border-box;
88
+ overflow: hidden;
 
 
89
  }
90
 
91
+ /* Styling for internal nodes (directories) to give them a frame */
92
+ .internal {
93
+ border: 1px solid #aaa;
 
94
  }
95
 
96
+ /* Styling for leaf nodes (files) */
97
+ .leaf {
98
+ background-clip: padding-box;
99
+ box-shadow: inset 0px 0px 0px 1px rgba(255, 255, 255, 0.8);
100
+ transition: filter 0.2s ease-in-out;
101
+ display: flex; /* Use flexbox for alignment of label */
102
+ align-items: flex-start; /* Align text to top */
103
+ justify-content: flex-start; /* Align text to left */
104
  }
105
 
106
+ .leaf:hover {
107
+ filter: brightness(1.15);
108
+ z-index: 10;
109
  }
110
 
111
+ /* Directory labels (now as links) */
112
+ .node-label {
113
+ display: block;
114
+ padding: 2px 5px;
115
+ color: #fff;
116
+ background-color: rgba(0,0,0,0.4);
117
+ font-size: 12px;
118
+ font-weight: bold;
119
+ white-space: nowrap;
120
  overflow: hidden;
121
+ text-overflow: ellipsis;
122
+ text-decoration: none;
123
+ cursor: pointer;
124
  }
125
+ .node-label:hover {
126
+ background-color: rgba(0,0,0,0.6);
127
+ }
128
+
129
+ /* File labels */
130
+ .leaf-label {
131
+ padding: 3px;
132
+ color: rgba(255, 255, 255, 0.95);
133
+ text-shadow: 1px 1px 2px rgba(0,0,0,0.7);
134
+ text-align: left;
135
+ font-size: 11px; /* Smaller font */
136
+ white-space: normal; /* Allow wrapping */
137
+ word-break: break-word; /* Break long words */
138
+ pointer-events: none; /* Make sure label doesn't block mouse events on parent */
139
+ }
140
+
141
+ /* Tooltip for Hover-overs */
142
+ #tooltip {
143
+ position: fixed; /* Use fixed to position relative to viewport */
144
+ background-color: rgba(0, 0, 0, 0.85);
145
  color: white;
146
+ padding: 8px 12px;
147
+ border-radius: 4px;
148
+ pointer-events: none; /* Allows mouse events to pass through to elements below */
149
+ opacity: 0;
150
+ transition: opacity 0.2s;
151
+ font-size: 14px;
152
+ z-index: 1001;
153
+ transform: translate(15px, 10px); /* Offset from cursor */
154
  }
155
 
156
+ /* Custom Context Menu */
157
+ #context-menu {
158
+ position: fixed;
159
+ display: none;
160
+ background-color: #ecf0f1;
161
+ border: 1px solid #bdc3c7;
162
+ box-shadow: 0 2px 10px rgba(0,0,0,0.2);
163
+ border-radius: 5px;
164
+ padding: 5px 0;
165
+ z-index: 1000;
166
  }
167
 
168
+ #context-menu button {
169
+ display: block;
170
+ width: 100%;
171
+ padding: 8px 20px;
172
+ border: none;
173
+ background: none;
174
+ text-align: left;
175
  cursor: pointer;
176
+ font-size: 14px;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
177
  }
178
 
179
+ #context-menu button:hover {
180
+ background-color: #3498db;
181
  color: white;
182
  }
183
 
184
+ /* Modal for confirmation */
185
+ .modal-overlay {
186
+ position: fixed;
187
+ top: 0;
188
+ left: 0;
189
+ width: 100%;
190
+ height: 100%;
191
+ background: rgba(0,0,0,0.5);
192
+ display: none;
193
+ align-items: center;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
194
  justify-content: center;
195
+ z-index: 2000;
 
 
 
 
 
 
196
  }
197
+ .modal-content {
198
+ background: white;
199
+ padding: 20px;
200
+ border-radius: 8px;
201
+ text-align: center;
202
+ box-shadow: 0 5px 15px rgba(0,0,0,0.3);
203
  }
204
+ .modal-content p {
205
+ margin-bottom: 20px;
 
 
 
 
206
  }
207
+ .modal-content button {
208
+ padding: 10px 20px;
209
+ border: none;
 
 
 
 
 
 
 
 
 
 
 
 
210
  border-radius: 5px;
211
+ cursor: pointer;
212
+ margin: 0 10px;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
213
  }
214
+ #modal-confirm {
215
+ background-color: #e74c3c;
216
+ color: white;
 
217
  }
218
+ #modal-cancel {
219
+ background-color: #bdc3c7;
 
 
 
 
220
  }
221
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
222
  </style>
223
  </head>
224
  <body>
 
 
 
 
 
225
 
226
+ <h1>Squarified Treemap Folder Explorer 🗺️</h1>
227
+ <p>Select a folder to visualize its contents. Hover over files or directories for info. Right-click on a file for options.</p>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
228
 
229
+ <label for="folder-picker" class="folder-picker-label">Choose a Folder</label>
230
+ <input type="file" id="folder-picker" webkitdirectory directory multiple />
 
 
 
 
 
231
 
232
+ <div id="treemap-container"></div>
233
+ <div id="tooltip"></div>
234
 
235
+ <!-- Custom Context Menu Structure -->
236
+ <div id="context-menu">
237
+ <button id="menu-copy-path">Copy Path</button>
238
+ <button id="menu-delete">Delete from View</button>
239
+ </div>
 
 
 
 
240
 
241
+ <!-- Modal Structure -->
242
+ <div id="delete-modal" class="modal-overlay">
243
+ <div class="modal-content">
244
+ <p>This will only remove the item from the visualization.<br>It <strong>will not</strong> be deleted from your computer. Do you want to continue?</p>
245
+ <button id="modal-confirm">Yes, Remove</button>
246
+ <button id="modal-cancel">Cancel</button>
247
+ </div>
248
+ </div>
249
 
 
 
 
 
 
 
 
250
 
251
+ <!-- D3.js Library for data visualization -->
252
+ <script src="https://d3js.org/d3.v7.min.js"></script>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
253
 
254
+ <!-- JS App Logic -->
255
+ <script>
256
+ document.addEventListener('DOMContentLoaded', () => {
257
+ const folderPicker = document.getElementById('folder-picker');
258
+ const treemapContainer = document.getElementById('treemap-container');
259
+ const tooltip = document.getElementById('tooltip');
260
+ const contextMenu = document.getElementById('context-menu');
261
+ const deleteModal = document.getElementById('delete-modal');
262
+
263
+ let currentFileTree = null; // To store the current data structure for modification
264
+ let nodeToDelete = null; // To store the node targeted for deletion
265
+
266
+ /**
267
+ * Processes the selected files and initiates the treemap rendering.
268
+ */
269
+ function handleFileSelect(event) {
270
+ const files = event.target.files;
271
+ if (files.length === 0) {
272
+ treemapContainer.innerHTML = '<p style="padding: 2rem;">No files selected or folder is empty.</p>';
273
+ return;
274
  }
275
+ currentFileTree = buildFileTree(files);
276
+ renderTreemap(currentFileTree);
277
  }
278
 
279
+ /**
280
+ * Builds a hierarchical tree structure from a flat FileList.
281
+ * This version adds a 'path' property to directories as well.
282
+ */
283
+ function buildFileTree(files) {
284
+ const root = { name: "root", path: "", children: [] }; // FIX: Added path property to root
285
+ for (const file of files) {
286
+ if (file.size === 0) continue;
 
 
 
 
 
 
 
 
287
  const pathParts = file.webkitRelativePath.split('/');
288
+ let currentNode = root;
289
  let currentPath = '';
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
290
 
291
+ for (let i = 0; i < pathParts.length; i++) {
292
+ const part = pathParts[i];
293
+ // Reconstruct path at each level
294
+ currentPath = currentPath ? `${currentPath}/${part}` : part;
295
+
296
+ if (i === pathParts.length - 1) { // It's a file
297
+ currentNode.children.push({ name: part, value: file.size, path: currentPath });
298
+ } else { // It's a directory
299
+ let dirNode = currentNode.children.find(child => child.name === part && child.children);
300
+ if (!dirNode) {
301
+ dirNode = { name: part, children: [], path: currentPath };
302
+ currentNode.children.push(dirNode);
303
+ }
304
+ currentNode = dirNode;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
305
  }
 
 
306
  }
307
  }
308
+ return root;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
309
  }
310
 
311
+ /**
312
+ * Renders the squarified treemap using D3.js.
313
+ */
314
+ function renderTreemap(data) {
315
+ treemapContainer.innerHTML = '';
316
+ const width = treemapContainer.clientWidth;
317
+ const height = treemapContainer.clientHeight;
318
+
319
+ const root = d3.hierarchy(data).sum(d => d.value).sort((a, b) => b.value - a.value);
320
+
321
+ const treemapLayout = d3.treemap()
322
+ .size([width, height])
323
+ .paddingInner(1)
324
+ .paddingOuter(3)
325
+ .paddingTop(20)
326
+ .tile(d3.treemapSquarify);
327
+
328
+ treemapLayout(root);
329
+ const color = d3.scaleOrdinal(d3.schemeCategory10);
330
+
331
+ const node = d3.select('#treemap-container')
332
+ .selectAll('div')
333
+ .data(root.descendants())
334
+ .join('div')
335
+ .attr('class', d => `node-group ${d.children ? 'internal' : 'leaf'}`)
336
+ .style('left', d => `${d.x0}px`)
337
+ .style('top', d => `${d.y0}px`)
338
+ .style('width', d => `${d.x1 - d.x0}px`)
339
+ .style('height', d => `${d.y1 - d.y0}px`);
340
 
341
+ // --- Handle Leaves (Files) ---
342
+ const leaves = node.filter(d => !d.children);
343
 
344
+ leaves.style('background-color', d => {
345
+ let ancestor = d;
346
+ while (ancestor.depth > 1) { ancestor = ancestor.parent; }
347
+ return color(ancestor.data.name);
348
+ })
349
+ .on('contextmenu', (event, d) => {
350
+ event.preventDefault();
351
+ event.stopPropagation(); // Stop event from bubbling to parent context menus
352
+ nodeToDelete = d;
353
+ contextMenu.style.display = 'block';
354
+ contextMenu.style.left = `${event.clientX}px`;
355
+ contextMenu.style.top = `${event.clientY}px`;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
356
  });
 
 
 
 
 
 
 
 
 
 
357
 
358
+ // Add labels to the leaf nodes
359
+ leaves.append('div')
360
+ .attr('class', 'leaf-label')
361
+ .text(d => d.data.name);
362
+
363
+ // Attach hover listeners to leaves
364
+ leaves.on('mouseenter', (event, d) => {
365
+ tooltip.style.opacity = 1; // FIX: Use direct style property
366
+ tooltip.innerHTML = `
367
+ <strong>File:</strong> ${d.data.name}<br>
368
+ <strong>Path:</strong> ${d.data.path}<br>
369
+ <strong>Size:</strong> ${formatBytes(d.value)}
370
+ `;
371
+ })
372
+ .on('mousemove', (event) => {
373
+ tooltip.style.left = `${event.clientX}px`; // FIX: Use direct style property
374
+ tooltip.style.top = `${event.clientY}px`; // FIX: Use direct style property
375
+ })
376
+ .on('mouseleave', () => {
377
+ tooltip.style.opacity = 0; // FIX: Use direct style property
378
+ });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
379
 
380
+ // --- Handle Directories ---
381
+ const directories = node.filter(d => d.children);
382
+
383
+ // Add labels to parent nodes (directories) as clickable links
384
+ directories.append('a')
385
+ .attr('class', 'node-label')
386
+ .attr('href', d => `file:///${d.data.path ? d.data.path.replace(/\//g, '\\') : ''}`) // FIX: Check if path exists
387
+ .on('click', event => event.preventDefault()) // Prevent default left-click behavior
388
+ .text(d => d.data.name);
389
+
390
+ // Attach hover listeners to directories
391
+ directories.on('mouseenter', (event, d) => {
392
+ tooltip.style.opacity = 1; // FIX: Use direct style property
393
+ tooltip.innerHTML = `
394
+ <strong>Directory:</strong> ${d.data.path}<br>
395
+ <strong>Total Size:</strong> ${formatBytes(d.value)}
396
+ `;
397
+ })
398
+ .on('mousemove', (event) => {
399
+ tooltip.style.left = `${event.clientX}px`; // FIX: Use direct style property
400
+ tooltip.style.top = `${event.clientY}px`; // FIX: Use direct style property
401
+ })
402
+ .on('mouseleave', () => {
403
+ tooltip.style.opacity = 0; // FIX: Use direct style property
404
+ });
405
  }
406
+
407
+ // --- Event Listeners ---
408
+ folderPicker.addEventListener('change', handleFileSelect);
409
+
410
+ // Hide context menu on any click
411
+ window.addEventListener('click', () => {
412
+ contextMenu.style.display = 'none';
413
+ });
414
+
415
+ // Context menu actions
416
+ document.getElementById('menu-copy-path').addEventListener('click', () => {
417
+ if (nodeToDelete && navigator.clipboard) {
418
+ navigator.clipboard.writeText(nodeToDelete.data.path).catch(err => console.error('Failed to copy path: ', err));
419
  }
420
+ });
421
 
422
+ document.getElementById('menu-delete').addEventListener('click', () => {
423
+ if (nodeToDelete) {
424
+ deleteModal.style.display = 'flex';
 
 
 
 
 
 
 
425
  }
426
+ });
427
+
428
+ // Modal actions
429
+ document.getElementById('modal-cancel').addEventListener('click', () => {
430
+ deleteModal.style.display = 'none';
431
+ nodeToDelete = null;
432
+ });
433
+
434
+ document.getElementById('modal-confirm').addEventListener('click', () => {
435
+ if (nodeToDelete && nodeToDelete.parent) {
436
+ // Find and remove the node from its parent's children array in the data
437
+ const children = nodeToDelete.parent.data.children;
438
+ const index = children.findIndex(child => child.path === nodeToDelete.data.path);
439
+ if (index > -1) {
440
+ children.splice(index, 1);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
441
  }
442
+ renderTreemap(currentFileTree); // Re-render with the modified data
443
  }
444
+ deleteModal.style.display = 'none';
445
+ nodeToDelete = null;
446
+ });
447
+
448
+ // Resize handler
449
+ window.addEventListener('resize', () => {
450
+ if (currentFileTree) {
451
+ renderTreemap(currentFileTree);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
452
  }
453
+ });
 
 
 
 
454
 
455
+ /**
456
+ * Formats bytes into a human-readable string (KB, MB, GB).
457
+ */
458
+ function formatBytes(bytes) {
459
+ if (bytes === 0) return '0 Bytes';
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
460
  const k = 1024;
461
+ const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB'];
462
  const i = Math.floor(Math.log(bytes) / Math.log(k));
463
+ return `${parseFloat((bytes / Math.pow(k, i)).toFixed(2))} ${sizes[i]}`;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
464
  }
465
+ });
 
 
 
 
 
 
 
 
 
 
 
 
466
  </script>
467
+
468
  </body>
469
+ </html>