|
<!DOCTYPE html> |
|
<html lang="en"> |
|
<head> |
|
<meta charset="UTF-8"> |
|
<meta name="viewport" content="width=device-width, initial-scale=1.0"> |
|
<title>Squarified Treemap Explorer</title> |
|
<style> |
|
* { |
|
margin: 0; |
|
padding: 0; |
|
box-sizing: border-box; |
|
} |
|
|
|
body { |
|
font-family: 'Arial', sans-serif; |
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); |
|
min-height: 100vh; |
|
padding: 20px; |
|
} |
|
|
|
.container { |
|
max-width: 1400px; |
|
margin: 0 auto; |
|
background: white; |
|
border-radius: 15px; |
|
box-shadow: 0 20px 40px rgba(0,0,0,0.1); |
|
overflow: hidden; |
|
} |
|
|
|
.header { |
|
background: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%); |
|
color: white; |
|
padding: 30px; |
|
text-align: center; |
|
} |
|
|
|
.header h1 { |
|
font-size: 2.5em; |
|
margin-bottom: 10px; |
|
text-shadow: 0 2px 4px rgba(0,0,0,0.3); |
|
} |
|
|
|
.header p { |
|
opacity: 0.9; |
|
font-size: 1.1em; |
|
} |
|
|
|
.controls { |
|
padding: 30px; |
|
background: #f8f9fa; |
|
border-bottom: 1px solid #e9ecef; |
|
} |
|
|
|
.file-input-wrapper { |
|
display: flex; |
|
flex-direction: column; |
|
align-items: center; |
|
gap: 15px; |
|
} |
|
|
|
.file-input { |
|
display: none; |
|
} |
|
|
|
.file-input-button { |
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); |
|
color: white; |
|
padding: 15px 30px; |
|
border: none; |
|
border-radius: 50px; |
|
font-size: 1.1em; |
|
cursor: pointer; |
|
transition: transform 0.2s, box-shadow 0.2s; |
|
box-shadow: 0 4px 15px rgba(102, 126, 234, 0.4); |
|
position: relative; |
|
overflow: hidden; |
|
} |
|
|
|
.file-input-button:hover { |
|
transform: translateY(-2px); |
|
box-shadow: 0 6px 20px rgba(102, 126, 234, 0.6); |
|
} |
|
|
|
.demo-button { |
|
background: linear-gradient(135deg, #00b894 0%, #00a085 100%); |
|
margin-left: 15px; |
|
} |
|
|
|
.button-group { |
|
display: flex; |
|
flex-wrap: wrap; |
|
gap: 15px; |
|
justify-content: center; |
|
} |
|
|
|
.stats { |
|
display: flex; |
|
justify-content: center; |
|
gap: 30px; |
|
margin-top: 20px; |
|
flex-wrap: wrap; |
|
} |
|
|
|
.stat-item { |
|
background: white; |
|
padding: 15px 25px; |
|
border-radius: 10px; |
|
box-shadow: 0 2px 10px rgba(0,0,0,0.1); |
|
text-align: center; |
|
} |
|
|
|
.stat-number { |
|
font-size: 1.5em; |
|
font-weight: bold; |
|
color: #667eea; |
|
} |
|
|
|
.stat-label { |
|
color: #666; |
|
font-size: 0.9em; |
|
} |
|
|
|
.visualization-area { |
|
padding: 30px; |
|
min-height: 600px; |
|
} |
|
|
|
.treemap-container { |
|
margin-bottom: 40px; |
|
border-radius: 10px; |
|
overflow: hidden; |
|
box-shadow: 0 5px 15px rgba(0,0,0,0.1); |
|
} |
|
|
|
.treemap-header { |
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); |
|
color: white; |
|
padding: 15px 20px; |
|
font-weight: bold; |
|
font-size: 1.1em; |
|
} |
|
|
|
.treemap { |
|
position: relative; |
|
background: #f8f9fa; |
|
min-height: 400px; |
|
border: 1px solid #e9ecef; |
|
} |
|
|
|
.treemap-node { |
|
position: absolute; |
|
border: 1px solid #fff; |
|
cursor: pointer; |
|
transition: all 0.3s ease; |
|
display: flex; |
|
align-items: center; |
|
justify-content: center; |
|
font-size: 12px; |
|
font-weight: 500; |
|
color: #333; |
|
overflow: hidden; |
|
} |
|
|
|
.treemap-node:hover { |
|
border-color: #667eea; |
|
border-width: 2px; |
|
transform: scale(1.02); |
|
z-index: 100; |
|
box-shadow: 0 5px 15px rgba(0,0,0,0.3); |
|
} |
|
|
|
.treemap-node.file { |
|
background: linear-gradient(135deg, #74b9ff 0%, #0984e3 100%); |
|
color: white; |
|
} |
|
|
|
.treemap-node.folder { |
|
background: linear-gradient(135deg, #fd79a8 0%, #e84393 100%); |
|
color: white; |
|
} |
|
|
|
.tooltip { |
|
position: absolute; |
|
background: rgba(0, 0, 0, 0.9); |
|
color: white; |
|
padding: 10px 15px; |
|
border-radius: 5px; |
|
font-size: 12px; |
|
pointer-events: none; |
|
z-index: 1000; |
|
opacity: 0; |
|
transition: opacity 0.3s; |
|
max-width: 250px; |
|
line-height: 1.4; |
|
} |
|
|
|
.tooltip.visible { |
|
opacity: 1; |
|
} |
|
|
|
.loading { |
|
text-align: center; |
|
padding: 60px; |
|
color: #666; |
|
} |
|
|
|
.loading-spinner { |
|
width: 50px; |
|
height: 50px; |
|
border: 3px solid #f3f3f3; |
|
border-top: 3px solid #667eea; |
|
border-radius: 50%; |
|
animation: spin 1s linear infinite; |
|
margin: 0 auto 20px; |
|
} |
|
|
|
@keyframes spin { |
|
0% { transform: rotate(0deg); } |
|
100% { transform: rotate(360deg); } |
|
} |
|
|
|
.breadcrumb { |
|
padding: 10px 20px; |
|
background: #e9ecef; |
|
font-size: 14px; |
|
color: #666; |
|
} |
|
|
|
@media (max-width: 768px) { |
|
.header h1 { |
|
font-size: 1.8em; |
|
} |
|
|
|
.stats { |
|
gap: 15px; |
|
} |
|
|
|
.stat-item { |
|
padding: 10px 15px; |
|
font-size: 0.9em; |
|
} |
|
|
|
.visualization-area { |
|
padding: 15px; |
|
} |
|
} |
|
</style> |
|
</head> |
|
<body> |
|
<div class="container"> |
|
<div class="header"> |
|
<h1>🗂️ Squarified Treemap Explorer</h1> |
|
<p>Visualize hierarchical file structures using advanced treemap algorithms</p> |
|
</div> |
|
|
|
<div class="controls"> |
|
<div class="file-input-wrapper"> |
|
<input type="file" id="folderInput" class="file-input" webkitdirectory multiple> |
|
<div class="button-group"> |
|
<button class="file-input-button" onclick="selectFolder()"> |
|
📁 Select Folder to Explore |
|
</button> |
|
<button class="file-input-button demo-button" onclick="generateDemoData()"> |
|
🎮 Try Demo Data |
|
</button> |
|
</div> |
|
<div class="stats" id="stats" style="display: none;"> |
|
<div class="stat-item"> |
|
<div class="stat-number" id="totalFiles">0</div> |
|
<div class="stat-label">Files</div> |
|
</div> |
|
<div class="stat-item"> |
|
<div class="stat-number" id="totalFolders">0</div> |
|
<div class="stat-label">Folders</div> |
|
</div> |
|
<div class="stat-item"> |
|
<div class="stat-number" id="totalSize">0 MB</div> |
|
<div class="stat-label">Total Size</div> |
|
</div> |
|
<div class="stat-item"> |
|
<div class="stat-number" id="maxDepth">0</div> |
|
<div class="stat-label">Max Depth</div> |
|
</div> |
|
</div> |
|
</div> |
|
</div> |
|
|
|
<div class="visualization-area" id="visualizationArea"> |
|
<div style="text-align: center; padding: 60px; color: #999;"> |
|
<h3>🎯 Ready to Explore</h3> |
|
<p>Select a folder above to visualize its structure, or try the demo data to see how it works!</p> |
|
</div> |
|
</div> |
|
</div> |
|
|
|
<div class="tooltip" id="tooltip"></div> |
|
|
|
<script> |
|
class SquarifiedTreemapExplorer { |
|
constructor() { |
|
this.tooltip = document.getElementById('tooltip'); |
|
this.visualizationArea = document.getElementById('visualizationArea'); |
|
this.folderInput = document.getElementById('folderInput'); |
|
this.fileData = null; |
|
this.setupEventListeners(); |
|
} |
|
|
|
setupEventListeners() { |
|
document.addEventListener('mousemove', (e) => { |
|
this.tooltip.style.left = e.pageX + 10 + 'px'; |
|
this.tooltip.style.top = e.pageY + 10 + 'px'; |
|
}); |
|
|
|
|
|
this.folderInput.addEventListener('change', (e) => { |
|
if (e.target.files.length > 0) { |
|
this.processFiles(e.target.files); |
|
} |
|
}); |
|
} |
|
|
|
async selectFolder() { |
|
try { |
|
|
|
if ('showDirectoryPicker' in window && window.location.protocol === 'https:') { |
|
const directoryHandle = await window.showDirectoryPicker(); |
|
await this.processDirectory(directoryHandle); |
|
} else { |
|
|
|
this.folderInput.click(); |
|
} |
|
} catch (error) { |
|
if (error.name !== 'AbortError') { |
|
console.log('File System Access API not available, using fallback'); |
|
this.folderInput.click(); |
|
} |
|
} |
|
} |
|
|
|
async processFiles(files) { |
|
this.showLoading(); |
|
|
|
try { |
|
const fileTree = this.buildFileTreeFromFiles(files); |
|
this.fileData = fileTree; |
|
this.updateStats(fileTree); |
|
this.generateTreemaps(fileTree); |
|
} catch (error) { |
|
console.error('Error processing files:', error); |
|
this.showError('Error processing file structure.'); |
|
} |
|
} |
|
|
|
buildFileTreeFromFiles(files) { |
|
const root = { |
|
name: 'Selected Folder', |
|
path: '', |
|
type: 'directory', |
|
size: 0, |
|
children: [] |
|
}; |
|
|
|
const pathMap = new Map(); |
|
pathMap.set('', root); |
|
|
|
|
|
const sortedFiles = Array.from(files).sort((a, b) => a.webkitRelativePath.localeCompare(b.webkitRelativePath)); |
|
|
|
for (const file of sortedFiles) { |
|
const pathParts = file.webkitRelativePath.split('/'); |
|
let currentPath = ''; |
|
|
|
|
|
for (let i = 0; i < pathParts.length - 1; i++) { |
|
const parentPath = currentPath; |
|
currentPath = currentPath ? `${currentPath}/${pathParts[i]}` : pathParts[i]; |
|
|
|
if (!pathMap.has(currentPath)) { |
|
const dirNode = { |
|
name: pathParts[i], |
|
path: currentPath, |
|
type: 'directory', |
|
size: 0, |
|
children: [] |
|
}; |
|
pathMap.set(currentPath, dirNode); |
|
pathMap.get(parentPath).children.push(dirNode); |
|
} |
|
} |
|
|
|
|
|
const fileName = pathParts[pathParts.length - 1]; |
|
const filePath = file.webkitRelativePath; |
|
const parentPath = pathParts.slice(0, -1).join('/'); |
|
|
|
const fileNode = { |
|
name: fileName, |
|
path: filePath, |
|
type: 'file', |
|
size: file.size, |
|
lastModified: file.lastModified |
|
}; |
|
|
|
pathMap.get(parentPath).children.push(fileNode); |
|
} |
|
|
|
|
|
this.calculateDirectorySizes(root); |
|
this.sortChildrenBySize(root); |
|
|
|
return root; |
|
} |
|
|
|
calculateDirectorySizes(node) { |
|
if (node.type === 'file') { |
|
return node.size; |
|
} |
|
|
|
let totalSize = 0; |
|
for (const child of node.children || []) { |
|
totalSize += this.calculateDirectorySizes(child); |
|
} |
|
node.size = totalSize; |
|
return totalSize; |
|
} |
|
|
|
sortChildrenBySize(node) { |
|
if (node.children) { |
|
node.children.sort((a, b) => b.size - a.size); |
|
node.children.forEach(child => this.sortChildrenBySize(child)); |
|
} |
|
} |
|
|
|
generateDemoData() { |
|
const demoData = { |
|
name: 'Demo Project', |
|
path: '', |
|
type: 'directory', |
|
size: 45320000, |
|
children: [ |
|
{ |
|
name: 'src', |
|
path: 'src', |
|
type: 'directory', |
|
size: 28500000, |
|
children: [ |
|
{ name: 'main.js', path: 'src/main.js', type: 'file', size: 15200000 }, |
|
{ name: 'utils.js', path: 'src/utils.js', type: 'file', size: 8500000 }, |
|
{ name: 'config.js', path: 'src/config.js', type: 'file', size: 3200000 }, |
|
{ name: 'helpers.js', path: 'src/helpers.js', type: 'file', size: 1600000 } |
|
] |
|
}, |
|
{ |
|
name: 'assets', |
|
path: 'assets', |
|
type: 'directory', |
|
size: 12400000, |
|
children: [ |
|
{ name: 'logo.png', path: 'assets/logo.png', type: 'file', size: 5600000 }, |
|
{ name: 'background.jpg', path: 'assets/background.jpg', type: 'file', size: 4200000 }, |
|
{ name: 'icons.svg', path: 'assets/icons.svg', type: 'file', size: 2600000 } |
|
] |
|
}, |
|
{ |
|
name: 'docs', |
|
path: 'docs', |
|
type: 'directory', |
|
size: 2800000, |
|
children: [ |
|
{ name: 'README.md', path: 'docs/README.md', type: 'file', size: 1200000 }, |
|
{ name: 'API.md', path: 'docs/API.md', type: 'file', size: 900000 }, |
|
{ name: 'CHANGELOG.md', path: 'docs/CHANGELOG.md', type: 'file', size: 700000 } |
|
] |
|
}, |
|
{ |
|
name: 'tests', |
|
path: 'tests', |
|
type: 'directory', |
|
size: 1320000, |
|
children: [ |
|
{ name: 'main.test.js', path: 'tests/main.test.js', type: 'file', size: 680000 }, |
|
{ name: 'utils.test.js', path: 'tests/utils.test.js', type: 'file', size: 440000 }, |
|
{ name: 'config.test.js', path: 'tests/config.test.js', type: 'file', size: 200000 } |
|
] |
|
}, |
|
{ name: 'package.json', path: 'package.json', type: 'file', size: 180000 }, |
|
{ name: 'webpack.config.js', path: 'webpack.config.js', type: 'file', size: 120000 } |
|
] |
|
}; |
|
|
|
this.fileData = demoData; |
|
this.updateStats(demoData); |
|
this.generateTreemaps(demoData); |
|
} |
|
|
|
async processDirectory(directoryHandle) { |
|
this.showLoading(); |
|
|
|
try { |
|
const fileTree = await this.buildFileTree(directoryHandle); |
|
this.fileData = fileTree; |
|
this.updateStats(fileTree); |
|
this.generateTreemaps(fileTree); |
|
} catch (error) { |
|
console.error('Error processing directory:', error); |
|
this.showError('Error processing directory structure.'); |
|
} |
|
} |
|
|
|
async buildFileTree(directoryHandle, path = '') { |
|
const node = { |
|
name: directoryHandle.name || 'Root', |
|
path: path, |
|
type: 'directory', |
|
size: 0, |
|
children: [] |
|
}; |
|
|
|
for await (const [name, handle] of directoryHandle.entries()) { |
|
try { |
|
const childPath = path ? `${path}/${name}` : name; |
|
|
|
if (handle.kind === 'file') { |
|
const file = await handle.getFile(); |
|
node.children.push({ |
|
name: name, |
|
path: childPath, |
|
type: 'file', |
|
size: file.size, |
|
lastModified: file.lastModified |
|
}); |
|
node.size += file.size; |
|
} else if (handle.kind === 'directory') { |
|
const subDir = await this.buildFileTree(handle, childPath); |
|
node.children.push(subDir); |
|
node.size += subDir.size; |
|
} |
|
} catch (error) { |
|
console.warn(`Skipping ${name}:`, error); |
|
} |
|
} |
|
|
|
|
|
node.children.sort((a, b) => b.size - a.size); |
|
return node; |
|
} |
|
|
|
updateStats(fileTree) { |
|
const stats = this.calculateStats(fileTree); |
|
|
|
document.getElementById('totalFiles').textContent = stats.files.toLocaleString(); |
|
document.getElementById('totalFolders').textContent = stats.folders.toLocaleString(); |
|
document.getElementById('totalSize').textContent = this.formatFileSize(stats.size); |
|
document.getElementById('maxDepth').textContent = stats.depth; |
|
document.getElementById('stats').style.display = 'flex'; |
|
} |
|
|
|
calculateStats(node, depth = 0) { |
|
let stats = { |
|
files: node.type === 'file' ? 1 : 0, |
|
folders: node.type === 'directory' ? 1 : 0, |
|
size: node.size || 0, |
|
depth: depth |
|
}; |
|
|
|
if (node.children) { |
|
for (const child of node.children) { |
|
const childStats = this.calculateStats(child, depth + 1); |
|
stats.files += childStats.files; |
|
stats.folders += childStats.folders; |
|
stats.size += childStats.size; |
|
stats.depth = Math.max(stats.depth, childStats.depth); |
|
} |
|
} |
|
|
|
return stats; |
|
} |
|
|
|
generateTreemaps(fileTree) { |
|
this.visualizationArea.innerHTML = ''; |
|
|
|
|
|
this.createTreemapContainer(fileTree, 'Root Directory', 0); |
|
|
|
|
|
if (fileTree.children) { |
|
const majorFolders = fileTree.children |
|
.filter(child => child.type === 'directory' && child.children && child.children.length > 0) |
|
.slice(0, 5); |
|
|
|
majorFolders.forEach((folder, index) => { |
|
this.createTreemapContainer(folder, folder.name, index + 1); |
|
}); |
|
} |
|
} |
|
|
|
createTreemapContainer(data, title, level) { |
|
const container = document.createElement('div'); |
|
container.className = 'treemap-container'; |
|
|
|
const header = document.createElement('div'); |
|
header.className = 'treemap-header'; |
|
header.textContent = `${title} (${this.formatFileSize(data.size)})`; |
|
|
|
const breadcrumb = document.createElement('div'); |
|
breadcrumb.className = 'breadcrumb'; |
|
breadcrumb.textContent = data.path || '/'; |
|
|
|
const treemap = document.createElement('div'); |
|
treemap.className = 'treemap'; |
|
treemap.style.height = level === 0 ? '500px' : '400px'; |
|
|
|
container.appendChild(header); |
|
container.appendChild(breadcrumb); |
|
container.appendChild(treemap); |
|
|
|
this.visualizationArea.appendChild(container); |
|
|
|
|
|
this.renderSquarifiedTreemap(treemap, data); |
|
} |
|
|
|
renderSquarifiedTreemap(container, data) { |
|
if (!data.children || data.children.length === 0) return; |
|
|
|
const rect = container.getBoundingClientRect(); |
|
const width = rect.width || 800; |
|
const height = rect.height || 400; |
|
|
|
const totalSize = data.size; |
|
const children = data.children.filter(child => child.size > 0); |
|
|
|
if (children.length === 0) return; |
|
|
|
|
|
const scaledChildren = children.map(child => ({ |
|
...child, |
|
scaledSize: (child.size / totalSize) * (width * height) |
|
})); |
|
|
|
const layout = this.squarify(scaledChildren, [], width, { x: 0, y: 0, width, height }); |
|
this.renderLayout(container, layout); |
|
} |
|
|
|
squarify(children, row, w, container) { |
|
if (children.length === 0) { |
|
if (row.length > 0) { |
|
return this.layoutRow(row, container); |
|
} |
|
return []; |
|
} |
|
|
|
const c = children[0]; |
|
const newRow = [...row, c]; |
|
|
|
if (row.length === 0 || this.worst(newRow, w) <= this.worst(row, w)) { |
|
return this.squarify(children.slice(1), newRow, w, container); |
|
} else { |
|
const rowLayout = this.layoutRow(row, container); |
|
const remaining = this.shrinkContainer(container, row, w); |
|
const restLayout = this.squarify(children, [], this.getShortSide(remaining), remaining); |
|
return [...rowLayout, ...restLayout]; |
|
} |
|
} |
|
|
|
worst(row, w) { |
|
if (row.length === 0) return Infinity; |
|
|
|
const areas = row.map(r => r.scaledSize); |
|
const sum = areas.reduce((a, b) => a + b, 0); |
|
const max = Math.max(...areas); |
|
const min = Math.min(...areas); |
|
|
|
const term1 = (w * w * max) / (sum * sum); |
|
const term2 = (sum * sum) / (w * w * min); |
|
|
|
return Math.max(term1, term2); |
|
} |
|
|
|
layoutRow(row, container) { |
|
if (row.length === 0) return []; |
|
|
|
const sum = row.reduce((acc, r) => acc + r.scaledSize, 0); |
|
const isVertical = container.width >= container.height; |
|
|
|
let layouts = []; |
|
let offset = 0; |
|
|
|
for (const item of row) { |
|
let rect; |
|
if (isVertical) { |
|
const height = container.height; |
|
const width = (item.scaledSize / sum) * (sum / height); |
|
rect = { |
|
x: container.x + offset, |
|
y: container.y, |
|
width: width, |
|
height: height, |
|
data: item |
|
}; |
|
offset += width; |
|
} else { |
|
const width = container.width; |
|
const height = (item.scaledSize / sum) * (sum / width); |
|
rect = { |
|
x: container.x, |
|
y: container.y + offset, |
|
width: width, |
|
height: height, |
|
data: item |
|
}; |
|
offset += height; |
|
} |
|
layouts.push(rect); |
|
} |
|
|
|
return layouts; |
|
} |
|
|
|
shrinkContainer(container, row, w) { |
|
const sum = row.reduce((acc, r) => acc + r.scaledSize, 0); |
|
const isVertical = container.width >= container.height; |
|
|
|
if (isVertical) { |
|
const usedWidth = sum / container.height; |
|
return { |
|
x: container.x + usedWidth, |
|
y: container.y, |
|
width: container.width - usedWidth, |
|
height: container.height |
|
}; |
|
} else { |
|
const usedHeight = sum / container.width; |
|
return { |
|
x: container.x, |
|
y: container.y + usedHeight, |
|
width: container.width, |
|
height: container.height - usedHeight |
|
}; |
|
} |
|
} |
|
|
|
getShortSide(container) { |
|
return Math.min(container.width, container.height); |
|
} |
|
|
|
renderLayout(container, layout) { |
|
container.innerHTML = ''; |
|
|
|
layout.forEach(rect => { |
|
const element = document.createElement('div'); |
|
element.className = `treemap-node ${rect.data.type}`; |
|
|
|
element.style.left = `${rect.x}px`; |
|
element.style.top = `${rect.y}px`; |
|
element.style.width = `${rect.width}px`; |
|
element.style.height = `${rect.height}px`; |
|
|
|
|
|
if (rect.width > 60 && rect.height > 20) { |
|
element.textContent = rect.data.name; |
|
} |
|
|
|
this.addTooltip(element, rect.data); |
|
container.appendChild(element); |
|
}); |
|
} |
|
|
|
addTooltip(element, data) { |
|
element.addEventListener('mouseenter', () => { |
|
const tooltipContent = this.createTooltipContent(data); |
|
this.tooltip.innerHTML = tooltipContent; |
|
this.tooltip.classList.add('visible'); |
|
}); |
|
|
|
element.addEventListener('mouseleave', () => { |
|
this.tooltip.classList.remove('visible'); |
|
}); |
|
} |
|
|
|
createTooltipContent(data) { |
|
let content = `<strong>${data.name}</strong><br>`; |
|
content += `Type: ${data.type}<br>`; |
|
content += `Size: ${this.formatFileSize(data.size)}<br>`; |
|
content += `Path: ${data.path}<br>`; |
|
|
|
if (data.type === 'file' && data.lastModified) { |
|
content += `Modified: ${new Date(data.lastModified).toLocaleDateString()}<br>`; |
|
} |
|
|
|
if (data.children) { |
|
content += `Items: ${data.children.length}`; |
|
} |
|
|
|
return content; |
|
} |
|
|
|
formatFileSize(bytes) { |
|
if (bytes === 0) return '0 B'; |
|
|
|
const k = 1024; |
|
const sizes = ['B', 'KB', 'MB', 'GB', 'TB']; |
|
const i = Math.floor(Math.log(bytes) / Math.log(k)); |
|
|
|
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]; |
|
} |
|
|
|
showLoading() { |
|
this.visualizationArea.innerHTML = ` |
|
<div class="loading"> |
|
<div class="loading-spinner"></div> |
|
<h3>📊 Processing Directory Structure</h3> |
|
<p>Analyzing files and building treemap visualization...</p> |
|
</div> |
|
`; |
|
} |
|
|
|
showError(message) { |
|
this.visualizationArea.innerHTML = ` |
|
<div style="text-align: center; padding: 60px; color: #e74c3c;"> |
|
<h3>❌ Error</h3> |
|
<p>${message}</p> |
|
</div> |
|
`; |
|
} |
|
} |
|
|
|
|
|
const app = new SquarifiedTreemapExplorer(); |
|
|
|
|
|
function selectFolder() { |
|
app.selectFolder(); |
|
} |
|
|
|
function generateDemoData() { |
|
app.generateDemoData(); |
|
} |
|
</script> |
|
</body> |
|
</html> |