| <!DOCTYPE html> |
| <html lang="en"> |
| <head> |
| <meta charset="UTF-8"> |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> |
| <title>Squarified Treemap - Folder Explorer</title> |
| <style> |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| body { |
| font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif; |
| margin: 0; |
| padding: 2rem; |
| background-color: #f4f4f9; |
| color: #333; |
| text-align: center; |
| display: flex; |
| flex-direction: column; |
| align-items: center; |
| justify-content: flex-start; |
| min-height: 100vh; |
| } |
| |
| h1 { |
| color: #2c3e50; |
| } |
| |
| p { |
| color: #555; |
| margin-bottom: 2rem; |
| max-width: 600px; |
| } |
| |
| |
| .folder-picker-label { |
| display: inline-block; |
| padding: 12px 24px; |
| background-color: #3498db; |
| color: white; |
| border-radius: 8px; |
| cursor: pointer; |
| font-weight: bold; |
| transition: background-color 0.3s ease, transform 0.2s ease; |
| } |
| |
| .folder-picker-label:hover { |
| background-color: #2980b9; |
| transform: translateY(-2px); |
| } |
| |
| #folder-picker { |
| display: none; |
| } |
| |
| |
| #treemap-container { |
| position: relative; |
| width: 90vw; |
| max-width: 1200px; |
| height: 75vh; |
| margin: 2rem auto; |
| border: 1px solid #ccc; |
| box-shadow: 0 4px 12px rgba(0,0,0,0.1); |
| background-color: #fff; |
| border-radius: 8px; |
| overflow: hidden; |
| } |
| |
| |
| .node-group { |
| position: absolute; |
| box-sizing: border-box; |
| overflow: hidden; |
| } |
| |
| |
| .internal { |
| border: 1px solid #aaa; |
| } |
| |
| |
| .leaf { |
| background-clip: padding-box; |
| box-shadow: inset 0px 0px 0px 1px rgba(255, 255, 255, 0.8); |
| transition: filter 0.2s ease-in-out; |
| display: flex; |
| align-items: flex-start; |
| justify-content: flex-start; |
| } |
| |
| .leaf:hover { |
| filter: brightness(1.15); |
| z-index: 10; |
| } |
| |
| |
| .node-label { |
| display: block; |
| padding: 2px 5px; |
| color: #fff; |
| background-color: rgba(0,0,0,0.4); |
| font-size: 12px; |
| font-weight: bold; |
| white-space: nowrap; |
| overflow: hidden; |
| text-overflow: ellipsis; |
| text-decoration: none; |
| cursor: pointer; |
| } |
| .node-label:hover { |
| background-color: rgba(0,0,0,0.6); |
| } |
| |
| |
| .leaf-label { |
| padding: 3px; |
| color: rgba(255, 255, 255, 0.95); |
| text-shadow: 1px 1px 2px rgba(0,0,0,0.7); |
| text-align: left; |
| font-size: 11px; |
| white-space: normal; |
| word-break: break-word; |
| pointer-events: none; |
| } |
| |
| |
| #tooltip { |
| position: fixed; |
| background-color: rgba(0, 0, 0, 0.85); |
| color: white; |
| padding: 8px 12px; |
| border-radius: 4px; |
| pointer-events: none; |
| opacity: 0; |
| transition: opacity 0.2s; |
| font-size: 14px; |
| z-index: 1001; |
| transform: translate(15px, 10px); |
| } |
| |
| |
| #context-menu { |
| position: fixed; |
| display: none; |
| background-color: #ecf0f1; |
| border: 1px solid #bdc3c7; |
| box-shadow: 0 2px 10px rgba(0,0,0,0.2); |
| border-radius: 5px; |
| padding: 5px 0; |
| z-index: 1000; |
| } |
| |
| #context-menu button { |
| display: block; |
| width: 100%; |
| padding: 8px 20px; |
| border: none; |
| background: none; |
| text-align: left; |
| cursor: pointer; |
| font-size: 14px; |
| } |
| |
| #context-menu button:hover { |
| background-color: #3498db; |
| color: white; |
| } |
| |
| |
| .modal-overlay { |
| position: fixed; |
| top: 0; |
| left: 0; |
| width: 100%; |
| height: 100%; |
| background: rgba(0,0,0,0.5); |
| display: none; |
| align-items: center; |
| justify-content: center; |
| z-index: 2000; |
| } |
| .modal-content { |
| background: white; |
| padding: 20px; |
| border-radius: 8px; |
| text-align: center; |
| box-shadow: 0 5px 15px rgba(0,0,0,0.3); |
| } |
| .modal-content p { |
| margin-bottom: 20px; |
| } |
| .modal-content button { |
| padding: 10px 20px; |
| border: none; |
| border-radius: 5px; |
| cursor: pointer; |
| margin: 0 10px; |
| } |
| #modal-confirm { |
| background-color: #e74c3c; |
| color: white; |
| } |
| #modal-cancel { |
| background-color: #bdc3c7; |
| } |
| |
| |
| #references { |
| max-width: 800px; |
| margin: 2rem auto; |
| text-align: left; |
| } |
| |
| #references h2 { |
| color: #2c3e50; |
| font-size: 1.5rem; |
| margin-bottom: 1rem; |
| } |
| |
| #references ul { |
| list-style-type: none; |
| padding: 0; |
| } |
| |
| #references li { |
| margin-bottom: 1rem; |
| color: #555; |
| } |
| |
| #references a { |
| color: #3498db; |
| text-decoration: none; |
| transition: color 0.2s ease; |
| } |
| |
| #references a:hover { |
| color: #2980b9; |
| text-decoration: underline; |
| } |
| |
| </style> |
| </head> |
| <body> |
|
|
| <h1>Squarified Treemap Folder Explorer 🗺️</h1> |
| <p>Select a folder to visualize its contents. Hover over files or directories for info. Right-click on a file for options.</p> |
|
|
| <label for="folder-picker" class="folder-picker-label">Choose a Folder</label> |
| <input type="file" id="folder-picker" webkitdirectory directory multiple /> |
|
|
| <div id="treemap-container"></div> |
| <div id="tooltip"></div> |
|
|
| |
| <div id="context-menu"> |
| <button id="menu-copy-path">Copy Path</button> |
| <button id="menu-delete">Delete from View</button> |
| </div> |
|
|
| |
| <div id="delete-modal" class="modal-overlay"> |
| <div class="modal-content"> |
| <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> |
| <button id="modal-confirm">Yes, Remove</button> |
| <button id="modal-cancel">Cancel</button> |
| </div> |
| </div> |
|
|
| |
| <div id="references"></div> |
|
|
| |
| <script src="https://d3js.org/d3.v7.min.js"></script> |
|
|
| |
| <script> |
| document.addEventListener('DOMContentLoaded', () => { |
| const folderPicker = document.getElementById('folder-picker'); |
| const treemapContainer = document.getElementById('treemap-container'); |
| const tooltip = document.getElementById('tooltip'); |
| const contextMenu = document.getElementById('context-menu'); |
| const deleteModal = document.getElementById('delete-modal'); |
| const referencesContainer = document.getElementById('references'); |
| |
| let currentFileTree = null; |
| let nodeToDelete = null; |
| |
| |
| const references = [ |
| { |
| title: "A heuristic extending the Squarified treemapping algorithm", |
| links: [ |
| { text: "Abstract", url: "https://arxiv.org/abs/1609.00754" }, |
| { text: "PDF", url: "https://arxiv.org/pdf/1609.00754" } |
| ] |
| }, |
| { |
| title: "Squarified Treemaps", |
| links: [{ text: "PDF", url: "https://vanwijk.win.tue.nl/stm.pdf" }] |
| }, |
| { |
| title: "Treemaps with Bounded Aspect Ratio", |
| links: [ |
| { text: "Abstract", url: "https://arxiv.org/abs/1012.1749" }, |
| { text: "PDF", url: "https://arxiv.org/pdf/1012.1749" } |
| ] |
| }, |
| { |
| title: "Interactive Visualisation of Hierarchical Quantitative Data: an Evaluation", |
| links: [ |
| { text: "Abstract", url: "https://www.arxiv.org/abs/1908.01277v1" }, |
| { text: "PDF", url: "https://www.arxiv.org/pdf/1908.01277v1" } |
| ] |
| }, |
| { |
| title: "Treemapping - Wikipedia", |
| links: [{ text: "Article", url: "https://en.wikipedia.org/wiki/Treemapping" }] |
| }, |
| { |
| title: "A Novel Algorithm for Real-time Procedural Generation of Building Floor Plans", |
| links: [{ text: "HTML", url: "https://ar5iv.labs.arxiv.org/html/1211.5842" }] |
| }, |
| { |
| title: "Fat Polygonal Partitions with Applications to Visualization and Embeddings", |
| links: [ |
| { text: "Abstract", url: "https://arxiv.org/abs/1009.1866" }, |
| { text: "PDF", url: "https://arxiv.org/pdf/1009.1866" } |
| ] |
| }, |
| { |
| title: "Tiling heuristics and evaluation metrics for treemaps with a target node aspect ratio", |
| links: [{ text: "PDF", url: "https://www.diva-portal.org/smash/get/diva2:1129639/FULLTEXT01.pdf" }] |
| } |
| ]; |
| |
| |
| function renderReferences() { |
| referencesContainer.innerHTML = ` |
| <h2>References</h2> |
| <ul> |
| ${references.map(ref => ` |
| <li> |
| ${ref.title}: |
| ${ref.links.map(link => `<a href="${link.url}" target="_blank">${link.text}</a>`).join(' | ')} |
| </li> |
| `).join('')} |
| </ul> |
| `; |
| } |
| |
| renderReferences(); |
| |
| |
| |
| |
| function handleFileSelect(event) { |
| const files = event.target.files; |
| if (files.length === 0) { |
| treemapContainer.innerHTML = '<p style="padding: 2rem;">No files selected or folder is empty.</p>'; |
| return; |
| } |
| currentFileTree = buildFileTree(files); |
| renderTreemap(currentFileTree); |
| } |
| |
| |
| |
| |
| |
| function buildFileTree(files) { |
| const root = { name: "root", path: "", children: [] }; |
| for (const file of files) { |
| if (file.size === 0) continue; |
| const pathParts = file.webkitRelativePath.split('/'); |
| let currentNode = root; |
| let currentPath = ''; |
| |
| for (let i = 0; i < pathParts.length; i++) { |
| const part = pathParts[i]; |
| |
| currentPath = currentPath ? `${currentPath}/${part}` : part; |
| |
| if (i === pathParts.length - 1) { |
| currentNode.children.push({ name: part, value: file.size, path: currentPath }); |
| } else { |
| let dirNode = currentNode.children.find(child => child.name === part && child.children); |
| if (!dirNode) { |
| dirNode = { name: part, children: [], path: currentPath }; |
| currentNode.children.push(dirNode); |
| } |
| currentNode = dirNode; |
| } |
| } |
| } |
| return root; |
| } |
| |
| |
| |
| |
| function renderTreemap(data) { |
| treemapContainer.innerHTML = ''; |
| const width = treemapContainer.clientWidth; |
| const height = treemapContainer.clientHeight; |
| |
| const root = d3.hierarchy(data).sum(d => d.value).sort((a, b) => b.value - a.value); |
| |
| const treemapLayout = d3.treemap() |
| .size([width, height]) |
| .paddingInner(1) |
| .paddingOuter(3) |
| .paddingTop(20) |
| .tile(d3.treemapSquarify); |
| |
| treemapLayout(root); |
| const color = d3.scaleOrdinal(d3.schemeCategory10); |
| |
| const node = d3.select('#treemap-container') |
| .selectAll('div') |
| .data(root.descendants()) |
| .join('div') |
| .attr('class', d => `node-group ${d.children ? 'internal' : 'leaf'}`) |
| .style('left', d => `${d.x0}px`) |
| .style('top', d => `${d.y0}px`) |
| .style('width', d => `${d.x1 - d.x0}px`) |
| .style('height', d => `${d.y1 - d.y0}px`); |
| |
| |
| const leaves = node.filter(d => !d.children); |
| |
| leaves.style('background-color', d => { |
| let ancestor = d; |
| while (ancestor.depth > 1) { ancestor = ancestor.parent; } |
| return color(ancestor.data.name); |
| }) |
| .on('contextmenu', (event, d) => { |
| event.preventDefault(); |
| event.stopPropagation(); |
| nodeToDelete = d; |
| contextMenu.style.display = 'block'; |
| contextMenu.style.left = `${event.clientX}px`; |
| contextMenu.style.top = `${event.clientY}px`; |
| }); |
| |
| |
| leaves.append('div') |
| .attr('class', 'leaf-label') |
| .text(d => d.data.name); |
| |
| |
| leaves.on('mouseenter', (event, d) => { |
| tooltip.style.opacity = 1; |
| tooltip.innerHTML = ` |
| <strong>File:</strong> ${d.data.name}<br> |
| <strong>Path:</strong> ${d.data.path}<br> |
| <strong>Size:</strong> ${formatBytes(d.value)} |
| `; |
| }) |
| .on('mousemove', (event) => { |
| tooltip.style.left = `${event.clientX}px`; |
| tooltip.style.top = `${event.clientY}px`; |
| }) |
| .on('mouseleave', () => { |
| tooltip.style.opacity = 0; |
| }); |
| |
| |
| const directories = node.filter(d => d.children); |
| |
| |
| directories.append('a') |
| .attr('class', 'node-label') |
| .attr('href', d => `file:///${d.data.path ? d.data.path.replace(/\//g, '\\') : ''}`) |
| .on('click', event => event.preventDefault()) |
| .text(d => d.data.name); |
| |
| |
| directories.on('mouseenter', (event, d) => { |
| tooltip.style.opacity = 1; |
| tooltip.innerHTML = ` |
| <strong>Directory:</strong> ${d.data.path}<br> |
| <strong>Total Size:</strong> ${formatBytes(d.value)} |
| `; |
| }) |
| .on('mousemove', (event) => { |
| tooltip.style.left = `${event.clientX}px`; |
| tooltip.style.top = `${event.clientY}px`; |
| }) |
| .on('mouseleave', () => { |
| tooltip.style.opacity = 0; |
| }); |
| } |
| |
| |
| folderPicker.addEventListener('change', handleFileSelect); |
| |
| |
| window.addEventListener('click', () => { |
| contextMenu.style.display = 'none'; |
| }); |
| |
| |
| document.getElementById('menu-copy-path').addEventListener('click', () => { |
| if (nodeToDelete && navigator.clipboard) { |
| navigator.clipboard.writeText(nodeToDelete.data.path).catch(err => console.error('Failed to copy path: ', err)); |
| } |
| }); |
| |
| document.getElementById('menu-delete').addEventListener('click', () => { |
| if (nodeToDelete) { |
| deleteModal.style.display = 'flex'; |
| } |
| }); |
| |
| |
| document.getElementById('modal-cancel').addEventListener('click', () => { |
| deleteModal.style.display = 'none'; |
| nodeToDelete = null; |
| }); |
| |
| document.getElementById('modal-confirm').addEventListener('click', () => { |
| if (nodeToDelete && nodeToDelete.parent) { |
| |
| const children = nodeToDelete.parent.data.children; |
| const index = children.findIndex(child => child.path === nodeToDelete.data.path); |
| if (index > -1) { |
| children.splice(index, 1); |
| } |
| renderTreemap(currentFileTree); |
| } |
| deleteModal.style.display = 'none'; |
| nodeToDelete = null; |
| }); |
| |
| |
| window.addEventListener('resize', () => { |
| if (currentFileTree) { |
| renderTreemap(currentFileTree); |
| } |
| }); |
| |
| |
| |
| |
| function formatBytes(bytes) { |
| if (bytes === 0) return '0 Bytes'; |
| const k = 1024; |
| const sizes = ['Bytes', '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]}`; |
| } |
| }); |
| </script> |
|
|
| </body> |
| </html> |