A-Legacy-in-Metal / index.html.v1
awacke1's picture
Create index.html.v1
f4aa6b0 verified
<!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';
});
// 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>