awacke1 commited on
Commit
33bd719
·
verified ·
1 Parent(s): 8196ee6

Update index.html

Browse files
Files changed (1) hide show
  1. index.html +354 -1254
index.html CHANGED
@@ -3,1351 +3,451 @@
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
- .treemap-node.selected {
240
- border: 3px solid #667eea !important;
241
- z-index: 99;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
242
  }
243
 
244
- .context-menu {
 
245
  position: fixed;
246
- background: white;
247
- border-radius: 8px;
248
- box-shadow: 0 4px 20px rgba(0,0,0,0.15);
249
- padding: 8px 0;
250
- z-index: 10000;
251
- min-width: 200px;
252
- border: 1px solid #e9ecef;
253
  display: none;
 
 
 
 
 
 
254
  }
255
 
256
- .context-menu-item {
257
- padding: 10px 16px;
 
 
 
 
 
258
  cursor: pointer;
259
- transition: background-color 0.2s;
260
- display: flex;
261
- align-items: center;
262
- gap: 10px;
263
  font-size: 14px;
264
  }
265
 
266
- .context-menu-item:hover {
267
- background-color: #f8f9fa;
268
- }
269
-
270
- .context-menu-item.disabled {
271
- opacity: 0.5;
272
- cursor: not-allowed;
273
- }
274
-
275
- .context-menu-item.disabled:hover {
276
- background-color: white;
277
- }
278
-
279
- .context-menu-separator {
280
- height: 1px;
281
- background-color: #e9ecef;
282
- margin: 4px 0;
283
  }
284
 
285
- .file-operations-panel {
 
286
  position: fixed;
287
- top: 20px;
288
- right: 20px;
289
- background: white;
290
- border-radius: 10px;
291
- box-shadow: 0 4px 20px rgba(0,0,0,0.15);
292
- padding: 20px;
293
- max-width: 300px;
294
- z-index: 1000;
295
  display: none;
 
 
 
296
  }
297
-
298
- .operations-header {
299
- font-weight: bold;
300
- margin-bottom: 10px;
301
- color: #667eea;
 
302
  }
303
-
304
- .space-savings {
305
- background: #e8f5e8;
306
- padding: 10px;
307
- border-radius: 5px;
308
- margin-top: 10px;
309
- border-left: 3px solid #28a745;
310
  }
311
-
312
- .warning-note {
313
- background: #fff3cd;
314
- border: 1px solid #ffeaa7;
315
  border-radius: 5px;
316
- padding: 15px;
317
- margin: 20px;
318
- color: #856404;
319
- }
320
-
321
- .warning-note h4 {
322
- margin: 0 0 10px 0;
323
- color: #856404;
324
  }
325
-
326
- .tooltip {
327
- position: absolute;
328
- background: rgba(0, 0, 0, 0.9);
329
  color: white;
330
- padding: 10px 15px;
331
- border-radius: 5px;
332
- font-size: 12px;
333
- pointer-events: none;
334
- z-index: 1000;
335
- opacity: 0;
336
- transition: opacity 0.3s;
337
- max-width: 250px;
338
- line-height: 1.4;
339
- }
340
-
341
- .tooltip.visible {
342
- opacity: 1;
343
- }
344
-
345
- .loading {
346
- text-align: center;
347
- padding: 60px;
348
- color: #666;
349
- }
350
-
351
- .loading-spinner {
352
- width: 50px;
353
- height: 50px;
354
- border: 3px solid #f3f3f3;
355
- border-top: 3px solid #667eea;
356
- border-radius: 50%;
357
- animation: spin 1s linear infinite;
358
- margin: 0 auto 20px;
359
  }
360
-
361
- @keyframes spin {
362
- 0% { transform: rotate(0deg); }
363
- 100% { transform: rotate(360deg); }
364
  }
365
 
366
- .breadcrumb {
367
- padding: 10px 20px;
368
- background: #e9ecef;
369
- font-size: 14px;
370
- color: #666;
371
- }
372
-
373
- @media (max-width: 768px) {
374
- .header h1 {
375
- font-size: 1.8em;
376
- }
377
-
378
- .stats {
379
- gap: 15px;
380
- }
381
-
382
- .stat-item {
383
- padding: 10px 15px;
384
- font-size: 0.9em;
385
- }
386
-
387
- .visualization-area {
388
- padding: 15px;
389
- }
390
- }
391
  </style>
392
  </head>
393
  <body>
394
- <div class="container">
395
- <div class="header">
396
- <h1>🗂️ Squarified Treemap Explorer</h1>
397
- <p>Visualize hierarchical file structures using advanced treemap algorithms</p>
398
- </div>
399
 
400
- <div class="controls">
401
- <div class="file-input-wrapper">
402
- <input type="file" id="folderInput" class="file-input" webkitdirectory multiple>
403
- <div class="button-group">
404
- <button class="file-input-button" onclick="selectFolder()">
405
- 📁 Select Folder to Explore
406
- </button>
407
- <button class="file-input-button demo-button" onclick="generateDemoData()">
408
- 🎮 Try Demo Data
409
- </button>
410
- </div>
411
- <div class="stats" id="stats" style="display: none;">
412
- <div class="stat-item">
413
- <div class="stat-number" id="totalFiles">0</div>
414
- <div class="stat-label">Files</div>
415
- </div>
416
- <div class="stat-item">
417
- <div class="stat-number" id="totalFolders">0</div>
418
- <div class="stat-label">Folders</div>
419
- </div>
420
- <div class="stat-item">
421
- <div class="stat-number" id="totalSize">0 MB</div>
422
- <div class="stat-label">Total Size</div>
423
- </div>
424
- <div class="stat-item">
425
- <div class="stat-number" id="maxDepth">0</div>
426
- <div class="stat-label">Max Depth</div>
427
- </div>
428
- </div>
429
- <div class="legend" id="legend" style="display: none;">
430
- <div class="legend-item">
431
- <div class="legend-color folder"></div>
432
- <span>📁 Folders</span>
433
- </div>
434
- <div class="legend-item">
435
- <div class="legend-color image"></div>
436
- <span>🖼️ Images</span>
437
- </div>
438
- <div class="legend-item">
439
- <div class="legend-color video"></div>
440
- <span>🎥 Video</span>
441
- </div>
442
- <div class="legend-item">
443
- <div class="legend-color audio"></div>
444
- <span>🎵 Audio</span>
445
- </div>
446
- <div class="legend-item">
447
- <div class="legend-color document"></div>
448
- <span>📄 Documents</span>
449
- </div>
450
- <div class="legend-item">
451
- <div class="legend-color office"></div>
452
- <span>📊 Office</span>
453
- </div>
454
- <div class="legend-item">
455
- <div class="legend-color code"></div>
456
- <span>💻 Code</span>
457
- </div>
458
- <div class="legend-item">
459
- <div class="legend-color other"></div>
460
- <span>📦 Other</span>
461
- </div>
462
- </div>
463
- </div>
464
- </div>
465
 
466
- <div class="visualization-area" id="visualizationArea">
467
- <div style="text-align: center; padding: 60px; color: #999;">
468
- <h3>🎯 Ready to Explore</h3>
469
- <p>Select a folder above to visualize its structure, or try the demo data!</p>
470
- <p style="font-size: 0.9em; margin-top: 10px;">💡 <strong>Tip:</strong> Right-click any file or folder for operations menu</p>
471
- </div>
472
- </div>
473
- </div>
474
 
475
- <div class="tooltip" id="tooltip"></div>
 
476
 
477
- <div class="context-menu" id="contextMenu">
478
- <div class="context-menu-item" onclick="app.copyPath()">
479
- 📋 Copy Path
480
- </div>
481
- <div class="context-menu-item" onclick="app.showFileInfo()">
482
- ℹ️ File Info
483
- </div>
484
- <div class="context-menu-separator"></div>
485
- <div class="context-menu-item disabled" onclick="app.showDeleteSimulation()">
486
- 🗑️ Simulate Delete
487
- </div>
488
- <div class="context-menu-item disabled">
489
- ✂️ Move File (Browser Limited)
490
- </div>
491
- <div class="context-menu-separator"></div>
492
- <div class="context-menu-item" onclick="app.selectSimilarFiles()">
493
- 🔍 Select Similar Files
494
- </div>
495
  </div>
496
 
497
- <div class="file-operations-panel" id="operationsPanel">
498
- <div class="operations-header">File Operations</div>
499
- <div id="operationContent">
500
- Select a file or folder to see available operations
 
 
501
  </div>
502
- <button onclick="app.closeOperationsPanel()" style="margin-top: 10px; padding: 5px 10px; border: none; background: #667eea; color: white; border-radius: 3px; cursor: pointer;">Close</button>
503
  </div>
504
 
505
- <div class="warning-note">
506
- <h4>⚠️ Browser Security Limitations</h4>
507
- <p>Web browsers cannot directly modify files on your computer for security reasons. This tool provides:</p>
508
- <ul>
509
- <li>✅ Local file analysis (no uploads)</li>
510
- <li>✅ Visual space usage mapping</li>
511
- <li>✅ File information and path copying</li>
512
- <li>❌ Cannot delete/move files directly</li>
513
- </ul>
514
- <p><strong>For actual file operations:</strong> Use your system's file manager with the paths this tool provides.</p>
515
- </div>
516
-
517
- <script>
518
- class SquarifiedTreemapExplorer {
519
- constructor() {
520
- this.tooltip = document.getElementById('tooltip');
521
- this.visualizationArea = document.getElementById('visualizationArea');
522
- this.folderInput = document.getElementById('folderInput');
523
- this.contextMenu = document.getElementById('contextMenu');
524
- this.operationsPanel = document.getElementById('operationsPanel');
525
- this.selectedFileData = null;
526
- this.fileData = null;
527
- this.setupEventListeners();
528
- }
529
-
530
- setupEventListeners() {
531
- document.addEventListener('mousemove', (e) => {
532
- this.tooltip.style.left = e.pageX + 10 + 'px';
533
- this.tooltip.style.top = e.pageY + 10 + 'px';
534
- });
535
-
536
- // Hide context menu on click elsewhere
537
- document.addEventListener('click', (e) => {
538
- if (!this.contextMenu.contains(e.target)) {
539
- this.contextMenu.style.display = 'none';
540
- }
541
- });
542
 
543
- // Prevent default context menu
544
- document.addEventListener('contextmenu', (e) => {
545
- if (e.target.classList.contains('treemap-node')) {
546
- e.preventDefault();
547
- }
548
- });
549
 
550
- // Listen for file input changes
551
- this.folderInput.addEventListener('change', (e) => {
552
- if (e.target.files.length > 0) {
553
- this.processFiles(e.target.files);
554
- }
555
- });
556
- }
557
-
558
- async selectFolder() {
559
- try {
560
- // Try modern File System Access API first
561
- if ('showDirectoryPicker' in window && window.location.protocol === 'https:') {
562
- const directoryHandle = await window.showDirectoryPicker();
563
- await this.processDirectory(directoryHandle);
564
- } else {
565
- // Fall back to file input
566
- this.folderInput.click();
567
- }
568
- } catch (error) {
569
- if (error.name !== 'AbortError') {
570
- console.log('File System Access API not available, using fallback');
571
- this.folderInput.click();
572
- }
573
- }
574
- }
575
-
576
- async processFiles(files) {
577
- this.showLoading();
578
-
579
- try {
580
- const fileTree = this.buildFileTreeFromFiles(files);
581
- this.fileData = fileTree;
582
- this.updateStats(fileTree);
583
- this.generateTreemaps(fileTree);
584
- } catch (error) {
585
- console.error('Error processing files:', error);
586
- this.showError('Error processing file structure.');
587
  }
588
- }
589
-
590
- buildFileTreeFromFiles(files) {
591
- const root = {
592
- name: 'Selected Folder',
593
- path: '',
594
- type: 'directory',
595
- size: 0,
596
- children: []
597
- };
598
-
599
- const pathMap = new Map();
600
- pathMap.set('', root);
601
-
602
- // Sort files by path to ensure directories are created before their contents
603
- const sortedFiles = Array.from(files).sort((a, b) => a.webkitRelativePath.localeCompare(b.webkitRelativePath));
604
-
605
- for (const file of sortedFiles) {
606
  const pathParts = file.webkitRelativePath.split('/');
 
607
  let currentPath = '';
608
-
609
- // Create directory structure
610
- for (let i = 0; i < pathParts.length - 1; i++) {
611
- const parentPath = currentPath;
612
- currentPath = currentPath ? `${currentPath}/${pathParts[i]}` : pathParts[i];
613
-
614
- if (!pathMap.has(currentPath)) {
615
- const dirNode = {
616
- name: pathParts[i],
617
- path: currentPath,
618
- type: 'directory',
619
- size: 0,
620
- children: []
621
- };
622
- pathMap.set(currentPath, dirNode);
623
- pathMap.get(parentPath).children.push(dirNode);
624
- }
625
- }
626
-
627
- // Add file
628
- const fileName = pathParts[pathParts.length - 1];
629
- const filePath = file.webkitRelativePath;
630
- const parentPath = pathParts.slice(0, -1).join('/');
631
-
632
- const fileNode = {
633
- name: fileName,
634
- path: filePath,
635
- type: 'file',
636
- size: file.size,
637
- lastModified: file.lastModified
638
- };
639
-
640
- pathMap.get(parentPath).children.push(fileNode);
641
- }
642
-
643
- // Calculate directory sizes and sort children
644
- this.calculateDirectorySizes(root);
645
- this.sortChildrenBySize(root);
646
-
647
- return root;
648
- }
649
-
650
- calculateDirectorySizes(node) {
651
- if (node.type === 'file') {
652
- return node.size;
653
- }
654
-
655
- let totalSize = 0;
656
- for (const child of node.children || []) {
657
- totalSize += this.calculateDirectorySizes(child);
658
- }
659
- node.size = totalSize;
660
- return totalSize;
661
- }
662
-
663
- sortChildrenBySize(node) {
664
- if (node.children) {
665
- node.children.sort((a, b) => b.size - a.size);
666
- node.children.forEach(child => this.sortChildrenBySize(child));
667
- }
668
- }
669
-
670
- generateDemoData() {
671
- const demoData = {
672
- name: 'Demo Project',
673
- path: '',
674
- type: 'directory',
675
- size: 78650000,
676
- children: [
677
- {
678
- name: 'src',
679
- path: 'src',
680
- type: 'directory',
681
- size: 28500000,
682
- children: [
683
- { name: 'main.js', path: 'src/main.js', type: 'file', size: 15200000 },
684
- { name: 'utils.py', path: 'src/utils.py', type: 'file', size: 8500000 },
685
- { name: 'config.json', path: 'src/config.json', type: 'file', size: 3200000 },
686
- { name: 'index.html', path: 'src/index.html', type: 'file', size: 1600000 }
687
- ]
688
- },
689
- {
690
- name: 'assets',
691
- path: 'assets',
692
- type: 'directory',
693
- size: 25800000,
694
- children: [
695
- { name: 'hero-video.mp4', path: 'assets/hero-video.mp4', type: 'file', size: 12400000 },
696
- { name: 'logo.png', path: 'assets/logo.png', type: 'file', size: 5600000 },
697
- { name: 'background.jpg', path: 'assets/background.jpg', type: 'file', size: 4200000 },
698
- { name: 'theme-song.mp3', path: 'assets/theme-song.mp3', type: 'file', size: 3600000 }
699
- ]
700
- },
701
- {
702
- name: 'docs',
703
- path: 'docs',
704
- type: 'directory',
705
- size: 8900000,
706
- children: [
707
- { name: 'manual.pdf', path: 'docs/manual.pdf', type: 'file', size: 5200000 },
708
- { name: 'README.md', path: 'docs/README.md', type: 'file', size: 1800000 },
709
- { name: 'API.txt', path: 'docs/API.txt', type: 'file', size: 1200000 },
710
- { name: 'CHANGELOG.md', path: 'docs/CHANGELOG.md', type: 'file', size: 700000 }
711
- ]
712
- },
713
- {
714
- name: 'data',
715
- path: 'data',
716
- type: 'directory',
717
- size: 11200000,
718
- children: [
719
- { name: 'report.xlsx', path: 'data/report.xlsx', type: 'file', size: 6800000 },
720
- { name: 'presentation.pptx', path: 'data/presentation.pptx', type: 'file', size: 4400000 }
721
- ]
722
- },
723
- {
724
- name: 'tests',
725
- path: 'tests',
726
- type: 'directory',
727
- size: 1320000,
728
- children: [
729
- { name: 'main.test.js', path: 'tests/main.test.js', type: 'file', size: 680000 },
730
- { name: 'utils.test.py', path: 'tests/utils.test.py', type: 'file', size: 440000 },
731
- { name: 'config.test.json', path: 'tests/config.test.json', type: 'file', size: 200000 }
732
- ]
733
- },
734
- { name: 'package.json', path: 'package.json', type: 'file', size: 180000 },
735
- { name: 'webpack.config.js', path: 'webpack.config.js', type: 'file', size: 120000 },
736
- { name: 'archive.zip', path: 'archive.zip', type: 'file', size: 2630000 }
737
- ]
738
- };
739
-
740
- this.fileData = demoData;
741
- this.updateStats(demoData);
742
- this.generateTreemaps(demoData);
743
- }
744
-
745
- async processDirectory(directoryHandle) {
746
- this.showLoading();
747
 
748
- try {
749
- const fileTree = await this.buildFileTree(directoryHandle);
750
- this.fileData = fileTree;
751
- this.updateStats(fileTree);
752
- this.generateTreemaps(fileTree);
753
- } catch (error) {
754
- console.error('Error processing directory:', error);
755
- this.showError('Error processing directory structure.');
756
- }
757
- }
758
-
759
- async buildFileTree(directoryHandle, path = '') {
760
- const node = {
761
- name: directoryHandle.name || 'Root',
762
- path: path,
763
- type: 'directory',
764
- size: 0,
765
- children: []
766
- };
767
-
768
- for await (const [name, handle] of directoryHandle.entries()) {
769
- try {
770
- const childPath = path ? `${path}/${name}` : name;
771
-
772
- if (handle.kind === 'file') {
773
- const file = await handle.getFile();
774
- node.children.push({
775
- name: name,
776
- path: childPath,
777
- type: 'file',
778
- size: file.size,
779
- lastModified: file.lastModified
780
- });
781
- node.size += file.size;
782
- } else if (handle.kind === 'directory') {
783
- const subDir = await this.buildFileTree(handle, childPath);
784
- node.children.push(subDir);
785
- node.size += subDir.size;
786
  }
787
- } catch (error) {
788
- console.warn(`Skipping ${name}:`, error);
789
  }
790
  }
791
-
792
- // Sort children by size (descending) for better treemap layout
793
- node.children.sort((a, b) => b.size - a.size);
794
- return node;
795
- }
796
-
797
- updateStats(fileTree) {
798
- const stats = this.calculateStats(fileTree);
799
-
800
- document.getElementById('totalFiles').textContent = stats.files.toLocaleString();
801
- document.getElementById('totalFolders').textContent = stats.folders.toLocaleString();
802
- document.getElementById('totalSize').textContent = this.formatFileSize(stats.size);
803
- document.getElementById('maxDepth').textContent = stats.depth;
804
- document.getElementById('stats').style.display = 'flex';
805
- document.getElementById('legend').style.display = 'flex';
806
- }
807
-
808
- getFileTypeCategory(filename) {
809
- if (!filename || filename.indexOf('.') === -1) return 'other';
810
-
811
- const extension = filename.toLowerCase().split('.').pop();
812
-
813
- const categories = {
814
- image: ['jpg', 'jpeg', 'png', 'webp', 'gif', 'bmp', 'ico'],
815
- video: ['mp4', 'avi', 'mov', 'mkv', 'wmv', 'flv', 'webm'],
816
- audio: ['mp3', 'wav', 'flac', 'aac', 'ogg', 'm4a'],
817
- document: ['pdf', 'md', 'txt', 'rtf', 'doc', 'docx'],
818
- office: ['xlsx', 'xls', 'pptx', 'ppt', 'csv'],
819
- code: ['html', 'htm', 'css', 'js', 'ts', 'py', 'java', 'cpp', 'c', 'php', 'rb', 'go', 'rs', 'swift', 'kt', 'json', 'xml', 'yml', 'yaml', 'svg']
820
- };
821
-
822
- for (const [category, extensions] of Object.entries(categories)) {
823
- if (extensions.includes(extension)) {
824
- return category;
825
- }
826
- }
827
-
828
- return 'other';
829
- }
830
-
831
- calculateStats(node, depth = 0) {
832
- let stats = {
833
- files: node.type === 'file' ? 1 : 0,
834
- folders: node.type === 'directory' ? 1 : 0,
835
- size: node.size || 0,
836
- depth: depth
837
- };
838
-
839
- if (node.children) {
840
- for (const child of node.children) {
841
- const childStats = this.calculateStats(child, depth + 1);
842
- stats.files += childStats.files;
843
- stats.folders += childStats.folders;
844
- stats.size += childStats.size;
845
- stats.depth = Math.max(stats.depth, childStats.depth);
846
- }
847
- }
848
-
849
- return stats;
850
  }
851
 
852
- generateTreemaps(fileTree) {
853
- this.visualizationArea.innerHTML = '';
854
-
855
- // Create main treemap
856
- this.createTreemapContainer(fileTree, 'Root Directory', 0);
857
-
858
- // Create treemaps for major subdirectories
859
- if (fileTree.children) {
860
- const majorFolders = fileTree.children
861
- .filter(child => child.type === 'directory' && child.children && child.children.length > 0)
862
- .slice(0, 5); // Show top 5 subdirectories
863
-
864
- majorFolders.forEach((folder, index) => {
865
- this.createTreemapContainer(folder, folder.name, index + 1);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
866
  });
867
- }
868
- }
869
-
870
- createTreemapContainer(data, title, level) {
871
- const container = document.createElement('div');
872
- container.className = 'treemap-container';
873
-
874
- const header = document.createElement('div');
875
- header.className = 'treemap-header';
876
- header.textContent = `${title} (${this.formatFileSize(data.size)})`;
877
-
878
- const breadcrumb = document.createElement('div');
879
- breadcrumb.className = 'breadcrumb';
880
- breadcrumb.textContent = data.path || '/';
881
-
882
- const treemap = document.createElement('div');
883
- treemap.className = 'treemap';
884
- treemap.style.height = level === 0 ? '500px' : '400px';
885
-
886
- container.appendChild(header);
887
- container.appendChild(breadcrumb);
888
- container.appendChild(treemap);
889
-
890
- this.visualizationArea.appendChild(container);
891
-
892
- // Generate squarified treemap layout
893
- this.renderSquarifiedTreemap(treemap, data);
894
- }
895
-
896
- renderSquarifiedTreemap(container, data) {
897
- if (!data.children || data.children.length === 0) return;
898
-
899
- const rect = container.getBoundingClientRect();
900
- const width = rect.width || 800;
901
- const height = rect.height || 400;
902
-
903
- const totalSize = data.size;
904
- const children = data.children.filter(child => child.size > 0);
905
-
906
- if (children.length === 0) return;
907
-
908
- // Scale areas to fit container
909
- const scaledChildren = children.map(child => ({
910
- ...child,
911
- scaledSize: (child.size / totalSize) * (width * height)
912
- }));
913
-
914
- const layout = this.squarify(scaledChildren, [], width, { x: 0, y: 0, width, height });
915
- this.renderLayout(container, layout);
916
- }
917
-
918
- squarify(children, row, w, container) {
919
- if (children.length === 0) {
920
- if (row.length > 0) {
921
- return this.layoutRow(row, container);
922
- }
923
- return [];
924
- }
925
-
926
- const c = children[0];
927
- const newRow = [...row, c];
928
-
929
- if (row.length === 0 || this.worst(newRow, w) <= this.worst(row, w)) {
930
- return this.squarify(children.slice(1), newRow, w, container);
931
- } else {
932
- const rowLayout = this.layoutRow(row, container);
933
- const remaining = this.shrinkContainer(container, row, w);
934
- const restLayout = this.squarify(children, [], this.getShortSide(remaining), remaining);
935
- return [...rowLayout, ...restLayout];
936
- }
937
- }
938
-
939
- worst(row, w) {
940
- if (row.length === 0) return Infinity;
941
-
942
- const areas = row.map(r => r.scaledSize);
943
- const sum = areas.reduce((a, b) => a + b, 0);
944
- const max = Math.max(...areas);
945
- const min = Math.min(...areas);
946
-
947
- const term1 = (w * w * max) / (sum * sum);
948
- const term2 = (sum * sum) / (w * w * min);
949
-
950
- return Math.max(term1, term2);
951
- }
952
-
953
- layoutRow(row, container) {
954
- if (row.length === 0) return [];
955
-
956
- const sum = row.reduce((acc, r) => acc + r.scaledSize, 0);
957
- const isVertical = container.width >= container.height;
958
-
959
- let layouts = [];
960
- let offset = 0;
961
-
962
- for (const item of row) {
963
- let rect;
964
- if (isVertical) {
965
- const height = container.height;
966
- const width = (item.scaledSize / sum) * (sum / height);
967
- rect = {
968
- x: container.x + offset,
969
- y: container.y,
970
- width: width,
971
- height: height,
972
- data: item
973
- };
974
- offset += width;
975
- } else {
976
- const width = container.width;
977
- const height = (item.scaledSize / sum) * (sum / width);
978
- rect = {
979
- x: container.x,
980
- y: container.y + offset,
981
- width: width,
982
- height: height,
983
- data: item
984
- };
985
- offset += height;
986
- }
987
- layouts.push(rect);
988
- }
989
 
990
- return layouts;
991
- }
992
-
993
- shrinkContainer(container, row, w) {
994
- const sum = row.reduce((acc, r) => acc + r.scaledSize, 0);
995
- const isVertical = container.width >= container.height;
996
-
997
- if (isVertical) {
998
- const usedWidth = sum / container.height;
999
- return {
1000
- x: container.x + usedWidth,
1001
- y: container.y,
1002
- width: container.width - usedWidth,
1003
- height: container.height
1004
- };
1005
- } else {
1006
- const usedHeight = sum / container.width;
1007
- return {
1008
- x: container.x,
1009
- y: container.y + usedHeight,
1010
- width: container.width,
1011
- height: container.height - usedHeight
1012
- };
1013
- }
1014
- }
1015
-
1016
- getShortSide(container) {
1017
- return Math.min(container.width, container.height);
1018
- }
1019
-
1020
- renderLayout(container, layout) {
1021
- container.innerHTML = '';
1022
-
1023
- layout.forEach(rect => {
1024
- const element = document.createElement('div');
1025
- let cssClass = `treemap-node ${rect.data.type}`;
1026
-
1027
- // Add file type specific class for files
1028
- if (rect.data.type === 'file') {
1029
- const fileType = this.getFileTypeCategory(rect.data.name);
1030
- cssClass = `treemap-node file-${fileType}`;
1031
- }
1032
-
1033
- element.className = cssClass;
1034
-
1035
- element.style.left = `${rect.x}px`;
1036
- element.style.top = `${rect.y}px`;
1037
- element.style.width = `${rect.width}px`;
1038
- element.style.height = `${rect.height}px`;
1039
-
1040
- // Show name only if rectangle is large enough
1041
- if (rect.width > 60 && rect.height > 20) {
1042
- element.textContent = rect.data.name;
1043
- }
1044
-
1045
- this.addTooltip(element, rect.data);
1046
- container.appendChild(element);
1047
- });
1048
- }
1049
-
1050
- addTooltip(element, data) {
1051
- element.addEventListener('mouseenter', () => {
1052
- const tooltipContent = this.createTooltipContent(data);
1053
- this.tooltip.innerHTML = tooltipContent;
1054
- this.tooltip.classList.add('visible');
1055
- });
1056
-
1057
- element.addEventListener('mouseleave', () => {
1058
- this.tooltip.classList.remove('visible');
1059
- });
1060
-
1061
- // Add right-click context menu
1062
- element.addEventListener('contextmenu', (e) => {
1063
- e.preventDefault();
1064
- this.selectedFileData = data;
1065
- this.showContextMenu(e.pageX, e.pageY);
1066
- });
1067
-
1068
- // Add click handler for selection
1069
- element.addEventListener('click', (e) => {
1070
- e.stopPropagation();
1071
- this.selectedFileData = data;
1072
- this.highlightSelection(element);
1073
- });
1074
- }
1075
-
1076
- showContextMenu(x, y) {
1077
- this.contextMenu.style.left = x + 'px';
1078
- this.contextMenu.style.top = y + 'px';
1079
- this.contextMenu.style.display = 'block';
1080
-
1081
- // Enable/disable menu items based on selection
1082
- const deleteItem = this.contextMenu.querySelector('.context-menu-item.disabled');
1083
- if (this.selectedFileData) {
1084
- deleteItem.classList.remove('disabled');
1085
- deleteItem.onclick = () => this.showDeleteSimulation();
1086
- }
1087
- }
1088
-
1089
- highlightSelection(element) {
1090
- // Remove previous selection
1091
- document.querySelectorAll('.treemap-node.selected').forEach(el => {
1092
- el.classList.remove('selected');
1093
- el.style.border = '1px solid #fff';
1094
- });
1095
-
1096
- // Highlight selected element
1097
- element.classList.add('selected');
1098
- element.style.border = '3px solid #667eea';
1099
- }
1100
 
1101
- copyPath() {
1102
- if (!this.selectedFileData) return;
1103
-
1104
- navigator.clipboard.writeText(this.selectedFileData.path).then(() => {
1105
- this.showNotification(`📋 Copied: ${this.selectedFileData.path}`);
1106
- }).catch(() => {
1107
- // Fallback for older browsers
1108
- const textArea = document.createElement('textarea');
1109
- textArea.value = this.selectedFileData.path;
1110
- document.body.appendChild(textArea);
1111
- textArea.select();
1112
- document.execCommand('copy');
1113
- document.body.removeChild(textArea);
1114
- this.showNotification(`📋 Copied: ${this.selectedFileData.path}`);
1115
- });
1116
- this.contextMenu.style.display = 'none';
 
 
 
 
 
 
 
 
 
1117
  }
1118
-
1119
- showFileInfo() {
1120
- if (!this.selectedFileData) return;
1121
-
1122
- const data = this.selectedFileData;
1123
- let content = `<div class="operations-header">📄 File Information</div>`;
1124
- content += `<strong>Name:</strong> ${data.name}<br>`;
1125
- content += `<strong>Type:</strong> ${data.type}`;
1126
-
1127
- if (data.type === 'file') {
1128
- const fileType = this.getFileTypeCategory(data.name);
1129
- const typeLabels = {
1130
- image: '🖼️ Image',
1131
- video: '🎥 Video',
1132
- audio: '🎵 Audio',
1133
- document: '📄 Document',
1134
- office: '📊 Office',
1135
- code: '💻 Code',
1136
- other: '📦 Other'
1137
- };
1138
- content += ` (${typeLabels[fileType]})`;
1139
- }
1140
-
1141
- content += `<br><strong>Size:</strong> ${this.formatFileSize(data.size)}<br>`;
1142
- content += `<strong>Path:</strong> <code style="background: #f8f9fa; padding: 2px 4px; border-radius: 3px; font-size: 12px; word-break: break-all;">${data.path}</code><br>`;
1143
-
1144
- if (data.type === 'file' && data.lastModified) {
1145
- content += `<strong>Modified:</strong> ${new Date(data.lastModified).toLocaleString()}<br>`;
1146
- }
1147
-
1148
- if (data.children) {
1149
- content += `<strong>Items:</strong> ${data.children.length}<br>`;
1150
- content += `<strong>Subdirectories:</strong> ${data.children.filter(c => c.type === 'directory').length}<br>`;
1151
- content += `<strong>Files:</strong> ${data.children.filter(c => c.type === 'file').length}`;
1152
  }
 
1153
 
1154
- document.getElementById('operationContent').innerHTML = content;
1155
- this.operationsPanel.style.display = 'block';
1156
- this.contextMenu.style.display = 'none';
1157
- }
1158
-
1159
- showDeleteSimulation() {
1160
- if (!this.selectedFileData) return;
1161
-
1162
- const data = this.selectedFileData;
1163
- let content = `<div class="operations-header">🗑️ Delete Simulation</div>`;
1164
- content += `<p><strong>Would delete:</strong> ${data.name}</p>`;
1165
- content += `<div class="space-savings">`;
1166
- content += `💾 <strong>Space to be freed:</strong> ${this.formatFileSize(data.size)}<br>`;
1167
-
1168
- if (data.type === 'directory' && data.children) {
1169
- const fileCount = this.countFiles(data);
1170
- content += `📁 <strong>Items to be removed:</strong> ${fileCount.files} files, ${fileCount.folders} folders`;
1171
  }
1172
- content += `</div>`;
1173
-
1174
- content += `<p style="margin-top: 10px; font-size: 12px; color: #666;">`;
1175
- content += `<strong>To actually delete:</strong><br>`;
1176
- content += `1. Copy path: <code style="background: #f8f9fa; padding: 2px 4px; border-radius: 3px; font-size: 11px;">${data.path}</code><br>`;
1177
- content += `2. Use your system's file manager<br>`;
1178
- content += `3. Navigate to the copied path<br>`;
1179
- content += `4. Delete the file/folder manually`;
1180
- content += `</p>`;
1181
-
1182
- document.getElementById('operationContent').innerHTML = content;
1183
- this.operationsPanel.style.display = 'block';
1184
- this.contextMenu.style.display = 'none';
1185
- }
1186
-
1187
- selectSimilarFiles() {
1188
- if (!this.selectedFileData || this.selectedFileData.type !== 'file') return;
1189
-
1190
- const extension = this.selectedFileData.name.toLowerCase().split('.').pop();
1191
- const similarFiles = this.findFilesByExtension(this.fileData, extension);
1192
-
1193
- let content = `<div class="operations-header">🔍 Similar Files (.${extension})</div>`;
1194
- content += `<p>Found ${similarFiles.length} files with .${extension} extension:</p>`;
1195
- content += `<div style="max-height: 200px; overflow-y: auto; margin: 10px 0;">`;
1196
-
1197
- let totalSize = 0;
1198
- similarFiles.forEach(file => {
1199
- totalSize += file.size;
1200
- content += `<div style="padding: 5px; border-bottom: 1px solid #eee; font-size: 12px;">`;
1201
- content += `<strong>${file.name}</strong><br>`;
1202
- content += `<span style="color: #666;">${file.path} - ${this.formatFileSize(file.size)}</span>`;
1203
- content += `</div>`;
1204
- });
1205
-
1206
- content += `</div>`;
1207
- content += `<div class="space-savings">`;
1208
- content += `📊 <strong>Total size:</strong> ${this.formatFileSize(totalSize)}`;
1209
- content += `</div>`;
1210
-
1211
- document.getElementById('operationContent').innerHTML = content;
1212
- this.operationsPanel.style.display = 'block';
1213
- this.contextMenu.style.display = 'none';
1214
- }
1215
-
1216
- findFilesByExtension(node, extension) {
1217
- let files = [];
1218
-
1219
- if (node.type === 'file' && node.name.toLowerCase().endsWith(`.${extension}`)) {
1220
- files.push(node);
1221
- }
1222
-
1223
- if (node.children) {
1224
- for (const child of node.children) {
1225
- files = files.concat(this.findFilesByExtension(child, extension));
1226
  }
 
1227
  }
1228
-
1229
- return files;
1230
- }
1231
-
1232
- countFiles(node) {
1233
- let count = { files: 0, folders: 0 };
1234
-
1235
- if (node.type === 'file') {
1236
- count.files = 1;
1237
- } else if (node.type === 'directory') {
1238
- count.folders = 1;
1239
- if (node.children) {
1240
- for (const child of node.children) {
1241
- const childCount = this.countFiles(child);
1242
- count.files += childCount.files;
1243
- count.folders += childCount.folders;
1244
- }
1245
- }
1246
  }
1247
-
1248
- return count;
1249
- }
1250
 
1251
- closeOperationsPanel() {
1252
- this.operationsPanel.style.display = 'none';
1253
- }
1254
-
1255
- showNotification(message) {
1256
- // Create temporary notification
1257
- const notification = document.createElement('div');
1258
- notification.style.cssText = `
1259
- position: fixed;
1260
- top: 20px;
1261
- left: 50%;
1262
- transform: translateX(-50%);
1263
- background: #28a745;
1264
- color: white;
1265
- padding: 10px 20px;
1266
- border-radius: 5px;
1267
- z-index: 10001;
1268
- font-size: 14px;
1269
- `;
1270
- notification.textContent = message;
1271
- document.body.appendChild(notification);
1272
-
1273
- setTimeout(() => {
1274
- document.body.removeChild(notification);
1275
- }, 3000);
1276
- }
1277
-
1278
- createTooltipContent(data) {
1279
- let content = `<strong>${data.name}</strong><br>`;
1280
- content += `Type: ${data.type}`;
1281
-
1282
- if (data.type === 'file') {
1283
- const fileType = this.getFileTypeCategory(data.name);
1284
- const typeLabels = {
1285
- image: '🖼️ Image',
1286
- video: '🎥 Video',
1287
- audio: '🎵 Audio',
1288
- document: '📄 Document',
1289
- office: '📊 Office',
1290
- code: '💻 Code',
1291
- other: '📦 Other'
1292
- };
1293
- content += ` (${typeLabels[fileType]})`;
1294
- }
1295
-
1296
- content += `<br>Size: ${this.formatFileSize(data.size)}<br>`;
1297
- content += `Path: ${data.path}<br>`;
1298
-
1299
- if (data.type === 'file' && data.lastModified) {
1300
- content += `Modified: ${new Date(data.lastModified).toLocaleDateString()}<br>`;
1301
- }
1302
-
1303
- if (data.children) {
1304
- content += `Items: ${data.children.length}`;
1305
- }
1306
-
1307
- return content;
1308
- }
1309
-
1310
- formatFileSize(bytes) {
1311
- if (bytes === 0) return '0 B';
1312
-
1313
  const k = 1024;
1314
- const sizes = ['B', 'KB', 'MB', 'GB', 'TB'];
1315
  const i = Math.floor(Math.log(bytes) / Math.log(k));
1316
-
1317
- return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
1318
  }
1319
-
1320
- showLoading() {
1321
- this.visualizationArea.innerHTML = `
1322
- <div class="loading">
1323
- <div class="loading-spinner"></div>
1324
- <h3>📊 Processing Directory Structure</h3>
1325
- <p>Analyzing files and building treemap visualization...</p>
1326
- </div>
1327
- `;
1328
- }
1329
-
1330
- showError(message) {
1331
- this.visualizationArea.innerHTML = `
1332
- <div style="text-align: center; padding: 60px; color: #e74c3c;">
1333
- <h3>❌ Error</h3>
1334
- <p>${message}</p>
1335
- </div>
1336
- `;
1337
- }
1338
- }
1339
-
1340
- // Initialize the application
1341
- const app = new SquarifiedTreemapExplorer();
1342
-
1343
- // Global functions for button clicks
1344
- function selectFolder() {
1345
- app.selectFolder();
1346
- }
1347
-
1348
- function generateDemoData() {
1349
- app.generateDemoData();
1350
- }
1351
  </script>
 
1352
  </body>
1353
- </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
+ /* General Styling */
 
 
 
 
 
9
  body {
10
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
11
+ margin: 0;
12
+ padding: 2rem;
13
+ background-color: #f4f4f9;
14
+ color: #333;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
15
  text-align: center;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
16
  display: flex;
17
  flex-direction: column;
18
  align-items: center;
19
+ justify-content: flex-start;
20
+ min-height: 100vh;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
21
  }
22
 
23
+ h1 {
24
+ color: #2c3e50;
 
 
 
 
25
  }
26
 
27
+ p {
28
+ color: #555;
29
+ margin-bottom: 2rem;
30
+ max-width: 600px;
 
 
31
  }
32
 
33
+ /* Input Button Styling */
34
+ .folder-picker-label {
35
+ display: inline-block;
36
+ padding: 12px 24px;
37
+ background-color: #3498db;
38
+ color: white;
39
+ border-radius: 8px;
40
+ cursor: pointer;
41
  font-weight: bold;
42
+ transition: background-color 0.3s ease, transform 0.2s ease;
 
 
 
 
 
43
  }
44
 
45
+ .folder-picker-label:hover {
46
+ background-color: #2980b9;
47
+ transform: translateY(-2px);
 
 
 
 
 
 
 
48
  }
49
 
50
+ #folder-picker {
51
+ display: none; /* Hide the default file input */
 
 
 
 
52
  }
53
 
54
+ /* Treemap Container */
55
+ #treemap-container {
56
  position: relative;
57
+ width: 90vw;
58
+ max-width: 1200px;
59
+ height: 75vh;
60
+ margin: 2rem auto;
61
+ border: 1px solid #ccc;
62
+ box-shadow: 0 4px 12px rgba(0,0,0,0.1);
63
+ background-color: #fff;
64
+ border-radius: 8px;
65
+ overflow: hidden; /* Ensures nodes don't spill out */
66
  }
67
+
68
+ /* Styling for node groups (both leaves and internal directories) */
69
+ .node-group {
70
  position: absolute;
71
+ box-sizing: border-box;
 
 
 
 
 
 
 
 
72
  overflow: hidden;
73
  }
74
 
75
+ /* Styling for internal nodes (directories) to give them a frame */
76
+ .internal {
77
+ border: 1px solid #aaa;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
78
  }
79
 
80
+ /* Styling for leaf nodes (files) */
81
+ .leaf {
82
+ background-clip: padding-box;
83
+ box-shadow: inset 0px 0px 0px 1px rgba(255, 255, 255, 0.8);
84
+ transition: filter 0.2s ease-in-out;
85
+ display: flex; /* Use flexbox for alignment of label */
86
+ align-items: flex-start; /* Align text to top */
87
+ justify-content: flex-start; /* Align text to left */
88
  }
89
 
90
+ .leaf:hover {
91
+ filter: brightness(1.15);
92
+ z-index: 10;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
93
  }
94
 
95
+ /* Directory labels (now as links) */
96
+ .node-label {
97
+ display: block;
98
+ padding: 2px 5px;
99
+ color: #fff;
100
+ background-color: rgba(0,0,0,0.4);
101
+ font-size: 12px;
102
+ font-weight: bold;
103
+ white-space: nowrap;
104
+ overflow: hidden;
105
+ text-overflow: ellipsis;
106
+ text-decoration: none;
107
+ cursor: pointer;
108
  }
109
+ .node-label:hover {
110
+ background-color: rgba(0,0,0,0.6);
111
+ }
112
+
113
+ /* File labels */
114
+ .leaf-label {
115
+ padding: 3px;
116
+ color: rgba(255, 255, 255, 0.95);
117
+ text-shadow: 1px 1px 2px rgba(0,0,0,0.7);
118
+ text-align: left;
119
+ font-size: 11px; /* Smaller font */
120
+ white-space: normal; /* Allow wrapping */
121
+ word-break: break-word; /* Break long words */
122
+ pointer-events: none; /* Make sure label doesn't block mouse events on parent */
123
+ }
124
+
125
+ /* Tooltip for Hover-overs */
126
+ #tooltip {
127
+ position: fixed; /* Use fixed to position relative to viewport */
128
+ background-color: rgba(0, 0, 0, 0.85);
129
+ color: white;
130
+ padding: 8px 12px;
131
+ border-radius: 4px;
132
+ pointer-events: none; /* Allows mouse events to pass through to elements below */
133
+ opacity: 0;
134
+ transition: opacity 0.2s;
135
+ font-size: 14px;
136
+ z-index: 1001;
137
+ transform: translate(15px, 10px); /* Offset from cursor */
138
  }
139
 
140
+ /* Custom Context Menu */
141
+ #context-menu {
142
  position: fixed;
 
 
 
 
 
 
 
143
  display: none;
144
+ background-color: #ecf0f1;
145
+ border: 1px solid #bdc3c7;
146
+ box-shadow: 0 2px 10px rgba(0,0,0,0.2);
147
+ border-radius: 5px;
148
+ padding: 5px 0;
149
+ z-index: 1000;
150
  }
151
 
152
+ #context-menu button {
153
+ display: block;
154
+ width: 100%;
155
+ padding: 8px 20px;
156
+ border: none;
157
+ background: none;
158
+ text-align: left;
159
  cursor: pointer;
 
 
 
 
160
  font-size: 14px;
161
  }
162
 
163
+ #context-menu button:hover {
164
+ background-color: #3498db;
165
+ color: white;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
166
  }
167
 
168
+ /* Modal for confirmation */
169
+ .modal-overlay {
170
  position: fixed;
171
+ top: 0;
172
+ left: 0;
173
+ width: 100%;
174
+ height: 100%;
175
+ background: rgba(0,0,0,0.5);
 
 
 
176
  display: none;
177
+ align-items: center;
178
+ justify-content: center;
179
+ z-index: 2000;
180
  }
181
+ .modal-content {
182
+ background: white;
183
+ padding: 20px;
184
+ border-radius: 8px;
185
+ text-align: center;
186
+ box-shadow: 0 5px 15px rgba(0,0,0,0.3);
187
  }
188
+ .modal-content p {
189
+ margin-bottom: 20px;
 
 
 
 
 
190
  }
191
+ .modal-content button {
192
+ padding: 10px 20px;
193
+ border: none;
 
194
  border-radius: 5px;
195
+ cursor: pointer;
196
+ margin: 0 10px;
 
 
 
 
 
 
197
  }
198
+ #modal-confirm {
199
+ background-color: #e74c3c;
 
 
200
  color: white;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
201
  }
202
+ #modal-cancel {
203
+ background-color: #bdc3c7;
 
 
204
  }
205
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
206
  </style>
207
  </head>
208
  <body>
 
 
 
 
 
209
 
210
+ <h1>Squarified Treemap Folder Explorer 🗺️</h1>
211
+ <p>Select a folder to visualize its contents. Hover over files or directories for info. Right-click on a file for options.</p>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
212
 
213
+ <label for="folder-picker" class="folder-picker-label">Choose a Folder</label>
214
+ <input type="file" id="folder-picker" webkitdirectory directory multiple />
 
 
 
 
 
 
215
 
216
+ <div id="treemap-container"></div>
217
+ <div id="tooltip"></div>
218
 
219
+ <!-- Custom Context Menu Structure -->
220
+ <div id="context-menu">
221
+ <button id="menu-copy-path">Copy Path</button>
222
+ <button id="menu-delete">Delete from View</button>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
223
  </div>
224
 
225
+ <!-- Modal Structure -->
226
+ <div id="delete-modal" class="modal-overlay">
227
+ <div class="modal-content">
228
+ <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>
229
+ <button id="modal-confirm">Yes, Remove</button>
230
+ <button id="modal-cancel">Cancel</button>
231
  </div>
 
232
  </div>
233
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
234
 
235
+ <!-- D3.js Library for data visualization -->
236
+ <script src="https://d3js.org/d3.v7.min.js"></script>
 
 
 
 
237
 
238
+ <!-- JS App Logic -->
239
+ <script>
240
+ document.addEventListener('DOMContentLoaded', () => {
241
+ const folderPicker = document.getElementById('folder-picker');
242
+ const treemapContainer = document.getElementById('treemap-container');
243
+ const tooltip = document.getElementById('tooltip');
244
+ const contextMenu = document.getElementById('context-menu');
245
+ const deleteModal = document.getElementById('delete-modal');
246
+
247
+ let currentFileTree = null; // To store the current data structure for modification
248
+ let nodeToDelete = null; // To store the node targeted for deletion
249
+
250
+ /**
251
+ * Processes the selected files and initiates the treemap rendering.
252
+ */
253
+ function handleFileSelect(event) {
254
+ const files = event.target.files;
255
+ if (files.length === 0) {
256
+ treemapContainer.innerHTML = '<p style="padding: 2rem;">No files selected or folder is empty.</p>';
257
+ return;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
258
  }
259
+ currentFileTree = buildFileTree(files);
260
+ renderTreemap(currentFileTree);
261
+ }
262
+
263
+ /**
264
+ * Builds a hierarchical tree structure from a flat FileList.
265
+ * This version adds a 'path' property to directories as well.
266
+ */
267
+ function buildFileTree(files) {
268
+ const root = { name: "root", path: "", children: [] }; // FIX: Added path property to root
269
+ for (const file of files) {
270
+ if (file.size === 0) continue;
 
 
 
 
 
 
271
  const pathParts = file.webkitRelativePath.split('/');
272
+ let currentNode = root;
273
  let currentPath = '';
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
274
 
275
+ for (let i = 0; i < pathParts.length; i++) {
276
+ const part = pathParts[i];
277
+ // Reconstruct path at each level
278
+ currentPath = currentPath ? `${currentPath}/${part}` : part;
279
+
280
+ if (i === pathParts.length - 1) { // It's a file
281
+ currentNode.children.push({ name: part, value: file.size, path: currentPath });
282
+ } else { // It's a directory
283
+ let dirNode = currentNode.children.find(child => child.name === part && child.children);
284
+ if (!dirNode) {
285
+ dirNode = { name: part, children: [], path: currentPath };
286
+ currentNode.children.push(dirNode);
287
+ }
288
+ currentNode = dirNode;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
289
  }
 
 
290
  }
291
  }
292
+ return root;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
293
  }
294
 
295
+ /**
296
+ * Renders the squarified treemap using D3.js.
297
+ */
298
+ function renderTreemap(data) {
299
+ treemapContainer.innerHTML = '';
300
+ const width = treemapContainer.clientWidth;
301
+ const height = treemapContainer.clientHeight;
302
+
303
+ const root = d3.hierarchy(data).sum(d => d.value).sort((a, b) => b.value - a.value);
304
+
305
+ const treemapLayout = d3.treemap()
306
+ .size([width, height])
307
+ .paddingInner(1)
308
+ .paddingOuter(3)
309
+ .paddingTop(20)
310
+ .tile(d3.treemapSquarify);
311
+
312
+ treemapLayout(root);
313
+ const color = d3.scaleOrdinal(d3.schemeCategory10);
314
+
315
+ const node = d3.select('#treemap-container')
316
+ .selectAll('div')
317
+ .data(root.descendants())
318
+ .join('div')
319
+ .attr('class', d => `node-group ${d.children ? 'internal' : 'leaf'}`)
320
+ .style('left', d => `${d.x0}px`)
321
+ .style('top', d => `${d.y0}px`)
322
+ .style('width', d => `${d.x1 - d.x0}px`)
323
+ .style('height', d => `${d.y1 - d.y0}px`);
324
+
325
+ // --- Handle Leaves (Files) ---
326
+ const leaves = node.filter(d => !d.children);
327
+
328
+ leaves.style('background-color', d => {
329
+ let ancestor = d;
330
+ while (ancestor.depth > 1) { ancestor = ancestor.parent; }
331
+ return color(ancestor.data.name);
332
+ })
333
+ .on('contextmenu', (event, d) => {
334
+ event.preventDefault();
335
+ event.stopPropagation(); // Stop event from bubbling to parent context menus
336
+ nodeToDelete = d;
337
+ contextMenu.style.display = 'block';
338
+ contextMenu.style.left = `${event.clientX}px`;
339
+ contextMenu.style.top = `${event.clientY}px`;
340
  });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
341
 
342
+ // Add labels to the leaf nodes
343
+ leaves.append('div')
344
+ .attr('class', 'leaf-label')
345
+ .text(d => d.data.name);
346
+
347
+ // Attach hover listeners to leaves
348
+ leaves.on('mouseenter', (event, d) => {
349
+ tooltip.style.opacity = 1; // FIX: Use direct style property
350
+ tooltip.innerHTML = `
351
+ <strong>File:</strong> ${d.data.name}<br>
352
+ <strong>Path:</strong> ${d.data.path}<br>
353
+ <strong>Size:</strong> ${formatBytes(d.value)}
354
+ `;
355
+ })
356
+ .on('mousemove', (event) => {
357
+ tooltip.style.left = `${event.clientX}px`; // FIX: Use direct style property
358
+ tooltip.style.top = `${event.clientY}px`; // FIX: Use direct style property
359
+ })
360
+ .on('mouseleave', () => {
361
+ tooltip.style.opacity = 0; // FIX: Use direct style property
362
+ });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
363
 
364
+ // --- Handle Directories ---
365
+ const directories = node.filter(d => d.children);
366
+
367
+ // Add labels to parent nodes (directories) as clickable links
368
+ directories.append('a')
369
+ .attr('class', 'node-label')
370
+ .attr('href', d => `file:///${d.data.path ? d.data.path.replace(/\//g, '\\') : ''}`) // FIX: Check if path exists
371
+ .on('click', event => event.preventDefault()) // Prevent default left-click behavior
372
+ .text(d => d.data.name);
373
+
374
+ // Attach hover listeners to directories
375
+ directories.on('mouseenter', (event, d) => {
376
+ tooltip.style.opacity = 1; // FIX: Use direct style property
377
+ tooltip.innerHTML = `
378
+ <strong>Directory:</strong> ${d.data.path}<br>
379
+ <strong>Total Size:</strong> ${formatBytes(d.value)}
380
+ `;
381
+ })
382
+ .on('mousemove', (event) => {
383
+ tooltip.style.left = `${event.clientX}px`; // FIX: Use direct style property
384
+ tooltip.style.top = `${event.clientY}px`; // FIX: Use direct style property
385
+ })
386
+ .on('mouseleave', () => {
387
+ tooltip.style.opacity = 0; // FIX: Use direct style property
388
+ });
389
  }
390
+
391
+ // --- Event Listeners ---
392
+ folderPicker.addEventListener('change', handleFileSelect);
393
+
394
+ // Hide context menu on any click
395
+ window.addEventListener('click', () => {
396
+ contextMenu.style.display = 'none';
397
+ });
398
+
399
+ // Context menu actions
400
+ document.getElementById('menu-copy-path').addEventListener('click', () => {
401
+ if (nodeToDelete && navigator.clipboard) {
402
+ navigator.clipboard.writeText(nodeToDelete.data.path).catch(err => console.error('Failed to copy path: ', err));
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
403
  }
404
+ });
405
 
406
+ document.getElementById('menu-delete').addEventListener('click', () => {
407
+ if (nodeToDelete) {
408
+ deleteModal.style.display = 'flex';
 
 
 
 
 
 
 
 
 
 
 
 
 
 
409
  }
410
+ });
411
+
412
+ // Modal actions
413
+ document.getElementById('modal-cancel').addEventListener('click', () => {
414
+ deleteModal.style.display = 'none';
415
+ nodeToDelete = null;
416
+ });
417
+
418
+ document.getElementById('modal-confirm').addEventListener('click', () => {
419
+ if (nodeToDelete && nodeToDelete.parent) {
420
+ // Find and remove the node from its parent's children array in the data
421
+ const children = nodeToDelete.parent.data.children;
422
+ const index = children.findIndex(child => child.path === nodeToDelete.data.path);
423
+ if (index > -1) {
424
+ children.splice(index, 1);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
425
  }
426
+ renderTreemap(currentFileTree); // Re-render with the modified data
427
  }
428
+ deleteModal.style.display = 'none';
429
+ nodeToDelete = null;
430
+ });
431
+
432
+ // Resize handler
433
+ window.addEventListener('resize', () => {
434
+ if (currentFileTree) {
435
+ renderTreemap(currentFileTree);
 
 
 
 
 
 
 
 
 
 
436
  }
437
+ });
 
 
438
 
439
+ /**
440
+ * Formats bytes into a human-readable string (KB, MB, GB).
441
+ */
442
+ function formatBytes(bytes) {
443
+ if (bytes === 0) return '0 Bytes';
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
444
  const k = 1024;
445
+ const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB'];
446
  const i = Math.floor(Math.log(bytes) / Math.log(k));
447
+ return `${parseFloat((bytes / Math.pow(k, i)).toFixed(2))} ${sizes[i]}`;
 
448
  }
449
+ });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
450
  </script>
451
+
452
  </body>
453
+ </html>