File size: 17,299 Bytes
00e9c56
3a8fe4b
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
96388b4
 
 
 
 
 
 
 
 
 
 
 
3a8fe4b
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
00e9c56
3a8fe4b
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
<!DOCTYPE html>
<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>