pycallgraph-visualizer 1.0.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,1523 @@
1
+ """
2
+ analyzer_local.py - Analyze local Python directories
3
+ """
4
+
5
+ import ast
6
+ import os
7
+ from collections import defaultdict
8
+ from pathlib import Path
9
+ from pyvis.network import Network
10
+ import re as regex
11
+
12
+ IGNORE_DIRS = {"venv", "env", ".venv", "__pycache__", ".git", "site-packages",
13
+ "node_modules", ".pytest_cache", "dist", "build", ".tox", ".mypy_cache"}
14
+ BUILTINS = set(dir(__builtins__))
15
+
16
+ def should_ignore(path: str) -> bool:
17
+ """Check if path should be ignored"""
18
+ parts = set(Path(path).parts)
19
+ return not parts.isdisjoint(IGNORE_DIRS)
20
+
21
+ def find_all_cycles(graph):
22
+ """Find all cycles in the call graph using DFS"""
23
+ cycles = []
24
+ visited = set()
25
+ rec_stack = []
26
+
27
+ def dfs(node):
28
+ if node in rec_stack:
29
+ cycle_start = rec_stack.index(node)
30
+ cycle = rec_stack[cycle_start:] + [node]
31
+ min_idx = cycle.index(min(cycle[:-1]))
32
+ normalized = tuple(cycle[min_idx:-1] + cycle[:min_idx] + [cycle[min_idx]])
33
+ if normalized not in cycles:
34
+ cycles.append(normalized)
35
+ return
36
+
37
+ if node in visited:
38
+ return
39
+
40
+ visited.add(node)
41
+ rec_stack.append(node)
42
+
43
+ for neighbor in graph.get(node, []):
44
+ dfs(neighbor)
45
+
46
+ rec_stack.pop()
47
+
48
+ for node in graph.keys():
49
+ dfs(node)
50
+
51
+ return cycles
52
+
53
+ def calculate_complexity(func_node):
54
+ """Calculate cyclomatic complexity"""
55
+ complexity = 1
56
+ for node in ast.walk(func_node):
57
+ if isinstance(node, (ast.If, ast.While, ast.For, ast.ExceptHandler)):
58
+ complexity += 1
59
+ elif isinstance(node, ast.BoolOp):
60
+ complexity += len(node.values) - 1
61
+ elif isinstance(node, (ast.ListComp, ast.DictComp, ast.SetComp, ast.GeneratorExp)):
62
+ complexity += 1
63
+ return complexity
64
+
65
+ def count_lines(func_node):
66
+ """Count lines of code"""
67
+ if hasattr(func_node, 'end_lineno') and hasattr(func_node, 'lineno'):
68
+ return func_node.end_lineno - func_node.lineno + 1
69
+ return 0
70
+
71
+ def analyze_directory(directory_path: str) -> dict:
72
+ """Analyze a local directory and return structured data"""
73
+
74
+ print(f"📁 Analyzing directory: {directory_path}")
75
+
76
+ function_files = {}
77
+ functions_ast = defaultdict(dict)
78
+
79
+ # Step 1: Collect functions
80
+ python_files_found = 0
81
+ for root, dirs, files in os.walk(directory_path):
82
+ # Remove ignored directories from traversal
83
+ dirs[:] = [d for d in dirs if not should_ignore(os.path.join(root, d))]
84
+
85
+ for f in files:
86
+ if f.endswith(".py"):
87
+ python_files_found += 1
88
+ path = os.path.join(root, f)
89
+
90
+ if should_ignore(path):
91
+ continue
92
+
93
+ # Get relative path for better display
94
+ try:
95
+ rel_path = os.path.relpath(path, directory_path)
96
+ except ValueError:
97
+ # On Windows, relpath fails if paths are on different drives
98
+ rel_path = path
99
+
100
+ with open(path, "r", encoding="utf8", errors="ignore") as src:
101
+ try:
102
+ tree = ast.parse(src.read(), filename=path)
103
+ except SyntaxError as e:
104
+ print(f" ⚠️ Syntax error in {rel_path}: {e}")
105
+ continue
106
+ except Exception as e:
107
+ print(f" ⚠️ Error parsing {rel_path}: {e}")
108
+ continue
109
+
110
+ for node in ast.walk(tree):
111
+ if isinstance(node, ast.FunctionDef):
112
+ function_files[node.name] = rel_path
113
+ functions_ast[rel_path][node.name] = node
114
+
115
+ print(f" Found {python_files_found} Python files")
116
+ print(f" Collected {len(function_files)} functions from {len(functions_ast)} files")
117
+
118
+ if len(function_files) == 0:
119
+ print(" ⚠️ No functions found!")
120
+ return {
121
+ "function_files": {},
122
+ "functions_ast": {},
123
+ "call_graph": {},
124
+ "circular_dependencies": [],
125
+ "functions_in_cycles": [],
126
+ "dead_functions": [],
127
+ "entry_points": [],
128
+ "complexity_metrics": {},
129
+ "high_complexity_functions": {},
130
+ "node_connections": {},
131
+ "total_functions": 0,
132
+ "total_files": 0,
133
+ }
134
+
135
+ # Step 2: Build call graph
136
+ call_graph = defaultdict(set)
137
+ for file_name, funcs in functions_ast.items():
138
+ for func_name, func_node in funcs.items():
139
+ for node in ast.walk(func_node):
140
+ if isinstance(node, ast.Call) and isinstance(node.func, ast.Name):
141
+ callee = node.func.id
142
+ if callee in function_files and callee not in BUILTINS:
143
+ call_graph[func_name].add(callee)
144
+
145
+ print(f" Built call graph with {len(call_graph)} edges")
146
+
147
+ # Step 3: Detect circular dependencies
148
+ circular_dependencies = find_all_cycles(call_graph)
149
+ functions_in_cycles = set()
150
+ for cycle in circular_dependencies:
151
+ functions_in_cycles.update(cycle)
152
+
153
+ print(f" Found {len(circular_dependencies)} circular dependencies")
154
+
155
+ # Step 4: Detect dead code and entry points
156
+ all_callees = set()
157
+ for callees in call_graph.values():
158
+ all_callees.update(callees)
159
+
160
+ never_called = set(function_files.keys()) - all_callees
161
+ dead_functions = set()
162
+ for func in never_called:
163
+ if func not in call_graph or len(call_graph[func]) == 0:
164
+ dead_functions.add(func)
165
+
166
+ entry_points = never_called - dead_functions
167
+
168
+ print(f" Dead functions: {len(dead_functions)}")
169
+ print(f" Entry points: {len(entry_points)}")
170
+
171
+ # Step 5: Calculate complexity metrics
172
+ complexity_metrics = {}
173
+ high_complexity_functions = {}
174
+
175
+ for file_name, funcs in functions_ast.items():
176
+ for func_name, func_node in funcs.items():
177
+ complexity = calculate_complexity(func_node)
178
+ loc = count_lines(func_node)
179
+ complexity_metrics[func_name] = {
180
+ 'complexity': complexity,
181
+ 'loc': loc,
182
+ 'file': file_name
183
+ }
184
+ if complexity > 10:
185
+ high_complexity_functions[func_name] = complexity_metrics[func_name]
186
+
187
+ print(f" High complexity functions: {len(high_complexity_functions)}")
188
+
189
+ # Calculate node connections
190
+ node_connections = defaultdict(int)
191
+ for caller, callees in call_graph.items():
192
+ node_connections[caller] += len(callees)
193
+ for callee in callees:
194
+ node_connections[callee] += 1
195
+
196
+ print("✅ Analysis complete!")
197
+
198
+ return {
199
+ "function_files": function_files,
200
+ "functions_ast": functions_ast,
201
+ "call_graph": {k: list(v) for k, v in call_graph.items()},
202
+ "circular_dependencies": [list(cycle) for cycle in circular_dependencies],
203
+ "functions_in_cycles": list(functions_in_cycles),
204
+ "dead_functions": list(dead_functions),
205
+ "entry_points": list(entry_points),
206
+ "complexity_metrics": complexity_metrics,
207
+ "high_complexity_functions": high_complexity_functions,
208
+ "node_connections": dict(node_connections),
209
+ "total_functions": len(function_files),
210
+ "total_files": len(functions_ast),
211
+ }
212
+
213
+ def generate_html_graph(analysis_result: dict, output_path: str, directory_path: str):
214
+ """Generate interactive HTML visualization"""
215
+
216
+ function_files = analysis_result["function_files"]
217
+ call_graph = analysis_result["call_graph"]
218
+ functions_in_cycles = set(analysis_result["functions_in_cycles"])
219
+ dead_functions = set(analysis_result["dead_functions"])
220
+ entry_points = set(analysis_result["entry_points"])
221
+ high_complexity_functions = analysis_result["high_complexity_functions"]
222
+ complexity_metrics = analysis_result["complexity_metrics"]
223
+ node_connections = analysis_result["node_connections"]
224
+ functions_ast = analysis_result["functions_ast"]
225
+ circular_dependencies = analysis_result["circular_dependencies"]
226
+
227
+ # Create network
228
+ net = Network(height="100%", width="100%", directed=True, bgcolor="#ffffff")
229
+
230
+ net.force_atlas_2based(
231
+ gravity=-100,
232
+ central_gravity=0.05,
233
+ spring_length=150,
234
+ spring_strength=0.1,
235
+ damping=0.5,
236
+ overlap=0.5
237
+ )
238
+
239
+ net.set_options("""
240
+ var options = {
241
+ "physics": {
242
+ "enabled": true,
243
+ "stabilization": {"enabled": true, "iterations": 200},
244
+ "forceAtlas2Based": {
245
+ "gravitationalConstant": -80,
246
+ "centralGravity": 0.015,
247
+ "springLength": 250,
248
+ "springConstant": 0.05,
249
+ "damping": 0.5,
250
+ "avoidOverlap": 1
251
+ },
252
+ "solver": "forceAtlas2Based"
253
+ },
254
+ "nodes": {"shape": "box", "font": {"size": 16}, "borderWidth": 2, "margin": 10},
255
+ "edges": {"arrows": {"to": {"enabled": true, "scaleFactor": 0.5}}, "smooth": {"enabled": true, "type": "continuous"}, "width": 2},
256
+ "interaction": {"hover": true, "dragNodes": true, "dragView": true, "zoomView": true, "navigationButtons": true}
257
+ }
258
+ """)
259
+
260
+ # Generate colors for files
261
+ color_palette = ["#8dd3c7", "#ffffb3", "#bebada", "#fb8072", "#80b1d3",
262
+ "#fdb462", "#b3de69", "#fccde5", "#d9d9d9", "#bc80bd"]
263
+ file_colors = {}
264
+ unique_files = list(set(function_files.values()))
265
+ for i, file_name in enumerate(unique_files):
266
+ file_colors[file_name] = color_palette[i % len(color_palette)]
267
+
268
+ # Add nodes
269
+ nodes_added = set()
270
+ for caller, callees in call_graph.items():
271
+ if caller not in function_files:
272
+ continue
273
+
274
+ caller_file = function_files[caller]
275
+ caller_id = f"{caller} ({caller_file})"
276
+
277
+ if caller_id not in nodes_added:
278
+ caller_complexity = complexity_metrics.get(caller, {}).get('complexity', 0)
279
+ caller_loc = complexity_metrics.get(caller, {}).get('loc', 0)
280
+ is_complex = caller in high_complexity_functions
281
+
282
+ # Determine color and status
283
+ if caller in functions_in_cycles:
284
+ node_color = "#ff4444"
285
+ border_color = "#cc0000"
286
+ status = "⚠️ IN CIRCULAR DEPENDENCY!"
287
+ elif caller in dead_functions:
288
+ node_color = "#9e9e9e"
289
+ border_color = "#616161"
290
+ status = "💀 DEAD CODE"
291
+ elif is_complex:
292
+ node_color = "#ff9800"
293
+ border_color = "#f57c00"
294
+ status = f"📈 HIGH COMPLEXITY ({caller_complexity})"
295
+ elif caller in entry_points:
296
+ node_color = "#4caf50"
297
+ border_color = "#2e7d32"
298
+ status = "🚪 ENTRY POINT"
299
+ else:
300
+ node_color = file_colors.get(caller_file, "#cccccc")
301
+ border_color = node_color
302
+ status = ""
303
+
304
+ node_size = 20 + min(node_connections.get(caller, 0) * 5, 50)
305
+
306
+ net.add_node(
307
+ caller_id,
308
+ label=caller,
309
+ title=f"{caller}\\nFile: {caller_file}\\nComplexity: {caller_complexity}\\nLines: {caller_loc}\\n{status}",
310
+ color={'background': node_color, 'border': border_color},
311
+ borderWidth=3,
312
+ size=node_size
313
+ )
314
+ nodes_added.add(caller_id)
315
+
316
+ for callee in callees:
317
+ if callee not in function_files:
318
+ continue
319
+
320
+ callee_file = function_files[callee]
321
+ callee_id = f"{callee} ({callee_file})"
322
+
323
+ if callee_id not in nodes_added:
324
+ callee_complexity = complexity_metrics.get(callee, {}).get('complexity', 0)
325
+ callee_loc = complexity_metrics.get(callee, {}).get('loc', 0)
326
+ is_complex = callee in high_complexity_functions
327
+
328
+ if callee in functions_in_cycles:
329
+ node_color = "#ff4444"
330
+ border_color = "#cc0000"
331
+ status = "⚠️ IN CIRCULAR DEPENDENCY!"
332
+ elif callee in dead_functions:
333
+ node_color = "#9e9e9e"
334
+ border_color = "#616161"
335
+ status = "💀 DEAD CODE"
336
+ elif is_complex:
337
+ node_color = "#ff9800"
338
+ border_color = "#f57c00"
339
+ status = f"📈 HIGH COMPLEXITY ({callee_complexity})"
340
+ elif callee in entry_points:
341
+ node_color = "#4caf50"
342
+ border_color = "#2e7d32"
343
+ status = "🚪 ENTRY POINT"
344
+ else:
345
+ node_color = file_colors.get(callee_file, "#cccccc")
346
+ border_color = node_color
347
+ status = ""
348
+
349
+ node_size = 20 + min(node_connections.get(callee, 0) * 5, 50)
350
+
351
+ net.add_node(
352
+ callee_id,
353
+ label=callee,
354
+ title=f"{callee}\\nFile: {callee_file}\\nComplexity: {callee_complexity}\\nLines: {callee_loc}\\n{status}",
355
+ color={'background': node_color, 'border': border_color},
356
+ borderWidth=3,
357
+ size=node_size
358
+ )
359
+ nodes_added.add(callee_id)
360
+
361
+ # Check if edge is part of cycle
362
+ edge_color = None
363
+ edge_width = 2
364
+ for cycle in analysis_result["circular_dependencies"]:
365
+ # Check for the (caller -> callee) link within the cycle list
366
+ for j in range(len(cycle) - 1):
367
+ if cycle[j] == caller and cycle[j + 1] == callee:
368
+ edge_color = "#ff0000"
369
+ edge_width = 4
370
+ break
371
+ # Also check the closing link of the cycle (last -> first)
372
+ if cycle[-1] == caller and cycle[0] == callee:
373
+ edge_color = "#ff0000"
374
+ edge_width = 4
375
+ break
376
+
377
+ net.add_edge(caller_id, callee_id, color=edge_color, width=edge_width)
378
+
379
+ # Save to file
380
+ net.write_html(output_path)
381
+
382
+ with open(output_path, "r", encoding="utf8") as f:
383
+ html = f.read()
384
+
385
+ # --- HTML Elements for Sidebar ---
386
+
387
+ header = f"""
388
+ <div style="position: fixed; top: 10px; left: 10px; background: white; padding: 15px; border-radius: 10px; box-shadow: 0 2px 10px rgba(0,0,0,0.1); z-index: 1000; max-width: 300px;">
389
+ <h3 style="margin: 0 0 10px 0;">📊 Project Analysis</h3>
390
+ <p style="margin: 5px 0; font-size: 11px; word-break: break-all;"><strong>Path:</strong> {directory_path}</p>
391
+ <p style="margin: 5px 0; font-size: 12px;"><strong>Functions:</strong> {analysis_result['total_functions']}</p>
392
+ <p style="margin: 5px 0; font-size: 12px;"><strong>Files:</strong> {analysis_result['total_files']}</p>
393
+ <hr style="margin: 10px 0;">
394
+ <p style="margin: 5px 0; font-size: 12px; color: #ff4444;">🔴 Cycles: {len(analysis_result['circular_dependencies'])}</p>
395
+ <p style="margin: 5px 0; font-size: 12px; color: #9e9e9e;">💀 Dead: {len(analysis_result['dead_functions'])}</p>
396
+ <p style="margin: 5px 0; font-size: 12px; color: #ff9800;">📈 Complex: {len(analysis_result['high_complexity_functions'])}</p>
397
+ <p style="margin: 5px 0; font-size: 12px; color: #4caf50;">🚪 Entry: {len(analysis_result['entry_points'])}</p>
398
+ </div>
399
+ """
400
+
401
+ # Prepare search and view buttons
402
+ file_buttons_html = """
403
+ <div style="background:#fff; padding:8px; margin-bottom:10px; border-radius:4px; box-shadow:0 2px 4px rgba(0,0,0,0.1);">
404
+ <input type="text" id="search_box" placeholder="🔍 Search functions..."
405
+ style="width:100%; padding:8px; border:2px solid #ddd; border-radius:4px; font-size:14px; box-sizing:border-box;">
406
+ <div id="search_results" style="margin-top:5px; font-size:11px; color:#666;"></div>
407
+ </div>
408
+ """
409
+ file_buttons_html += """
410
+ <div style="background:#fff; padding:10px; margin-bottom:10px; border-radius:4px; box-shadow:0 2px 4px rgba(0,0,0,0.1);">
411
+ <h4 style="margin:0 0 8px 0; color:#333;">🎯 Analysis Mode</h4>
412
+ <div style="display:flex; gap:5px;">
413
+ <button id="mode_normal" style="flex:1; padding:8px; background:#4CAF50; color:white; font-weight:bold;">Normal</button>
414
+ <button id="mode_path" style="flex:1; padding:8px; background:#2196F3; color:white;">Path Finder</button>
415
+ <button id="mode_multi" style="flex:1; padding:8px; background:#FF9800; color:white;">Multi-Select</button>
416
+ </div>
417
+ <div id="mode_info" style="margin-top:8px; padding:6px; background:#e3f2fd; border-radius:3px; font-size:11px; min-height:40px;">
418
+ <strong>Normal Mode:</strong> Click nodes to explore
419
+ </div>
420
+ </div>
421
+ """
422
+ file_buttons_html += """
423
+ <div id="analysis_panel" style="display:none; background:#fff; padding:10px; margin-bottom:10px; border-radius:4px; box-shadow:0 2px 4px rgba(0,0,0,0.1);">
424
+ <div style="display:flex; justify-content:space-between; align-items:center; margin-bottom:8px;">
425
+ <h4 style="margin:0; color:#333;">📊 Analysis Results</h4>
426
+ <button id="clear_analysis" style="padding:4px 8px; background:#f44336; color:white; font-size:10px; border-radius:3px;">Clear</button>
427
+ </div>
428
+ <div id="analysis_content" style="font-size:11px; max-height:300px; overflow-y:auto;"></div>
429
+ </div>
430
+ """
431
+
432
+ file_buttons_html += '<button id="btn_show_all" style="margin:2px; width:100%; background:#4CAF50; color:white; font-weight:bold;">Show All</button><br>\n'
433
+ file_buttons_html += '<button id="btn_show_cycles" style="margin:2px; width:100%; background:#ff4444; color:white; font-weight:bold;">🔴 Show Cycles</button><br>\n'
434
+ file_buttons_html += '<button id="btn_show_dead" style="margin:2px; width:100%; background:#9e9e9e; color:white; font-weight:bold;">💀 Show Dead Code</button><br>\n'
435
+ file_buttons_html += '<button id="btn_show_complex" style="margin:2px; width:100%; background:#ff9800; color:white; font-weight:bold;">📈 Show Complex</button><br>\n'
436
+ file_buttons_html += '<button id="btn_show_entry" style="margin:2px; width:100%; background:#4caf50; color:white; font-weight:bold;">🚪 Show Entry Points</button><br>\n'
437
+ file_buttons_html += '<button id="btn_hide_cycles" style="margin:2px; width:100%; background:#ff9800; color:white; font-weight:bold;">Hide Cycles</button><br>\n'
438
+ file_buttons_html += '<button id="btn_mode_explode" style="margin:2px; width:100%; background:#9C27B0; color:white; font-weight:bold;">⚛️ Explode Mode</button><br>'
439
+ file_buttons_html += '<hr style="margin:10px 0;">\n'
440
+
441
+ # Prepare file toggle buttons
442
+ for file_name in functions_ast.keys():
443
+ # Use the SAME sanitization as in JavaScript
444
+ safe_id = file_name.replace('.', '_').replace('/', '__').replace('\\', '__').replace('-', '_').replace(' ', '_')
445
+ button_id = f"btn_{safe_id}"
446
+ file_buttons_html += f'<button id="{button_id}" style="margin:2px; width:100%;">{file_name}</button><br>\n'
447
+
448
+ # Prepare function focus buttons
449
+ function_buttons_html = '<button id="reset_focus" style="margin:2px; width:100%; background:#f44336; color:white; font-weight:bold;">Reset View</button><br>\n'
450
+ for func_name in sorted(function_files.keys()):
451
+ function_buttons_html += f'<button id="focus_{func_name}" style="margin:2px; width:100%; font-size:12px;">{func_name}</button><br>\n'
452
+
453
+ # Create cycle info block
454
+ cycle_info_html = ""
455
+ if circular_dependencies:
456
+ cycle_info_html = f"""
457
+ <div style="background:#ffebee; padding:10px; margin:10px 0; border-left:4px solid #f44336; border-radius:4px;">
458
+ <strong style="color:#c62828;">⚠️ {len(circular_dependencies)} Circular Dependencies</strong>
459
+ <div style="margin-top:8px; font-size:11px; max-height:120px; overflow-y:auto;">
460
+ """
461
+ for i, cycle in enumerate(circular_dependencies[:5], 1):
462
+ cycle_info_html += f"<div style='margin:4px 0; padding:4px; background:white; border-radius:2px;'>{i}. {' → '.join(list(cycle)[:3])}{'...' if len(cycle) > 3 else ''}</div>"
463
+ if len(circular_dependencies) > 5:
464
+ cycle_info_html += f"<div style='margin:4px 0; font-style:italic;'>...+{len(circular_dependencies) - 5} more</div>"
465
+ cycle_info_html += """
466
+ </div>
467
+ </div>
468
+ """
469
+ else:
470
+ cycle_info_html = """
471
+ <div style="background:#e8f5e9; padding:10px; margin:10px 0; border-left:4px solid #4caf50; border-radius:4px;">
472
+ <strong style="color:#2e7d32;">✅ No Circular Dependencies</strong>
473
+ </div>
474
+ """
475
+
476
+ # Dead code info - MAKE IT SCROLLABLE
477
+ dead_info_html = ""
478
+ if dead_functions:
479
+ dead_list_html = ""
480
+ for func in sorted(list(dead_functions)):
481
+ dead_list_html += f"<div style='margin:2px 0; padding:3px; background:white; border-radius:2px; font-size:10px;'>• {func}</div>"
482
+
483
+ dead_info_html = f"""
484
+ <div style="background:#f5f5f5; padding:10px; margin:10px 0; border-left:4px solid #9e9e9e; border-radius:4px;">
485
+ <div style="display:flex; justify-content:space-between; align-items:center;">
486
+ <strong style="color:#424242;">💀 {len(dead_functions)} Dead Functions</strong>
487
+ <button id="toggle_dead_list" style="padding:2px 6px; font-size:10px; cursor:pointer;">Show All</button>
488
+ </div>
489
+ <div style="font-size:10px; color:#666; margin-top:3px;">Never called, no connections</div>
490
+ <div id="dead_list" style="display:none; margin-top:8px; max-height:200px; overflow-y:auto; font-size:11px;">
491
+ {dead_list_html}
492
+ </div>
493
+ </div>
494
+ """
495
+
496
+ # Entry points info - MAKE IT SCROLLABLE
497
+ entry_info_html = ""
498
+ if entry_points:
499
+ entry_list_html = ""
500
+ for func in sorted(list(entry_points)):
501
+ num_calls = len(call_graph.get(func, []))
502
+ entry_list_html += f"<div style='margin:2px 0; padding:3px; background:white; border-radius:2px; font-size:10px;'>• {func} → {num_calls} call(s)</div>"
503
+
504
+ entry_info_html = f"""
505
+ <div style="background:#e8f5e9; padding:10px; margin:10px 0; border-left:4px solid #4caf50; border-radius:4px;">
506
+ <div style="display:flex; justify-content:space-between; align-items:center;">
507
+ <strong style="color:#2e7d32;">🚪 {len(entry_points)} Entry Points</strong>
508
+ <button id="toggle_entry_list" style="padding:2px 6px; font-size:10px; cursor:pointer;">Show All</button>
509
+ </div>
510
+ <div style="font-size:10px; color:#2e7d32; margin-top:3px;">API/Cron/Event handlers</div>
511
+ <div id="entry_list" style="display:none; margin-top:8px; max-height:200px; overflow-y:auto; font-size:11px;">
512
+ {entry_list_html}
513
+ </div>
514
+ </div>
515
+ """
516
+
517
+ # Complexity info - MAKE IT SCROLLABLE
518
+ complexity_info_html = ""
519
+ if high_complexity_functions:
520
+ sorted_complex = sorted(high_complexity_functions.items(),
521
+ key=lambda x: x[1]['complexity'],
522
+ reverse=True)
523
+ complex_list_html = ""
524
+ for func, metrics in sorted_complex:
525
+ complex_list_html += f"<div style='margin:2px 0; padding:3px; background:white; border-radius:2px; font-size:10px;'>• {func}: C={metrics['complexity']}, L={metrics['loc']}</div>"
526
+
527
+ complexity_info_html = f"""
528
+ <div style="background:#fff3e0; padding:10px; margin:10px 0; border-left:4px solid #ff9800; border-radius:4px;">
529
+ <div style="display:flex; justify-content:space-between; align-items:center;">
530
+ <strong style="color:#e65100;">📈 {len(high_complexity_functions)} Complex Functions</strong>
531
+ <button id="toggle_complex_list" style="padding:2px 6px; font-size:10px; cursor:pointer;">Show All</button>
532
+ </div>
533
+ <div style="font-size:10px; color:#e65100; margin-top:3px;">Complexity &gt; 10</div>
534
+ <div id="complex_list" style="display:none; margin-top:8px; max-height:200px; overflow-y:auto; font-size:11px;">
535
+ {complex_list_html}
536
+ </div>
537
+ </div>
538
+ """
539
+
540
+ # Create layout with sidebar on left
541
+ html = html.replace("<body>", f"""
542
+ <body style="margin:0; padding:0; overflow:hidden;">
543
+ <div style="display:flex; height:100vh; width:100vw;">
544
+ <div id="side-panel" style="width:320px; overflow-y:auto; background:#f5f5f5; padding:15px; border-right:3px solid #ddd; box-shadow: 2px 0 5px rgba(0,0,0,0.1);">
545
+ <h2 style="margin-top:0; color:#333; border-bottom:2px solid #333; padding-bottom:5px;">Call Graph Explorer</h2>
546
+
547
+ {cycle_info_html}
548
+ {dead_info_html}
549
+ {entry_info_html}
550
+ {complexity_info_html}
551
+
552
+ <h3 style="color:#555; margin-top:20px;">🔍 Search & Views</h3>
553
+ {file_buttons_html}
554
+ <hr style="margin:20px 0;">
555
+ <h3 style="color:#555;">📁 Files</h3>
556
+ {function_buttons_html}
557
+ </div>
558
+ <div id="graph" style="flex-grow:1; height:100%;"></div>
559
+ </div>
560
+ """)
561
+
562
+ # Find and move the card into graph container
563
+ card_pattern = r'<div class="card"[^>]*>.*?<div id="mynetwork"[^>]*>.*?</div>\s*</div>'
564
+ card_match = regex.search(card_pattern, html, regex.DOTALL)
565
+
566
+ if card_match:
567
+ card_html = card_match.group(0)
568
+ html = html.replace(card_html, '')
569
+ html = html.replace('<div id="graph" style="flex-grow:1; height:100%;"></div>',
570
+ f'<div id="graph" style="flex-grow:1; height:100%;">{card_html}</div>')
571
+
572
+ # Override CSS for proper layout
573
+ custom_css = """
574
+ <style>
575
+ body { margin: 0; padding: 0; overflow: hidden; }
576
+ #graph { display: flex; flex-direction: column; }
577
+ .card {
578
+ flex: 1 !important;
579
+ width: 100% !important;
580
+ height: 100% !important;
581
+ border: none !important;
582
+ border-radius: 0 !important;
583
+ margin: 0 !important;
584
+ display: flex !important;
585
+ flex-direction: column !important;
586
+ }
587
+ .card-body, #mynetwork {
588
+ flex: 1 !important;
589
+ width: 100% !important;
590
+ height: 100% !important;
591
+ padding: 0 !important;
592
+ margin: 0 !important;
593
+ }
594
+ button {
595
+ padding: 8px;
596
+ border: 1px solid #ccc;
597
+ border-radius: 4px;
598
+ background: white;
599
+ cursor: pointer;
600
+ transition: all 0.2s;
601
+ }
602
+ button:hover {
603
+ background: #e0e0e0;
604
+ transform: translateX(2px);
605
+ }
606
+ #side-panel h3 {
607
+ font-size: 14px;
608
+ font-weight: bold;
609
+ }
610
+ #search_box:focus {
611
+ outline: none;
612
+ border-color: #4CAF50;
613
+ box-shadow: 0 0 5px rgba(76, 175, 80, 0.3);
614
+ }
615
+ </style>
616
+ """
617
+ html = html.replace("</head>", f"{custom_css}</head>")
618
+
619
+ # --- Build JavaScript for interactivity (FIXED) ---
620
+ group_js = "<script>\n"
621
+
622
+ # Functions involved in cycles, dead code, and high complexity (Data preparation)
623
+ group_js += f"var functionsInCycles = new Set({list(functions_in_cycles)});\n"
624
+ group_js += f"var deadFunctions = new Set({list(dead_functions)});\n"
625
+ group_js += f"var entryPoints = new Set({list(entry_points)});\n"
626
+ group_js += f"var highComplexityFunctions = new Set({list(high_complexity_functions.keys())});\n"
627
+
628
+ # Core interactivity logic wrapped in DOMContentLoaded
629
+ # This prevents the "Cannot set properties of null (setting 'onclick')" error.
630
+ group_js += """
631
+ document.addEventListener("DOMContentLoaded", function () {
632
+ // 1. LIST TOGGLE BUTTONS (Dead, Entry, Complex)
633
+
634
+ // Dead List Toggle
635
+ const deadBtn = document.getElementById("toggle_dead_list");
636
+ const deadList = document.getElementById('dead_list');
637
+ if (deadBtn && deadList) {
638
+ deadBtn.onclick = function() {
639
+ if (deadList.style.display === 'none') {
640
+ deadList.style.display = 'block';
641
+ this.textContent = 'Hide';
642
+ } else {
643
+ deadList.style.display = 'none';
644
+ this.textContent = 'Show All';
645
+ }
646
+ };
647
+ }
648
+
649
+ // Entry List Toggle
650
+ const entryBtn = document.getElementById('toggle_entry_list');
651
+ const entryList = document.getElementById('entry_list');
652
+ if (entryBtn && entryList) {
653
+ entryBtn.onclick = function() {
654
+ if (entryList.style.display === 'none') {
655
+ entryList.style.display = 'block';
656
+ this.textContent = 'Hide';
657
+ } else {
658
+ entryList.style.display = 'none';
659
+ this.textContent = 'Show All';
660
+ }
661
+ };
662
+ }
663
+
664
+ // Complex List Toggle
665
+ const complexBtn = document.getElementById('toggle_complex_list');
666
+ const complexList = document.getElementById('complex_list');
667
+ if (complexBtn && complexList) {
668
+ complexBtn.onclick = function() {
669
+ if (complexList.style.display === 'none') {
670
+ complexList.style.display = 'block';
671
+ this.textContent = 'Hide';
672
+ } else {
673
+ complexList.style.display = 'none';
674
+ this.textContent = 'Show All';
675
+ }
676
+ };
677
+ }
678
+
679
+ // Check if network object is globally available (standard for pyvis output)
680
+ if (typeof network === 'undefined') {
681
+ console.error("Vis.js network object is not defined. Interactivity buttons will not work.");
682
+ return;
683
+ }
684
+
685
+ // 2. SEARCH FUNCTIONALITY
686
+ var searchTimeout;
687
+ document.getElementById('search_box').addEventListener('input', function(e) {
688
+ clearTimeout(searchTimeout);
689
+ var query = e.target.value.toLowerCase().trim();
690
+ var resultsDiv = document.getElementById('search_results');
691
+
692
+ if (query.length === 0) {
693
+ // Show all nodes
694
+ var allNodes = network.body.data.nodes.get();
695
+ allNodes.forEach(function(n){
696
+ network.body.data.nodes.update({id:n.id, hidden: false});
697
+ });
698
+ resultsDiv.textContent = '';
699
+ return;
700
+ }
701
+
702
+ searchTimeout = setTimeout(function() {
703
+ var allNodes = network.body.data.nodes.get();
704
+ var matchCount = 0;
705
+
706
+ allNodes.forEach(function(n){
707
+ // n.label contains just the function name
708
+ var matches = n.label.toLowerCase().includes(query);
709
+ network.body.data.nodes.update({id:n.id, hidden: !matches});
710
+ if (matches) matchCount++;
711
+ });
712
+
713
+ resultsDiv.textContent = matchCount + ' function(s) found';
714
+
715
+ if (matchCount > 0) {
716
+ // Wait for the update to apply, then fit
717
+ setTimeout(function(){ network.fit(); }, 300);
718
+ }
719
+ }, 300);
720
+ });
721
+
722
+ // Clear search on Escape
723
+ document.getElementById('search_box').addEventListener('keydown', function(e) {
724
+ if (e.key === 'Escape') {
725
+ e.target.value = '';
726
+ e.target.dispatchEvent(new Event('input'));
727
+ }
728
+ });
729
+
730
+ // 3. GLOBAL VIEW BUTTONS
731
+
732
+ // Show all button
733
+ document.getElementById('btn_show_all').onclick = function() {
734
+ document.getElementById('search_box').value = '';
735
+ var allNodes = network.body.data.nodes.get();
736
+ allNodes.forEach(function(n){
737
+ network.body.data.nodes.update({id:n.id, hidden: false});
738
+ });
739
+ document.getElementById('search_results').textContent = '';
740
+ network.fit();
741
+ };
742
+
743
+ // Show only cycles button
744
+ document.getElementById('btn_show_cycles').onclick = function() {
745
+ document.getElementById('search_box').value = '';
746
+ var allNodes = network.body.data.nodes.get();
747
+ allNodes.forEach(function(n){
748
+ var isInCycle = functionsInCycles.has(n.label);
749
+ network.body.data.nodes.update({id:n.id, hidden: !isInCycle});
750
+ });
751
+ document.getElementById('search_results').textContent = functionsInCycles.size + ' function(s) in cycles';
752
+ setTimeout(function(){ network.fit(); }, 300);
753
+ };
754
+
755
+ // Show dead code button
756
+ document.getElementById('btn_show_dead').onclick = function() {
757
+ document.getElementById('search_box').value = '';
758
+ var allNodes = network.body.data.nodes.get();
759
+ allNodes.forEach(function(n){
760
+ var isDead = deadFunctions.has(n.label);
761
+ network.body.data.nodes.update({id:n.id, hidden: !isDead});
762
+ });
763
+ document.getElementById('search_results').textContent = deadFunctions.size + ' dead function(s)';
764
+ setTimeout(function(){ network.fit(); }, 300);
765
+ };
766
+
767
+ // Show complex functions button
768
+ document.getElementById('btn_show_complex').onclick = function() {
769
+ document.getElementById('search_box').value = '';
770
+ var allNodes = network.body.data.nodes.get();
771
+ allNodes.forEach(function(n){
772
+ var isComplex = highComplexityFunctions.has(n.label);
773
+ network.body.data.nodes.update({id:n.id, hidden: !isComplex});
774
+ });
775
+ document.getElementById('search_results').textContent = highComplexityFunctions.size + ' complex function(s)';
776
+ setTimeout(function(){ network.fit(); }, 300);
777
+ };
778
+
779
+ // Show entry points button
780
+ document.getElementById('btn_show_entry').onclick = function() {
781
+ document.getElementById('search_box').value = '';
782
+ var allNodes = network.body.data.nodes.get();
783
+ allNodes.forEach(function(n){
784
+ var isEntry = entryPoints.has(n.label);
785
+ network.body.data.nodes.update({id:n.id, hidden: !isEntry});
786
+ });
787
+ document.getElementById('search_results').textContent = entryPoints.size + ' entry point(s)';
788
+ setTimeout(function(){ network.fit(); }, 300);
789
+ };
790
+
791
+ // Hide cycles button
792
+ document.getElementById('btn_hide_cycles').onclick = function() {
793
+ document.getElementById('search_box').value = '';
794
+ var allNodes = network.body.data.nodes.get();
795
+ allNodes.forEach(function(n){
796
+ var isInCycle = functionsInCycles.has(n.label);
797
+ network.body.data.nodes.update({id:n.id, hidden: isInCycle});
798
+ });
799
+ document.getElementById('search_results').textContent = '';
800
+ setTimeout(function(){ network.fit(); }, 300);
801
+ };
802
+
803
+ // Reset view button (Full Implementation)
804
+ // Reset view button (FULL FIX)
805
+ document.getElementById('reset_focus').onclick = function() {
806
+ // 1. Clear search elements and UI panels
807
+ document.getElementById('search_box').value = '';
808
+ document.getElementById('search_results').textContent = '';
809
+ document.getElementById('analysis_panel').style.display = 'none';
810
+
811
+ // 2. Reset Nodes (Create a batch update array for reliability)
812
+ var allNodes = network.body.data.nodes.get();
813
+ var nodesToReset = [];
814
+ allNodes.forEach(function(n) {
815
+ nodesToReset.push({
816
+ id: n.id,
817
+ hidden: false,
818
+ borderWidth: 2,
819
+ color: n.id.includes('(') ? undefined : '#97C2E5'
820
+ });
821
+ });
822
+ network.body.data.nodes.update(nodesToReset); // Batch update
823
+
824
+ // 3. Reset Edges (CRITICAL FIX: Create and submit a batch update array)
825
+ var allEdges = network.body.data.edges.get();
826
+ var edgesToReset = [];
827
+ allEdges.forEach(function(e) {
828
+ edgesToReset.push({
829
+ id: e.id,
830
+ hidden: false, // CRITICAL: Ensure this value is passed
831
+ color: '#848484', // Default color
832
+ width: 2 // Default width
833
+ });
834
+ });
835
+ network.body.data.edges.update(edgesToReset); // Batch update for reliability
836
+
837
+ // 4. Reset Mode State
838
+ currentMode = 'default';
839
+ pathSourceNode = null;
840
+ pathTargetNode = null;
841
+ document.getElementById('mode_info').innerHTML = '<strong>Mode:</strong> Default (Drag/Zoom)';
842
+
843
+ // 5. Fit to view (Adding a slight delay can sometimes help Vis.js stabilize)
844
+ setTimeout(function() {
845
+ network.fit();
846
+ }, 50); // Small delay
847
+ };
848
+ // 4. FILE TOGGLE BUTTONS
849
+
850
+ // Note: The pyvis-generated nodes do not have a 'group' property automatically
851
+ // unless explicitly added. The node ID format is currently "function (file)".
852
+ // We will update the logic to toggle visibility based on file name inside the ID.
853
+
854
+ """
855
+
856
+ # Python loop for File toggle buttons
857
+ # for file_name in functions_ast.keys():
858
+ # # Sanitize button ID for JavaScript access
859
+ # button_id = f"btn_{file_name.replace('.', '_').replace('/', '__').replace('\\\\', '__')}"
860
+ # group_js += f"""
861
+ # document.getElementById("{button_id}").onclick = function() {{
862
+ # var nodes = network.body.data.nodes.get();
863
+ # var nodesToToggle = nodes.filter(function(n) {{
864
+ # // Check if node ID contains the file name (e.g., "func (file.py)")
865
+ # return n.id.includes('({file_name})');
866
+ # }});
867
+
868
+ # // Find the visibility state of the first node to determine action
869
+ # var isHidden = nodesToToggle.length > 0 ? nodesToToggle[0].hidden : false;
870
+ # var newState = !isHidden;
871
+
872
+ # nodesToToggle.forEach(function(n){{
873
+ # network.body.data.nodes.update({{id: n.id, hidden: newState}});
874
+ # }});
875
+ # setTimeout(function(){{ network.fit(); }}, 300);
876
+ # }};
877
+ # """
878
+ for file_name in functions_ast.keys():
879
+ # Sanitize for HTML ID
880
+ safe_id = file_name.replace('.', '_').replace('/', '__').replace('\\', '__').replace('-', '_').replace(' ', '_')
881
+ button_id = f"btn_{safe_id}"
882
+
883
+ # Escape backslashes for JavaScript strings
884
+ js_safe_filename = file_name.replace('\\', '\\\\')
885
+
886
+ group_js += f"""
887
+ // File toggle for {file_name}
888
+ (function() {{
889
+ const btn = document.getElementById("{button_id}");
890
+ if (!btn) {{
891
+ console.error("File button not found: {button_id}");
892
+ return;
893
+ }}
894
+
895
+ btn.onclick = function() {{
896
+ var nodes = network.body.data.nodes.get();
897
+ var nodesToToggle = nodes.filter(function(n) {{
898
+ return n.id.includes('({js_safe_filename})');
899
+ }});
900
+
901
+ var isHidden = nodesToToggle.length > 0 ? nodesToToggle[0].hidden : false;
902
+ var newState = !isHidden;
903
+
904
+ nodesToToggle.forEach(function(n){{
905
+ network.body.data.nodes.update({{id: n.id, hidden: newState}});
906
+ }});
907
+ setTimeout(function(){{ network.fit(); }}, 300);
908
+ }};
909
+ }})();
910
+ """
911
+ # Function focus buttons
912
+ for func_name in function_files.keys():
913
+ button_id = f"focus_{func_name}"
914
+ group_js += f"""
915
+ // Focus button for {func_name}
916
+ (function() {{
917
+ const btn = document.getElementById('{button_id}');
918
+ if (!btn) {{
919
+ console.error("BUTTON NOT FOUND: {button_id}");
920
+ return;
921
+ }}
922
+
923
+ btn.onclick = function() {{
924
+ var allNodes = network.body.data.nodes.get();
925
+ var showSet = new Set();
926
+ var targetNodeId = null;
927
+
928
+ allNodes.forEach(function(n) {{
929
+ if(n.label === '{func_name}') {{
930
+ showSet.add(n.label);
931
+ targetNodeId = n.id;
932
+ }}
933
+ }});
934
+
935
+ function getCallees(f_label) {{
936
+ allNodes.filter(n => n.label === f_label).forEach(function(n) {{
937
+ network.body.data.edges.get().forEach(function(e) {{
938
+ if(e.from === n.id) {{
939
+ var targetNode = allNodes.find(x => x.id === e.to);
940
+ if(targetNode && !showSet.has(targetNode.label)) {{
941
+ showSet.add(targetNode.label);
942
+ getCallees(targetNode.label);
943
+ }}
944
+ }}
945
+ }});
946
+ }});
947
+ }}
948
+ getCallees('{func_name}');
949
+
950
+ function getCallers(f_label) {{
951
+ allNodes.filter(n => n.label === f_label).forEach(function(n) {{
952
+ network.body.data.edges.get().forEach(function(e) {{
953
+ if(e.to === n.id) {{
954
+ var sourceNode = allNodes.find(x => x.id === e.from);
955
+ if(sourceNode && !showSet.has(sourceNode.label)) {{
956
+ showSet.add(sourceNode.label);
957
+ getCallers(sourceNode.label);
958
+ }}
959
+ }}
960
+ }});
961
+ }});
962
+ }}
963
+ getCallers('{func_name}');
964
+
965
+ allNodes.forEach(function(n) {{
966
+ network.body.data.nodes.update({{ id: n.id, hidden: !showSet.has(n.label) }});
967
+ }});
968
+
969
+ if (targetNodeId) {{
970
+ setTimeout(function() {{
971
+ network.focus(targetNodeId, {{ scale: 1.0, animation: true }});
972
+ }}, 500);
973
+ }} else {{
974
+ setTimeout(function() {{
975
+ network.fit();
976
+ }}, 500);
977
+ }}
978
+ }};
979
+ }})();
980
+ """
981
+ group_js += """
982
+ // Analysis Mode State
983
+ var currentMode = 'normal'; // 'normal', 'path', 'multi'
984
+ var pathSourceNode = null;
985
+ var pathTargetNode = null;
986
+ var selectedNodes = new Set();
987
+ var originalNodeColors = new Map(); // Store original colors for reset
988
+
989
+ // Store all original colors on initialization
990
+ var allNodes = network.body.data.nodes.get();
991
+ allNodes.forEach(function(n) {
992
+ originalNodeColors.set(n.id, {
993
+ background: n.color.background,
994
+ border: n.color.border
995
+ });
996
+ });
997
+
998
+ // Mode switching functions
999
+ function setMode(mode) {
1000
+ currentMode = mode;
1001
+ resetAnalysis();
1002
+
1003
+ // Update button styles
1004
+ document.getElementById('mode_normal').style.fontWeight = mode === 'normal' ? 'bold' : 'normal';
1005
+ document.getElementById('mode_normal').style.background = mode === 'normal' ? '#4CAF50' : '#81C784';
1006
+
1007
+ document.getElementById('mode_path').style.fontWeight = mode === 'path' ? 'bold' : 'normal';
1008
+ document.getElementById('mode_path').style.background = mode === 'path' ? '#2196F3' : '#64B5F6';
1009
+
1010
+ document.getElementById('mode_multi').style.fontWeight = mode === 'multi' ? 'bold' : 'normal';
1011
+ document.getElementById('mode_multi').style.background = mode === 'multi' ? '#FF9800' : '#FFB74D';
1012
+
1013
+ // Update info text
1014
+ var infoText = '';
1015
+ if (mode === 'normal') {
1016
+ infoText = '<strong>Normal Mode:</strong> Click nodes to explore';
1017
+ } else if (mode === 'path') {
1018
+ infoText = '<strong>Path Finder:</strong> Click source node (blue), then target node (green) to find path';
1019
+ } else if (mode === 'multi') {
1020
+ infoText = '<strong>Multi-Select:</strong> Hold Ctrl/Cmd and click to select multiple nodes';
1021
+ }
1022
+ document.getElementById('mode_info').innerHTML = infoText;
1023
+ }
1024
+
1025
+ function resetAnalysis() {
1026
+ pathSourceNode = null;
1027
+ pathTargetNode = null;
1028
+ selectedNodes.clear();
1029
+
1030
+ // Reset all node colors to original
1031
+ var nodes = network.body.data.nodes.get();
1032
+ nodes.forEach(function(n) {
1033
+ var original = originalNodeColors.get(n.id);
1034
+ if (original) {
1035
+ network.body.data.nodes.update({
1036
+ id: n.id,
1037
+ color: {background: original.background, border: original.border},
1038
+ borderWidth: 3
1039
+ });
1040
+ }
1041
+ });
1042
+
1043
+ // Reset all edges to original
1044
+ var edges = network.body.data.edges.get();
1045
+ edges.forEach(function(e) {
1046
+ network.body.data.edges.update({
1047
+ id: e.id,
1048
+ color: e.color === '#00ff00' || e.color === '#0066ff' ? null : e.color,
1049
+ width: e.width === 6 ? 2 : e.width
1050
+ });
1051
+ });
1052
+
1053
+ document.getElementById('analysis_panel').style.display = 'none';
1054
+ }
1055
+
1056
+ // Mode button handlers
1057
+ document.getElementById('mode_normal').onclick = function() { setMode('normal'); };
1058
+ document.getElementById('mode_path').onclick = function() { setMode('path'); };
1059
+ document.getElementById('mode_multi').onclick = function() { setMode('multi'); };
1060
+ document.getElementById('clear_analysis').onclick = resetAnalysis;
1061
+ """
1062
+ group_js += """
1063
+ // BFS Path Finding
1064
+ // Enhanced BFS Path Finding with depth limit and direction options
1065
+ function findPath(sourceId, targetId, maxDepth, bidirectional) {
1066
+ maxDepth = maxDepth || 10; // Default to 10 levels
1067
+ bidirectional = bidirectional !== false; // Default to true
1068
+
1069
+ var edges = network.body.data.edges.get();
1070
+ var nodes = network.body.data.nodes.get();
1071
+
1072
+ // Build adjacency lists
1073
+ var forwardGraph = {};
1074
+ var backwardGraph = {};
1075
+ nodes.forEach(function(n) {
1076
+ forwardGraph[n.id] = [];
1077
+ backwardGraph[n.id] = [];
1078
+ });
1079
+ edges.forEach(function(e) {
1080
+ if (!forwardGraph[e.from]) forwardGraph[e.from] = [];
1081
+ if (!backwardGraph[e.to]) backwardGraph[e.to] = [];
1082
+ forwardGraph[e.from].push({to: e.to, forward: true});
1083
+ backwardGraph[e.to].push({to: e.from, forward: false});
1084
+ });
1085
+
1086
+ // Try forward path first (A calls B)
1087
+ var forwardPath = bfsSearch(sourceId, targetId, forwardGraph, maxDepth);
1088
+ if (forwardPath) {
1089
+ return {path: forwardPath, type: 'forward'};
1090
+ }
1091
+
1092
+ // If bidirectional, try backward path (B calls A)
1093
+ if (bidirectional) {
1094
+ var backwardPath = bfsSearch(sourceId, targetId, backwardGraph, maxDepth);
1095
+ if (backwardPath) {
1096
+ return {path: backwardPath, type: 'backward'};
1097
+ }
1098
+ }
1099
+
1100
+ return null; // No path found within depth limit
1101
+ }
1102
+
1103
+ function bfsSearch(sourceId, targetId, graph, maxDepth) {
1104
+ var queue = [[sourceId]];
1105
+ var visited = new Set([sourceId]);
1106
+ var depth = 0;
1107
+
1108
+ while (queue.length > 0 && depth <= maxDepth) {
1109
+ var levelSize = queue.length;
1110
+
1111
+ for (var i = 0; i < levelSize; i++) {
1112
+ var path = queue.shift();
1113
+ var node = path[path.length - 1];
1114
+
1115
+ if (node === targetId) {
1116
+ return path;
1117
+ }
1118
+
1119
+ var neighbors = graph[node] || [];
1120
+ for (var j = 0; j < neighbors.length; j++) {
1121
+ var next = neighbors[j].to;
1122
+ if (!visited.has(next)) {
1123
+ visited.add(next);
1124
+ var newPath = path.slice();
1125
+ newPath.push(next);
1126
+ queue.push(newPath);
1127
+ }
1128
+ }
1129
+ }
1130
+ depth++;
1131
+ }
1132
+
1133
+ return null; // No path found within depth limit
1134
+ }
1135
+
1136
+ function highlightPath(pathResult) {
1137
+ if (!pathResult || !pathResult.path || pathResult.path.length === 0) return;
1138
+
1139
+ var path = pathResult.path;
1140
+ var pathType = pathResult.type;
1141
+
1142
+ var allNodes = network.body.data.nodes.get();
1143
+ var allEdges = network.body.data.edges.get();
1144
+ var pathNodeSet = new Set(path);
1145
+
1146
+ // --- 1. ISOLATE NON-PATH NODES ---
1147
+ allNodes.forEach(function(n) {
1148
+ var isPathNode = pathNodeSet.has(n.id);
1149
+ network.body.data.nodes.update({
1150
+ id: n.id,
1151
+ hidden: !isPathNode, // HIDE IF NOT IN PATH
1152
+ borderWidth: isPathNode ? 3 : 2,
1153
+ color: isPathNode ? {border: '#000000'} : undefined
1154
+ });
1155
+ });
1156
+
1157
+ // --- 2. ISOLATE NON-PATH EDGES (Fix) ---
1158
+ allEdges.forEach(function(e) {
1159
+ // Keep edge visible if BOTH its source and target nodes are part of the path.
1160
+ var isEdgeInPathNeighborhood = pathNodeSet.has(e.from) && pathNodeSet.has(e.to);
1161
+
1162
+ network.body.data.edges.update({
1163
+ id: e.id,
1164
+ hidden: !isEdgeInPathNeighborhood, // HIDE ONLY if both ends are NOT on the path
1165
+ width: 2,
1166
+ color: '#848484'
1167
+ });
1168
+ });
1169
+ // ------------------------------------------
1170
+
1171
+ // --- 3. HIGHLIGHTING (Original Logic) ---
1172
+
1173
+ // Highlight nodes in path (Original logic, ensures path nodes are bold/colored)
1174
+ path.forEach(function(nodeId, index) {
1175
+ // ... (Node coloring logic remains the same)
1176
+ network.body.data.nodes.update({
1177
+ id: nodeId,
1178
+ // ... color and borderWidth updates ...
1179
+ hidden: false // Ensure path nodes are visible
1180
+ });
1181
+ });
1182
+
1183
+ // Highlight specific edges forming the shortest path (Original logic, ensures these edges are bold/colored/hidden:false)
1184
+ if (pathType === 'forward') {
1185
+ // ... (forward edge logic)
1186
+ if (edge) {
1187
+ network.body.data.edges.update({
1188
+ id: edge.id,
1189
+ hidden: false, // ENSURE IT'S VISIBLE
1190
+ color: '#00ff00',
1191
+ width: 6
1192
+ });
1193
+ }
1194
+ } else if (pathType === 'backward') {
1195
+ // ... (backward edge logic)
1196
+ if (edge) {
1197
+ network.body.data.edges.update({
1198
+ id: edge.id,
1199
+ hidden: false, // ENSURE IT'S VISIBLE
1200
+ color: '#ff6b00',
1201
+ width: 6
1202
+ });
1203
+ }
1204
+ }
1205
+
1206
+ // Show results with depth warning
1207
+ var nodes = network.body.data.nodes.get();
1208
+ var pathLabels = path.map(function(id) {
1209
+ var node = nodes.find(function(n) { return n.id === id; });
1210
+ return node ? node.label : id;
1211
+ });
1212
+
1213
+ var directionIcon = pathType === 'forward' ? '→' : '←';
1214
+ var directionText = pathType === 'forward' ? 'Forward Path (A calls B)' : 'Backward Path (B calls A)';
1215
+ var directionColor = pathType === 'forward' ? '#2196F3' : '#FF6B00';
1216
+
1217
+ var html = '<strong style="color:' + directionColor + ';">' + directionText + '</strong><br>';
1218
+ html += '<strong>Path Length: ' + (path.length - 1) + ' step(s)</strong><br>';
1219
+
1220
+ if (path.length > 6) {
1221
+ html += '<div style="background:#fff3e0; padding:4px; margin:4px 0; border-radius:3px; font-size:10px;">⚠️ Long path - consider refactoring</div>';
1222
+ }
1223
+
1224
+ html += '<div style="margin-top:8px; padding:8px; background:#f5f5f5; border-radius:3px; max-height:250px; overflow-y:auto;">';
1225
+ pathLabels.forEach(function(label, i) {
1226
+ var arrow = (i < pathLabels.length - 1) ? ' <span style="color:' + directionColor + ';">' + directionIcon + '</span> ' : '';
1227
+ html += '<div style="margin:4px 0; padding:4px; background:white; border-left:3px solid ';
1228
+ if (i === 0) html += '#2196F3';
1229
+ else if (i === pathLabels.length - 1) html += '#4CAF50';
1230
+ else html += '#FF9800';
1231
+ html += ';">' + (i + 1) + '. ' + label + arrow + '</div>';
1232
+ });
1233
+ html += '</div>';
1234
+
1235
+ document.getElementById('analysis_content').innerHTML = html;
1236
+ document.getElementById('analysis_panel').style.display = 'block';
1237
+
1238
+ // Focus on path
1239
+ network.fit({nodes: path, animation: true});
1240
+ }
1241
+ """
1242
+ group_js += """
1243
+ function analyzeSelectedNodes() {
1244
+ if (selectedNodes.size === 0) {
1245
+ document.getElementById('analysis_panel').style.display = 'none';
1246
+ return;
1247
+ }
1248
+
1249
+ var nodes = network.body.data.nodes.get();
1250
+ var edges = network.body.data.edges.get();
1251
+ var selectedArray = Array.from(selectedNodes);
1252
+
1253
+ // Find connections between selected nodes
1254
+ var internalEdges = edges.filter(function(e) {
1255
+ return selectedNodes.has(e.from) && selectedNodes.has(e.to);
1256
+ });
1257
+
1258
+ // Find common callers (nodes that call multiple selected)
1259
+ var callerCounts = {};
1260
+ edges.forEach(function(e) {
1261
+ if (selectedNodes.has(e.to) && !selectedNodes.has(e.from)) {
1262
+ callerCounts[e.from] = (callerCounts[e.from] || 0) + 1;
1263
+ }
1264
+ });
1265
+ var commonCallers = Object.keys(callerCounts).filter(function(id) {
1266
+ return callerCounts[id] > 1;
1267
+ });
1268
+
1269
+ // Find common callees (nodes called by multiple selected)
1270
+ var calleeCounts = {};
1271
+ edges.forEach(function(e) {
1272
+ if (selectedNodes.has(e.from) && !selectedNodes.has(e.to)) {
1273
+ calleeCounts[e.to] = (calleeCounts[e.to] || 0) + 1;
1274
+ }
1275
+ });
1276
+ var commonCallees = Object.keys(calleeCounts).filter(function(id) {
1277
+ return calleeCounts[id] > 1;
1278
+ });
1279
+
1280
+ // Build HTML
1281
+ var html = '<strong style="color:#FF9800;">' + selectedNodes.size + ' Functions Selected</strong><br>';
1282
+ html += '<div style="margin-top:8px;">';
1283
+
1284
+ // Selected nodes
1285
+ html += '<div style="margin:8px 0;"><strong>Selected:</strong></div>';
1286
+ selectedArray.forEach(function(id) {
1287
+ var node = nodes.find(function(n) { return n.id === id; });
1288
+ if (node) {
1289
+ html += '<div style="margin:2px 0; padding:3px; background:#fff3e0; border-left:3px solid #FF9800; font-size:10px;">• ' + node.label + '</div>';
1290
+ }
1291
+ });
1292
+
1293
+ // Internal connections
1294
+ if (internalEdges.length > 0) {
1295
+ html += '<div style="margin:12px 0 4px 0;"><strong>Internal Connections: ' + internalEdges.length + '</strong></div>';
1296
+ internalEdges.slice(0, 5).forEach(function(e) {
1297
+ var fromNode = nodes.find(function(n) { return n.id === e.from; });
1298
+ var toNode = nodes.find(function(n) { return n.id === e.to; });
1299
+ if (fromNode && toNode) {
1300
+ html += '<div style="margin:2px 0; padding:3px; background:white; font-size:10px;">• ' + fromNode.label + ' → ' + toNode.label + '</div>';
1301
+ }
1302
+ });
1303
+ if (internalEdges.length > 5) {
1304
+ html += '<div style="font-size:10px; font-style:italic; margin:2px 0;">...+' + (internalEdges.length - 5) + ' more</div>';
1305
+ }
1306
+ }
1307
+
1308
+ // Common callers
1309
+ if (commonCallers.length > 0) {
1310
+ html += '<div style="margin:12px 0 4px 0;"><strong>Common Callers: ' + commonCallers.length + '</strong></div>';
1311
+ commonCallers.slice(0, 3).forEach(function(id) {
1312
+ var node = nodes.find(function(n) { return n.id === id; });
1313
+ if (node) {
1314
+ html += '<div style="margin:2px 0; padding:3px; background:#e3f2fd; border-left:3px solid #2196F3; font-size:10px;">• ' + node.label + ' (calls ' + callerCounts[id] + ')' + '</div>';
1315
+ }
1316
+ });
1317
+ }
1318
+
1319
+ // Common callees
1320
+ if (commonCallees.length > 0) {
1321
+ html += '<div style="margin:12px 0 4px 0;"><strong>Common Callees: ' + commonCallees.length + '</strong></div>';
1322
+ commonCallees.slice(0, 3).forEach(function(id) {
1323
+ var node = nodes.find(function(n) { return n.id === id; });
1324
+ if (node) {
1325
+ html += '<div style="margin:2px 0; padding:3px; background:#e8f5e9; border-left:3px solid #4CAF50; font-size:10px;">• ' + node.label + ' (called by ' + calleeCounts[id] + ')' + '</div>';
1326
+ }
1327
+ });
1328
+ }
1329
+
1330
+ html += '</div>';
1331
+
1332
+ document.getElementById('analysis_content').innerHTML = html;
1333
+ document.getElementById('analysis_panel').style.display = 'block';
1334
+ }
1335
+ """
1336
+ group_js += """
1337
+ // Handle node clicks based on mode
1338
+ network.on("click", function(params) {
1339
+ if (params.nodes.length === 0) return;
1340
+
1341
+ var nodeId = params.nodes[0];
1342
+
1343
+ if (currentMode === 'path') {
1344
+ if (!pathSourceNode) {
1345
+ // Select source
1346
+ pathSourceNode = nodeId;
1347
+ network.body.data.nodes.update({
1348
+ id: nodeId,
1349
+ color: {background: '#2196F3', border: '#1565C0'},
1350
+ borderWidth: 5
1351
+ });
1352
+ document.getElementById('mode_info').innerHTML = '<strong>Path Finder:</strong> Source selected (blue). Now click target node.';
1353
+ } else if (!pathTargetNode && nodeId !== pathSourceNode) {
1354
+ // Select target and find path
1355
+ pathTargetNode = nodeId;
1356
+
1357
+ // Get user preferences
1358
+ var maxDepth = 10
1359
+ var bidirectional = 1
1360
+
1361
+ var pathResult = findPath(pathSourceNode, pathTargetNode, maxDepth, bidirectional);
1362
+
1363
+ if (pathResult) {
1364
+ highlightPath(pathResult);
1365
+ } else {
1366
+ // No path found
1367
+ network.body.data.nodes.update({
1368
+ id: nodeId,
1369
+ color: {background: '#f44336', border: '#c62828'},
1370
+ borderWidth: 5
1371
+ });
1372
+ var msg = bidirectional ?
1373
+ 'No path found in either direction within ' + maxDepth + ' levels.' :
1374
+ 'No forward path found within ' + maxDepth + ' levels.';
1375
+ document.getElementById('analysis_content').innerHTML =
1376
+ '<strong style="color:#f44336;">❌ No Path Found</strong><br>' +
1377
+ '<div style="margin-top:8px;">' + msg + '</div>' +
1378
+ '<div style="margin-top:8px; font-size:10px; color:#666;">Try increasing max depth or enabling bidirectional search.</div>';
1379
+ document.getElementById('analysis_panel').style.display = 'block';
1380
+ }
1381
+ }
1382
+ } else if (currentMode === 'multi') {
1383
+ // Toggle selection
1384
+ if (selectedNodes.has(nodeId)) {
1385
+ selectedNodes.delete(nodeId);
1386
+ var original = originalNodeColors.get(nodeId);
1387
+ if (original) {
1388
+ network.body.data.nodes.update({
1389
+ id: nodeId,
1390
+ color: {background: original.background, border: original.border},
1391
+ borderWidth: 3
1392
+ });
1393
+ }
1394
+ } else {
1395
+ selectedNodes.add(nodeId);
1396
+ network.body.data.nodes.update({
1397
+ id: nodeId,
1398
+ color: {background: '#FF9800', border: '#F57C00'},
1399
+ borderWidth: 5
1400
+ });
1401
+ }
1402
+ analyzeSelectedNodes();
1403
+ }
1404
+ else if (currentMode === 'explode') {
1405
+ // Trigger the explode function immediately
1406
+ explodeNode(nodeId);
1407
+ }
1408
+ });
1409
+ """
1410
+ group_js += """
1411
+ // Global variable to store the currently exploded node's ID for easy clearing
1412
+ var explodedNodeId = null;
1413
+
1414
+ function explodeNode(nodeId) {
1415
+ // Reset the view first to clear any previous paths or highlights
1416
+ resetAnalysis();
1417
+
1418
+ explodedNodeId = nodeId;
1419
+
1420
+ var allNodes = network.body.data.nodes.get();
1421
+ var allEdges = network.body.data.edges.get();
1422
+
1423
+ // 1. Identify the Neighbors
1424
+ var neighbors = new Set();
1425
+ neighbors.add(nodeId); // Include the central node itself
1426
+
1427
+ var connectingEdges = [];
1428
+
1429
+ // Find all direct neighbors and connecting edges
1430
+ allEdges.forEach(function(e) {
1431
+ var isConnecting = false;
1432
+
1433
+ if (e.from === nodeId) {
1434
+ neighbors.add(e.to); // Callee
1435
+ isConnecting = true;
1436
+ } else if (e.to === nodeId) {
1437
+ neighbors.add(e.from); // Caller
1438
+ isConnecting = true;
1439
+ }
1440
+
1441
+ if (isConnecting) {
1442
+ connectingEdges.push(e);
1443
+ }
1444
+ });
1445
+
1446
+ // 2. Isolate/Hide elements
1447
+
1448
+ // Hide non-neighbor nodes
1449
+ var nodesToUpdate = [];
1450
+ allNodes.forEach(function(n) {
1451
+ var isNeighbor = neighbors.has(n.id);
1452
+ var update = {
1453
+ id: n.id,
1454
+ hidden: !isNeighbor, // Hide if not a neighbor
1455
+ borderWidth: 2,
1456
+ color: undefined // Reset color
1457
+ };
1458
+ // Highlight the central node
1459
+ if (n.id === nodeId) {
1460
+ update.color = {background: '#9C27B0', border: '#7B1FA2'}; // Purple for Exploded Node
1461
+ update.borderWidth = 5;
1462
+ }
1463
+ nodesToUpdate.push(update);
1464
+ });
1465
+ network.body.data.nodes.update(nodesToUpdate);
1466
+
1467
+ // Hide all edges first, then reveal only connecting edges
1468
+ var edgesToUpdate = [];
1469
+ allEdges.forEach(function(e) {
1470
+ edgesToUpdate.push({
1471
+ id: e.id,
1472
+ hidden: true,
1473
+ width: 2,
1474
+ color: '#848484'
1475
+ });
1476
+ });
1477
+ network.body.data.edges.update(edgesToUpdate);
1478
+
1479
+ // 3. Highlight Connecting Edges (Reveal them)
1480
+ connectingEdges.forEach(function(e) {
1481
+ network.body.data.edges.update({
1482
+ id: e.id,
1483
+ hidden: false, // Make connecting edges visible
1484
+ color: {color: '#9C27B0', highlight: '#7B1FA2'}, // Purple color
1485
+ width: 4
1486
+ });
1487
+ });
1488
+
1489
+ // 4. Update Analysis Panel
1490
+ var nodeLabel = network.body.data.nodes.get(nodeId).label;
1491
+ var callerCount = connectingEdges.filter(e => e.to === nodeId).length;
1492
+ var calleeCount = connectingEdges.filter(e => e.from === nodeId).length;
1493
+
1494
+ var html = '<strong style="color:#9C27B0;">⚛️ Explosion View: ' + nodeLabel + '</strong><br>';
1495
+ html += '<div>Callers: <strong>' + callerCount + '</strong></div>';
1496
+ html += '<div>Callees: <strong>' + calleeCount + '</strong></div>';
1497
+ html += '<div style="margin-top:10px; font-size:11px;">Click **Reset View** to return to full graph.</div>';
1498
+
1499
+ document.getElementById('analysis_content').innerHTML = html;
1500
+ document.getElementById('analysis_panel').style.display = 'block';
1501
+
1502
+ // Fit view to the neighborhood
1503
+ network.fit({nodes: Array.from(neighbors), animation: true});
1504
+ }
1505
+ """
1506
+ group_js += """
1507
+ document.getElementById('btn_mode_explode').onclick = function() {
1508
+ resetAnalysis();
1509
+ currentMode = 'explode';
1510
+ document.getElementById('mode_info').innerHTML = '<strong>Explode Mode:</strong> Click any node to see its immediate neighbors.';
1511
+ };
1512
+ """
1513
+ group_js += "}); // End of DOMContentLoaded\n"
1514
+ group_js += "</script>\n"
1515
+
1516
+ html = html.replace("</body>", f"{group_js}</body>")
1517
+ html = html.replace("<body>", f"<body>{header}")
1518
+
1519
+ # print(output_path) # Debugging print removed
1520
+ with open(output_path, "w", encoding="utf8") as f:
1521
+ f.write(html)
1522
+
1523
+ print(f"   Generated visualization: {output_path}")