Spaces:
Running
Running
| <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'; | |
| }); | |
| // Listen for file input changes | |
| this.folderInput.addEventListener('change', (e) => { | |
| if (e.target.files.length > 0) { | |
| this.processFiles(e.target.files); | |
| } | |
| }); | |
| } | |
| async selectFolder() { | |
| try { | |
| // Try modern File System Access API first | |
| if ('showDirectoryPicker' in window && window.location.protocol === 'https:') { | |
| const directoryHandle = await window.showDirectoryPicker(); | |
| await this.processDirectory(directoryHandle); | |
| } else { | |
| // Fall back to file input | |
| 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); | |
| // Sort files by path to ensure directories are created before their contents | |
| 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 = ''; | |
| // Create directory structure | |
| 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); | |
| } | |
| } | |
| // Add file | |
| 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); | |
| } | |
| // Calculate directory sizes and sort children | |
| 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); | |
| } | |
| } | |
| // Sort children by size (descending) for better treemap layout | |
| 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 = ''; | |
| // Create main treemap | |
| this.createTreemapContainer(fileTree, 'Root Directory', 0); | |
| // Create treemaps for major subdirectories | |
| if (fileTree.children) { | |
| const majorFolders = fileTree.children | |
| .filter(child => child.type === 'directory' && child.children && child.children.length > 0) | |
| .slice(0, 5); // Show top 5 subdirectories | |
| 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); | |
| // Generate squarified treemap layout | |
| 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; | |
| // Scale areas to fit container | |
| 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`; | |
| // Show name only if rectangle is large enough | |
| 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> | |
| `; | |
| } | |
| } | |
| // Initialize the application | |
| const app = new SquarifiedTreemapExplorer(); | |
| // Global functions for button clicks | |
| function selectFolder() { | |
| app.selectFolder(); | |
| } | |
| function generateDemoData() { | |
| app.generateDemoData(); | |
| } | |
| </script> | |
| </body> | |
| </html> |