Spaces:
Running
Running
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>Python Code Structure Visualizer</title> | |
| <script src="https://cdn.tailwindcss.com"></script> | |
| <script src="https://d3js.org/d3.v7.min.js"></script> | |
| <link rel="preconnect" href="https://fonts.googleapis.com"> | |
| <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin> | |
| <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=Fira+Code:wght@400;500&display=swap" rel="stylesheet"> | |
| <style> | |
| body { | |
| font-family: 'Inter', sans-serif; | |
| } | |
| .fira-code { | |
| font-family: 'Fira Code', monospace; | |
| } | |
| /* Custom styles for D3 graph */ | |
| .graph-container { | |
| width: 100%; | |
| height: 100%; | |
| min-height: 500px; | |
| cursor: grab; | |
| } | |
| .graph-container:active { | |
| cursor: grabbing; | |
| } | |
| .node circle { | |
| stroke: #fff; | |
| stroke-width: 1.5px; | |
| } | |
| .node text { | |
| font-size: 10px; | |
| font-family: 'Fira Code', monospace; | |
| paint-order: stroke; | |
| stroke: #111827; /* Match dark background */ | |
| stroke-width: 3px; | |
| stroke-linecap: butt; | |
| stroke-linejoin: miter; | |
| pointer-events: none; | |
| } | |
| .link { | |
| stroke-opacity: 0.6; | |
| } | |
| .link.inheritance { | |
| stroke-dasharray: 5, 5; | |
| stroke: #60a5fa; /* blue-400 */ | |
| } | |
| .link.method { | |
| stroke: #4b5563; /* gray-600 */ | |
| } | |
| .node.selected > circle { | |
| stroke: #facc15; /* yellow-400 */ | |
| stroke-width: 3px; | |
| } | |
| /* Tab styling */ | |
| .tab.active { | |
| background-color: #3b82f6; /* blue-500 */ | |
| color: white; | |
| } | |
| </style> | |
| </head> | |
| <body class="bg-gray-900 text-gray-200 flex flex-col h-screen"> | |
| <header class="bg-gray-800/50 backdrop-blur-sm border-b border-gray-700 p-4 shadow-lg"> | |
| <h1 class="text-2xl font-bold text-center text-white">Bloatedness Visualizer</h1> | |
| <p class="text-center text-gray-400 mt-1">Paste your model in the 'Main' tab and add dependencies in other tabs.</p> | |
| </header> | |
| <main class="flex-grow flex flex-col md:flex-row gap-4 p-4 overflow-hidden"> | |
| <!-- Left Panel: Code Input & Controls --> | |
| <div class="md:w-1/3 flex flex-col h-full"> | |
| <div class="flex-grow flex flex-col bg-gray-800 rounded-lg shadow-2xl border border-gray-700"> | |
| <div class="p-4 border-b border-gray-700 flex justify-between items-center"> | |
| <h2 class="text-lg font-semibold">Code Input</h2> | |
| <button id="visualize-btn" class="bg-blue-600 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded-md transition-colors duration-300"> | |
| Visualize | |
| </button> | |
| </div> | |
| <div class="border-b border-gray-700 bg-gray-900/50 px-2 pt-2 flex items-center gap-2"> | |
| <div id="tab-bar" class="flex gap-1"> | |
| <!-- Tabs will be dynamically inserted here --> | |
| </div> | |
| <button id="add-tab-btn" class="ml-auto bg-gray-600 hover:bg-gray-500 text-white font-bold h-8 w-8 rounded-full transition-colors duration-200">+</button> | |
| </div> | |
| <div id="code-inputs-container" class="flex-grow relative"> | |
| <!-- Textareas will be dynamically inserted here --> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Right Panel: Visualization & Details --> | |
| <div class="md:w-2/3 flex flex-col gap-4 h-full"> | |
| <div class="flex-grow bg-gray-800 rounded-lg shadow-2xl border border-gray-700 relative overflow-hidden"> | |
| <div id="graph-container" class="w-full h-full"></div> | |
| <div id="loading-spinner" class="absolute inset-0 bg-gray-800/50 flex items-center justify-center hidden z-10"> | |
| <svg class="animate-spin h-10 w-10 text-white" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"> | |
| <circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle> | |
| <path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path> | |
| </svg> | |
| </div> | |
| </div> | |
| <div id="details-panel" class="h-40 bg-gray-800 rounded-lg shadow-2xl border border-gray-700 p-4 flex flex-col"> | |
| <h3 class="text-lg font-semibold border-b border-gray-700 pb-2 mb-2">Details</h3> | |
| <div id="details-content" class="text-gray-400 fira-code overflow-y-auto"> | |
| <p>Click on a node in the graph to see its details. Scroll to zoom, drag to pan.</p> | |
| </div> | |
| </div> | |
| </div> | |
| </main> | |
| <script> | |
| // --- DOM Element References --- | |
| const visualizeBtn = document.getElementById('visualize-btn'); | |
| const graphContainer = document.getElementById('graph-container'); | |
| const detailsContent = document.getElementById('details-content'); | |
| const loadingSpinner = document.getElementById('loading-spinner'); | |
| const tabBar = document.getElementById('tab-bar'); | |
| const codeInputsContainer = document.getElementById('code-inputs-container'); | |
| const addTabBtn = document.getElementById('add-tab-btn'); | |
| // --- Example Code --- | |
| fetch("main_code.py") | |
| .then(res => res.text()) | |
| .then(code => { | |
| const exampleCodeMain = code; | |
| // do something with it | |
| }); | |
| fetch("dependencies.py") | |
| .then(res_deps => res_deps.text()) | |
| .then(code_deps => { | |
| const exampleCodeDeps = code_deps; | |
| // do something with it | |
| }); | |
| // --- Tab Management --- | |
| let tabCounter = 0; | |
| function addTab(name, content = '', isActive = false) { | |
| tabCounter++; | |
| const tabId = `tab-${tabCounter}`; | |
| const textareaId = `textarea-${tabCounter}`; | |
| // Create Tab Button | |
| const tabButton = document.createElement('button'); | |
| tabButton.id = tabId; | |
| tabButton.className = 'tab px-4 py-2 text-sm font-medium rounded-t-md transition-colors duration-200'; | |
| tabButton.textContent = name; | |
| tabButton.dataset.textareaId = textareaId; | |
| tabBar.appendChild(tabButton); | |
| // Create Textarea | |
| const textarea = document.createElement('textarea'); | |
| textarea.id = textareaId; | |
| textarea.className = 'fira-code w-full h-full p-4 bg-gray-900 text-gray-300 resize-none focus:outline-none absolute top-0 left-0'; | |
| textarea.placeholder = `Paste dependency code here...`; | |
| textarea.value = content; | |
| codeInputsContainer.appendChild(textarea); | |
| tabButton.addEventListener('click', () => switchTab(tabId)); | |
| if (isActive) { | |
| switchTab(tabId); | |
| } else { | |
| textarea.classList.add('hidden'); | |
| } | |
| } | |
| function switchTab(tabId) { | |
| // Update tab buttons | |
| document.querySelectorAll('.tab').forEach(tab => { | |
| tab.classList.toggle('active', tab.id === tabId); | |
| }); | |
| // Update textareas | |
| document.querySelectorAll('#code-inputs-container textarea').forEach(area => { | |
| area.classList.toggle('hidden', area.id !== document.getElementById(tabId).dataset.textareaId); | |
| }); | |
| } | |
| addTabBtn.addEventListener('click', () => addTab(`Dep ${tabCounter}`)); | |
| // --- Core Logic: Parser --- | |
| function parsePythonCode(code) { | |
| const nodes = []; | |
| const links = []; | |
| const nodeRegistry = new Set(); | |
| let currentClassInfo = null; | |
| const lines = code.split('\n'); | |
| lines.forEach(line => { | |
| const indentation = line.match(/^\s*/)[0].length; | |
| if (line.trim().length > 0 && indentation === 0) { | |
| currentClassInfo = null; | |
| } | |
| const classMatch = /^\s*class\s+([\w\d_]+)\s*(?:\(([\w\d_,\s]+)\))?:/.exec(line); | |
| if (classMatch) { | |
| const className = classMatch[1]; | |
| const parents = classMatch[2] ? classMatch[2].split(',').map(p => p.trim()) : []; | |
| if (!nodeRegistry.has(className)) { | |
| nodes.push({ id: className, type: 'class', parents: parents }); | |
| nodeRegistry.add(className); | |
| } else { | |
| // If class was already created as an external placeholder, update it | |
| const existingNode = nodes.find(n => n.id === className); | |
| if (existingNode && existingNode.isExternal) { | |
| existingNode.isExternal = false; | |
| existingNode.parents = parents; | |
| } | |
| } | |
| currentClassInfo = { name: className, indentation: indentation }; | |
| parents.forEach(parent => { | |
| if (!nodeRegistry.has(parent)) { | |
| nodes.push({ id: parent, type: 'class', isExternal: true, parents: [] }); | |
| nodeRegistry.add(parent); | |
| } | |
| links.push({ source: className, target: parent, type: 'inheritance' }); | |
| }); | |
| } | |
| const methodMatch = /^\s+def\s+([\w\d_]+)\s*\(([^)]*)\)/.exec(line); | |
| if (currentClassInfo && methodMatch && indentation > currentClassInfo.indentation) { | |
| const methodName = methodMatch[1]; | |
| const signature = methodMatch[2]; | |
| const methodId = `${currentClassInfo.name}.${methodName}`; | |
| if (!nodeRegistry.has(methodId)) { | |
| nodes.push({ id: methodId, name: methodName, type: 'method', parentClass: currentClassInfo.name, signature: `(${signature})` }); | |
| nodeRegistry.add(methodId); | |
| links.push({ source: currentClassInfo.name, target: methodId, type: 'method' }); | |
| } | |
| } | |
| }); | |
| return { nodes, links }; | |
| } | |
| // --- Core Logic: D3 Visualization --- | |
| let simulation; | |
| function renderGraph(data) { | |
| graphContainer.innerHTML = ''; | |
| const width = graphContainer.clientWidth; | |
| const height = graphContainer.clientHeight; | |
| const svg = d3.select(graphContainer).append("svg") | |
| .attr("viewBox", [-width / 2, -height / 2, width, height]); | |
| const container = svg.append("g"); | |
| // Add zoom capabilities | |
| const zoom = d3.zoom() | |
| .scaleExtent([0.1, 4]) | |
| .on("zoom", (event) => { | |
| container.attr("transform", event.transform); | |
| }); | |
| svg.call(zoom); | |
| if (simulation) { | |
| simulation.stop(); | |
| } | |
| simulation = d3.forceSimulation(data.nodes) | |
| .force("link", d3.forceLink(data.links).id(d => d.id).distance(d => d.type === 'inheritance' ? 150 : 60).strength(0.5)) | |
| .force("charge", d3.forceManyBody().strength(-400)) | |
| .force("center", d3.forceCenter(0, 0)) | |
| .force("x", d3.forceX()) | |
| .force("y", d3.forceY()); | |
| const link = container.append("g") | |
| .selectAll("line") | |
| .data(data.links) | |
| .join("line") | |
| .attr("class", d => `link ${d.type}`); | |
| const node = container.append("g") | |
| .selectAll("g") | |
| .data(data.nodes) | |
| .join("g") | |
| .attr("class", "node") | |
| .call(drag(simulation)); | |
| node.append("circle") | |
| .attr("r", d => d.type === 'class' ? 15 : 8) | |
| .attr("fill", d => { | |
| if (d.type !== 'class') return '#9ca3af'; | |
| return d.isExternal ? '#be185d' : '#2563eb'; | |
| }); | |
| node.append("text") | |
| .text(d => d.type === 'class' ? d.id : d.name) | |
| .attr("x", d => d.type === 'class' ? 18 : 12) | |
| .attr("y", 3) | |
| .attr("fill", "#e5e7eb"); | |
| node.on("click", (event, d) => { | |
| event.stopPropagation(); // Prevent zoom from firing on node click | |
| updateDetailsPanel(d); | |
| node.classed("selected", n => n.id === d.id); | |
| }); | |
| simulation.on("tick", () => { | |
| link.attr("x1", d => d.source.x).attr("y1", d => d.source.y) | |
| .attr("x2", d => d.target.x).attr("y2", d => d.target.y); | |
| node.attr("transform", d => `translate(${d.x},${d.y})`); | |
| }); | |
| } | |
| // --- Interactivity --- | |
| function drag(simulation) { | |
| function dragstarted(event, d) { | |
| if (!event.active) simulation.alphaTarget(0.3).restart(); | |
| d.fx = d.x; | |
| d.fy = d.y; | |
| } | |
| function dragged(event, d) { | |
| d.fx = event.x; | |
| d.fy = event.y; | |
| } | |
| function dragended(event, d) { | |
| if (!event.active) simulation.alphaTarget(0); | |
| d.fx = null; | |
| d.fy = null; | |
| } | |
| return d3.drag() | |
| .on("start", dragstarted) | |
| .on("drag", dragged) | |
| .on("end", dragended); | |
| } | |
| // --- UI Updates --- | |
| function updateDetailsPanel(d) { | |
| let content = ''; | |
| if (d.type === 'class') { | |
| content = ` | |
| <p><span class="text-gray-100 font-semibold">Name:</span> ${d.id}</p> | |
| <p><span class="text-gray-100 font-semibold">Type:</span> ${d.isExternal ? 'External Class' : 'Class'}</p> | |
| <p><span class="text-gray-100 font-semibold">Inherits from:</span> ${d.parents && d.parents.length > 0 ? d.parents.join(', ') : 'None'}</p> | |
| ${d.isExternal ? '<p class="text-pink-400 mt-1">Note: This class was not defined in the provided code.</p>' : ''} | |
| `; | |
| } else if (d.type === 'method') { | |
| content = ` | |
| <p><span class="text-gray-100 font-semibold">Name:</span> ${d.name}</p> | |
| <p><span class="text-gray-100 font-semibold">Type:</span> Method</p> | |
| <p><span class="text-gray-100 font-semibold">Belongs to:</span> ${d.parentClass}</p> | |
| <p><span class="text-gray-100 font-semibold">Signature:</span> ${d.name}${d.signature}</p> | |
| `; | |
| } | |
| detailsContent.innerHTML = content; | |
| } | |
| function handleVisualize() { | |
| loadingSpinner.classList.remove('hidden'); | |
| setTimeout(() => { | |
| try { | |
| let allCode = ''; | |
| document.querySelectorAll('#code-inputs-container textarea').forEach(area => { | |
| allCode += area.value + '\n'; | |
| }); | |
| if (!allCode.trim()) { | |
| graphContainer.innerHTML = '<p class="p-4 text-center text-gray-400">Please paste some code to visualize.</p>'; | |
| return; | |
| } | |
| const graphData = parsePythonCode(allCode); | |
| renderGraph(graphData); | |
| } catch (error) { | |
| console.error("Failed to visualize code:", error); | |
| graphContainer.innerHTML = `<p class="p-4 text-center text-red-400">An error occurred during parsing. Check the console for details.</p>`; | |
| } finally { | |
| loadingSpinner.classList.add('hidden'); | |
| } | |
| }, 50); | |
| } | |
| // --- Event Listeners --- | |
| visualizeBtn.addEventListener('click', handleVisualize); | |
| // --- Initial Load --- | |
| window.addEventListener('load', () => { | |
| addTab('Main', exampleCodeMain, true); | |
| addTab('Deps', exampleCodeDeps); | |
| handleVisualize(); | |
| }); | |
| window.addEventListener('resize', () => { | |
| let allCode = ''; | |
| document.querySelectorAll('#code-inputs-container textarea').forEach(area => { | |
| allCode += area.value + '\n'; | |
| }); | |
| if (allCode.trim()) { | |
| handleVisualize(); | |
| } | |
| }); | |
| </script> | |
| </body> | |
| </html> | |