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> | |