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.
- pycallgraph_visualizer/__init__.py +29 -0
- pycallgraph_visualizer/analyzer.py +1523 -0
- pycallgraph_visualizer/cli.py +181 -0
- pycallgraph_visualizer-1.0.1.dist-info/METADATA +36 -0
- pycallgraph_visualizer-1.0.1.dist-info/RECORD +9 -0
- pycallgraph_visualizer-1.0.1.dist-info/WHEEL +5 -0
- pycallgraph_visualizer-1.0.1.dist-info/entry_points.txt +2 -0
- pycallgraph_visualizer-1.0.1.dist-info/licenses/LICENSE +21 -0
- pycallgraph_visualizer-1.0.1.dist-info/top_level.txt +1 -0
|
@@ -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 > 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}")
|