Spaces:
Sleeping
Sleeping
Upload 3 files
Browse files- app.py +121 -42
- introduction.py +33 -20
- utility.py +63 -0
app.py
CHANGED
|
@@ -5,29 +5,102 @@ import dash_cytoscape as cyto
|
|
| 5 |
from dash.dependencies import Input, Output, State
|
| 6 |
import pandas as pd
|
| 7 |
|
| 8 |
-
from utility import parse_functions_from_files, get_reachable_functions, get_backtrace_functions
|
| 9 |
from introduction import get_intro_markdown
|
| 10 |
|
| 11 |
app = dash.Dash(__name__, suppress_callback_exceptions=True)
|
| 12 |
server = app.server
|
| 13 |
|
|
|
|
| 14 |
app.layout = html.Div([
|
| 15 |
-
html.H2("Function Dependency Visualizer (AST-Based)"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 16 |
dcc.Store(id="parsed-functions-store"),
|
| 17 |
-
|
| 18 |
-
|
| 19 |
-
|
| 20 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 21 |
dcc.Tabs(id="tab-selector", value="intro", children=[
|
| 22 |
dcc.Tab(label="📘 Introduction", value="intro"),
|
| 23 |
dcc.Tab(label="📊 Graph Explorer", value="graph"),
|
| 24 |
dcc.Tab(label="📁 File Summary", value="summary")
|
| 25 |
-
]),
|
| 26 |
-
|
| 27 |
-
html.Div(id="
|
| 28 |
-
html.Div(id="
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 29 |
])
|
| 30 |
|
|
|
|
| 31 |
@app.callback(
|
| 32 |
Output("main-function", "options"),
|
| 33 |
Output("main-function", "value"),
|
|
@@ -36,6 +109,7 @@ app.layout = html.Div([
|
|
| 36 |
State("upload", "filename")
|
| 37 |
)
|
| 38 |
def handle_upload(contents, filenames):
|
|
|
|
| 39 |
if not contents:
|
| 40 |
return [], None, {}
|
| 41 |
uploaded_files = {}
|
|
@@ -43,13 +117,19 @@ def handle_upload(contents, filenames):
|
|
| 43 |
_, content_string = content.split(",")
|
| 44 |
uploaded_files[name] = base64.b64decode(content_string).decode("utf-8")
|
| 45 |
parsed = parse_functions_from_files(uploaded_files)
|
| 46 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 47 |
return options, options[0]["value"] if options else None, parsed
|
| 48 |
|
| 49 |
@app.callback(
|
| 50 |
-
Output("intro-tab", "style"),
|
| 51 |
-
Output("graph-tab", "style"),
|
| 52 |
-
Output("summary-tab", "style"),
|
| 53 |
Input("tab-selector", "value")
|
| 54 |
)
|
| 55 |
def toggle_tabs(tab):
|
|
@@ -63,48 +143,46 @@ def toggle_tabs(tab):
|
|
| 63 |
def show_intro(_):
|
| 64 |
return get_intro_markdown()
|
| 65 |
|
|
|
|
|
|
|
| 66 |
@app.callback(
|
| 67 |
-
Output("graph-tab", "children"),
|
| 68 |
Input("main-function", "value"),
|
| 69 |
Input("backtrace-toggle", "value"),
|
|
|
|
| 70 |
State("parsed-functions-store", "data")
|
| 71 |
)
|
| 72 |
-
def update_graph(main_func, backtrace_mode, parsed):
|
| 73 |
if not parsed or not main_func:
|
| 74 |
return html.Div("Upload files and select a main function.")
|
|
|
|
| 75 |
graph = {k: v["calls"] for k, v in parsed.items()}
|
| 76 |
reachable = get_backtrace_functions(main_func, graph) if "backtrace" in backtrace_mode else get_reachable_functions(main_func, graph)
|
| 77 |
-
nodes = [{"data": {"id": name, "label": name}} for name in reachable]
|
| 78 |
-
#edges = [{"data": {"source": src, "target": tgt}, "classes": "edge"} for src in reachable for tgt in parsed[src]["calls"] if tgt in reachable]
|
| 79 |
-
edges = []
|
| 80 |
-
for src in reachable:
|
| 81 |
-
call_sequence = parsed[src]["calls"]
|
| 82 |
-
call_index = 1
|
| 83 |
-
for tgt in call_sequence:
|
| 84 |
-
if tgt in reachable:
|
| 85 |
-
edge = {
|
| 86 |
-
"data": {
|
| 87 |
-
"source": src,
|
| 88 |
-
"target": tgt,
|
| 89 |
-
"label": str(call_index)
|
| 90 |
-
},
|
| 91 |
-
"style": {
|
| 92 |
-
"line-width": 4 if call_sequence.count(tgt) > 1 else 2
|
| 93 |
-
}
|
| 94 |
-
}
|
| 95 |
-
edges.append(edge)
|
| 96 |
-
call_index += 1
|
| 97 |
|
|
|
|
| 98 |
|
| 99 |
|
| 100 |
return cyto.Cytoscape(
|
| 101 |
id="cytoscape-graph",
|
| 102 |
-
layout={"name": "
|
| 103 |
style={"width": "100%", "height": "600px"},
|
| 104 |
elements=nodes + edges,
|
| 105 |
stylesheet=[
|
| 106 |
-
{"selector": "node", "style": {
|
| 107 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 108 |
{"selector": "edge", "style": {
|
| 109 |
"label": "data(label)",
|
| 110 |
"curve-style": "bezier",
|
|
@@ -113,9 +191,9 @@ def update_graph(main_func, backtrace_mode, parsed):
|
|
| 113 |
"line-color": "#888",
|
| 114 |
"arrow-scale": 2,
|
| 115 |
"font-size": "14px",
|
| 116 |
-
"text-margin-y": -10
|
|
|
|
| 117 |
}}
|
| 118 |
-
|
| 119 |
],
|
| 120 |
userZoomingEnabled=True,
|
| 121 |
userPanningEnabled=True,
|
|
@@ -124,8 +202,9 @@ def update_graph(main_func, backtrace_mode, parsed):
|
|
| 124 |
wheelSensitivity=0.1
|
| 125 |
)
|
| 126 |
|
|
|
|
| 127 |
@app.callback(
|
| 128 |
-
Output("summary-tab", "children"),
|
| 129 |
Input("parsed-functions-store", "data"),
|
| 130 |
Input("function-detail-toggle", "value")
|
| 131 |
)
|
|
|
|
| 5 |
from dash.dependencies import Input, Output, State
|
| 6 |
import pandas as pd
|
| 7 |
|
| 8 |
+
from utility import parse_functions_from_files, get_reachable_functions, get_backtrace_functions, build_nodes_and_edges
|
| 9 |
from introduction import get_intro_markdown
|
| 10 |
|
| 11 |
app = dash.Dash(__name__, suppress_callback_exceptions=True)
|
| 12 |
server = app.server
|
| 13 |
|
| 14 |
+
|
| 15 |
app.layout = html.Div([
|
| 16 |
+
html.H2("Function Dependency Visualizer (AST-Based)", style={
|
| 17 |
+
"textAlign": "center",
|
| 18 |
+
"marginTop": "20px",
|
| 19 |
+
"fontSize": "28px",
|
| 20 |
+
"color": "#333"
|
| 21 |
+
}),
|
| 22 |
dcc.Store(id="parsed-functions-store"),
|
| 23 |
+
|
| 24 |
+
html.Div([
|
| 25 |
+
html.Div([
|
| 26 |
+
html.Label("Upload Files"),
|
| 27 |
+
dcc.Upload(
|
| 28 |
+
id="upload",
|
| 29 |
+
children=html.Div("📤 Drag and drop or click to upload Python files", style={
|
| 30 |
+
"border": "2px dashed #ccc",
|
| 31 |
+
"padding": "10px",
|
| 32 |
+
"textAlign": "center",
|
| 33 |
+
"cursor": "pointer",
|
| 34 |
+
"color": "#555",
|
| 35 |
+
"fontSize": "14px"
|
| 36 |
+
}),
|
| 37 |
+
multiple=True
|
| 38 |
+
)
|
| 39 |
+
], style={"width": "24%", "display": "inline-block", "verticalAlign": "top", "marginRight": "1%"}),
|
| 40 |
+
|
| 41 |
+
html.Div([
|
| 42 |
+
html.Label("Main Function"),
|
| 43 |
+
dcc.Dropdown(id="main-function", style={"width": "100%"})
|
| 44 |
+
], style={"width": "24%", "display": "inline-block", "verticalAlign": "top", "marginRight": "1%"}),
|
| 45 |
+
|
| 46 |
+
html.Div([
|
| 47 |
+
html.Label("Max Depth"),
|
| 48 |
+
dcc.Dropdown(
|
| 49 |
+
id="max-depth",
|
| 50 |
+
options=[{"label": str(i), "value": i} for i in range(1, 11)],
|
| 51 |
+
value=10,
|
| 52 |
+
clearable=False,
|
| 53 |
+
style={"width": "100%"}
|
| 54 |
+
)
|
| 55 |
+
], style={"width": "24%", "display": "inline-block", "verticalAlign": "top", "marginRight": "1%"}),
|
| 56 |
+
|
| 57 |
+
html.Div([
|
| 58 |
+
html.Label("Options"),
|
| 59 |
+
dcc.Checklist(
|
| 60 |
+
id="backtrace-toggle",
|
| 61 |
+
options=[{"label": "Backtrace Mode", "value": "backtrace"}],
|
| 62 |
+
value=[],
|
| 63 |
+
labelStyle={"display": "block", "fontWeight": "normal"}
|
| 64 |
+
),
|
| 65 |
+
dcc.Checklist(
|
| 66 |
+
id="function-detail-toggle",
|
| 67 |
+
options=[{"label": "Show Function-Level Detail", "value": "show"}],
|
| 68 |
+
value=[],
|
| 69 |
+
labelStyle={"display": "block", "fontWeight": "normal"},
|
| 70 |
+
style={"marginTop": "5px"}
|
| 71 |
+
)
|
| 72 |
+
], style={"width": "24%", "display": "inline-block", "verticalAlign": "top"})
|
| 73 |
+
], style={
|
| 74 |
+
"backgroundColor": "#f9f9f9",
|
| 75 |
+
"padding": "15px",
|
| 76 |
+
"borderRadius": "10px",
|
| 77 |
+
"margin": "20px auto",
|
| 78 |
+
"width": "95%",
|
| 79 |
+
"boxShadow": "0 2px 6px rgba(0,0,0,0.1)"
|
| 80 |
+
}),
|
| 81 |
+
|
| 82 |
+
|
| 83 |
dcc.Tabs(id="tab-selector", value="intro", children=[
|
| 84 |
dcc.Tab(label="📘 Introduction", value="intro"),
|
| 85 |
dcc.Tab(label="📊 Graph Explorer", value="graph"),
|
| 86 |
dcc.Tab(label="📁 File Summary", value="summary")
|
| 87 |
+
], style={"margin": "0 20px"}),
|
| 88 |
+
|
| 89 |
+
html.Div(html.Div(id="intro-tab", style={"margin": "0 20px"}), id="intro-tab-container"),
|
| 90 |
+
html.Div(id="graph-tab-container", style={"margin": "0 20px"}),
|
| 91 |
+
html.Div(id="summary-tab-container", style={"margin": "0 20px"}),
|
| 92 |
+
|
| 93 |
+
|
| 94 |
+
html.Footer("Built by Tomas Larsson • MIT Licensed", style={
|
| 95 |
+
"textAlign": "center",
|
| 96 |
+
"fontSize": "14px",
|
| 97 |
+
"marginTop": "40px",
|
| 98 |
+
"marginBottom": "20px",
|
| 99 |
+
"color": "#888"
|
| 100 |
+
})
|
| 101 |
])
|
| 102 |
|
| 103 |
+
|
| 104 |
@app.callback(
|
| 105 |
Output("main-function", "options"),
|
| 106 |
Output("main-function", "value"),
|
|
|
|
| 109 |
State("upload", "filename")
|
| 110 |
)
|
| 111 |
def handle_upload(contents, filenames):
|
| 112 |
+
print("=== handle_upload triggered ===")
|
| 113 |
if not contents:
|
| 114 |
return [], None, {}
|
| 115 |
uploaded_files = {}
|
|
|
|
| 117 |
_, content_string = content.split(",")
|
| 118 |
uploaded_files[name] = base64.b64decode(content_string).decode("utf-8")
|
| 119 |
parsed = parse_functions_from_files(uploaded_files)
|
| 120 |
+
print("== PARSED FUNCTION NAMES ==")
|
| 121 |
+
for fn, meta in parsed.items():
|
| 122 |
+
print(f"{fn} → calls: {meta.get('calls', [])}")
|
| 123 |
+
print("== END ==")
|
| 124 |
+
|
| 125 |
+
# options = [{"label": fn, "value": fn} for fn in parsed if parsed[fn]["calls"]]
|
| 126 |
+
options = [{"label": fn, "value": fn} for fn in parsed]
|
| 127 |
return options, options[0]["value"] if options else None, parsed
|
| 128 |
|
| 129 |
@app.callback(
|
| 130 |
+
Output("intro-tab-container", "style"),
|
| 131 |
+
Output("graph-tab-container", "style"),
|
| 132 |
+
Output("summary-tab-container", "style"),
|
| 133 |
Input("tab-selector", "value")
|
| 134 |
)
|
| 135 |
def toggle_tabs(tab):
|
|
|
|
| 143 |
def show_intro(_):
|
| 144 |
return get_intro_markdown()
|
| 145 |
|
| 146 |
+
from collections import deque, defaultdict
|
| 147 |
+
|
| 148 |
@app.callback(
|
| 149 |
+
Output("graph-tab-container", "children"),
|
| 150 |
Input("main-function", "value"),
|
| 151 |
Input("backtrace-toggle", "value"),
|
| 152 |
+
Input("max-depth", "value"),
|
| 153 |
State("parsed-functions-store", "data")
|
| 154 |
)
|
| 155 |
+
def update_graph(main_func, backtrace_mode, max_depth, parsed):
|
| 156 |
if not parsed or not main_func:
|
| 157 |
return html.Div("Upload files and select a main function.")
|
| 158 |
+
|
| 159 |
graph = {k: v["calls"] for k, v in parsed.items()}
|
| 160 |
reachable = get_backtrace_functions(main_func, graph) if "backtrace" in backtrace_mode else get_reachable_functions(main_func, graph)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 161 |
|
| 162 |
+
nodes, edges = build_nodes_and_edges(parsed, main_func, reachable, reverse="backtrace" in backtrace_mode, max_depth=max_depth)
|
| 163 |
|
| 164 |
|
| 165 |
return cyto.Cytoscape(
|
| 166 |
id="cytoscape-graph",
|
| 167 |
+
layout={"name": "preset"},
|
| 168 |
style={"width": "100%", "height": "600px"},
|
| 169 |
elements=nodes + edges,
|
| 170 |
stylesheet=[
|
| 171 |
+
{"selector": "node", "style": {
|
| 172 |
+
"label": "data(label)",
|
| 173 |
+
"text-wrap": "wrap",
|
| 174 |
+
"text-valign": "bottom",
|
| 175 |
+
"text-halign": "center"
|
| 176 |
+
}},
|
| 177 |
+
{"selector": ".main", "style": {
|
| 178 |
+
"background-color": "blue",
|
| 179 |
+
"line-color": "blue",
|
| 180 |
+
"color": "black",
|
| 181 |
+
"label": "data(label)",
|
| 182 |
+
"text-wrap": "wrap",
|
| 183 |
+
"text-valign": "bottom",
|
| 184 |
+
"text-halign": "center"
|
| 185 |
+
}},
|
| 186 |
{"selector": "edge", "style": {
|
| 187 |
"label": "data(label)",
|
| 188 |
"curve-style": "bezier",
|
|
|
|
| 191 |
"line-color": "#888",
|
| 192 |
"arrow-scale": 2,
|
| 193 |
"font-size": "14px",
|
| 194 |
+
"text-margin-y": -10,
|
| 195 |
+
"text-margin-x": 10,
|
| 196 |
}}
|
|
|
|
| 197 |
],
|
| 198 |
userZoomingEnabled=True,
|
| 199 |
userPanningEnabled=True,
|
|
|
|
| 202 |
wheelSensitivity=0.1
|
| 203 |
)
|
| 204 |
|
| 205 |
+
|
| 206 |
@app.callback(
|
| 207 |
+
Output("summary-tab-container", "children"),
|
| 208 |
Input("parsed-functions-store", "data"),
|
| 209 |
Input("function-detail-toggle", "value")
|
| 210 |
)
|
introduction.py
CHANGED
|
@@ -2,43 +2,56 @@ from dash import dcc
|
|
| 2 |
|
| 3 |
def get_intro_markdown():
|
| 4 |
return dcc.Markdown("""
|
| 5 |
-
###
|
| 6 |
|
| 7 |
-
|
|
|
|
| 8 |
|
| 9 |
---
|
| 10 |
|
| 11 |
#### 🔍 Features
|
| 12 |
-
- Upload
|
| 13 |
-
-
|
| 14 |
-
-
|
| 15 |
-
-
|
| 16 |
-
-
|
| 17 |
-
-
|
| 18 |
- Call order on edges (`1`, `2`, `3`)
|
| 19 |
-
- Thicker lines
|
|
|
|
|
|
|
| 20 |
|
| 21 |
---
|
| 22 |
|
| 23 |
-
####
|
| 24 |
1. Switch to the **Graph Explorer** tab
|
| 25 |
-
2. Upload
|
| 26 |
-
3.
|
| 27 |
-
4. Explore
|
| 28 |
-
|
| 29 |
---
|
| 30 |
-
|
| 31 |
-
### 👋 About the Creator
|
| 32 |
|
| 33 |
-
|
| 34 |
|
| 35 |
-
|
|
|
|
| 36 |
|
| 37 |
- Tax-efficient investing
|
| 38 |
- Retirement modeling
|
| 39 |
- Personal finance analytics
|
| 40 |
-
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 41 |
|
| 42 |
-
|
|
|
|
| 43 |
|
| 44 |
""")
|
|
|
|
| 2 |
|
| 3 |
def get_intro_markdown():
|
| 4 |
return dcc.Markdown("""
|
| 5 |
+
### ⚡ Function Call Visualizer for Python
|
| 6 |
|
| 7 |
+
Upload `.py` files and instantly see how functions connect—**visually**.
|
| 8 |
+
Ideal for refactoring, onboarding, debugging, or exploring unfamiliar code.
|
| 9 |
|
| 10 |
---
|
| 11 |
|
| 12 |
#### 🔍 Features
|
| 13 |
+
- Upload **one or more** Python files
|
| 14 |
+
- Pick a top-level function to trace all its calls
|
| 15 |
+
- Auto-inferred:
|
| 16 |
+
- Function arguments and return types (e.g. `df: pd.DataFrame`)
|
| 17 |
+
- Session state usage (e.g. Streamlit `session_state`)
|
| 18 |
+
- Interactive graph with:
|
| 19 |
- Call order on edges (`1`, `2`, `3`)
|
| 20 |
+
- Thicker lines for repeated calls
|
| 21 |
+
- Directional arrows and clear layout by call depth
|
| 22 |
+
- Toggleable summary tables
|
| 23 |
|
| 24 |
---
|
| 25 |
|
| 26 |
+
#### 🚀 How to Use
|
| 27 |
1. Switch to the **Graph Explorer** tab
|
| 28 |
+
2. Upload `.py` file(s)
|
| 29 |
+
3. Select an entry-point function
|
| 30 |
+
4. Explore how your code is structured—at a glance
|
| 31 |
+
|
| 32 |
---
|
|
|
|
|
|
|
| 33 |
|
| 34 |
+
### 👋 About the Creator
|
| 35 |
|
| 36 |
+
This open-source tool is built by **Tomas Larsson**,
|
| 37 |
+
a data scientist and creator of [**my.moneytoolbox.com**](https://mymoneytoolbox.com), where he shares tools and insights for:
|
| 38 |
|
| 39 |
- Tax-efficient investing
|
| 40 |
- Retirement modeling
|
| 41 |
- Personal finance analytics
|
| 42 |
+
- Code-first decision tools for DIY investors and engineers
|
| 43 |
+
|
| 44 |
+
---
|
| 45 |
+
|
| 46 |
+
### 🤝 Connect
|
| 47 |
+
|
| 48 |
+
Love dev tools like this?
|
| 49 |
+
Tomas shares more on [**LinkedIn**](https://www.linkedin.com/in/tomaslarsson/) —
|
| 50 |
+
follow along for open-source tools at the intersection of data, finance, and visualization.
|
| 51 |
+
|
| 52 |
+
---
|
| 53 |
|
| 54 |
+
### 🧠 Tags
|
| 55 |
+
`#python` `#static-analysis` `#ast` `#code-visualization` `#developer-tools` `#streamlit` `#dash` `#visualization`
|
| 56 |
|
| 57 |
""")
|
utility.py
CHANGED
|
@@ -81,3 +81,66 @@ def get_backtrace_functions(target, graph):
|
|
| 81 |
visited.add(node)
|
| 82 |
stack.extend(reverse_graph.get(node, []))
|
| 83 |
return visited
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 81 |
visited.add(node)
|
| 82 |
stack.extend(reverse_graph.get(node, []))
|
| 83 |
return visited
|
| 84 |
+
|
| 85 |
+
from collections import deque, defaultdict
|
| 86 |
+
|
| 87 |
+
def build_nodes_and_edges(parsed, root_func, reachable, reverse=False, max_depth=10):
|
| 88 |
+
depth_map = {}
|
| 89 |
+
x_offset_map = defaultdict(int)
|
| 90 |
+
positions = {}
|
| 91 |
+
visited = set()
|
| 92 |
+
queue = deque([(root_func, 0)])
|
| 93 |
+
|
| 94 |
+
while queue:
|
| 95 |
+
current, depth = queue.popleft()
|
| 96 |
+
if current in visited or depth > max_depth:
|
| 97 |
+
continue
|
| 98 |
+
visited.add(current)
|
| 99 |
+
|
| 100 |
+
adjusted_depth = -depth if reverse else depth
|
| 101 |
+
x = x_offset_map[adjusted_depth] * 300
|
| 102 |
+
y = adjusted_depth * 150
|
| 103 |
+
positions[current] = {"x": x, "y": y}
|
| 104 |
+
x_offset_map[adjusted_depth] += 1
|
| 105 |
+
|
| 106 |
+
if reverse:
|
| 107 |
+
# Find callers of this function
|
| 108 |
+
next_funcs = [
|
| 109 |
+
caller for caller, meta in parsed.items()
|
| 110 |
+
if current in meta["calls"] and caller in reachable
|
| 111 |
+
]
|
| 112 |
+
else:
|
| 113 |
+
# Find callees
|
| 114 |
+
next_funcs = [
|
| 115 |
+
callee for callee in parsed[current]["calls"]
|
| 116 |
+
if callee in reachable
|
| 117 |
+
]
|
| 118 |
+
|
| 119 |
+
for nxt in next_funcs:
|
| 120 |
+
queue.append((nxt, depth + 1))
|
| 121 |
+
|
| 122 |
+
nodes = [{
|
| 123 |
+
"data": {"id": name, "label": name},
|
| 124 |
+
"position": positions.get(name, {"x": 0, "y": 0}),
|
| 125 |
+
"classes": "main" if name == root_func else ""
|
| 126 |
+
} for name in visited]
|
| 127 |
+
|
| 128 |
+
edges = []
|
| 129 |
+
for src in visited:
|
| 130 |
+
call_sequence = parsed[src]["calls"]
|
| 131 |
+
call_index = 1
|
| 132 |
+
for tgt in call_sequence:
|
| 133 |
+
if tgt in visited:
|
| 134 |
+
edges.append({
|
| 135 |
+
"data": {
|
| 136 |
+
"source": src,
|
| 137 |
+
"target": tgt,
|
| 138 |
+
"label": str(call_index)
|
| 139 |
+
},
|
| 140 |
+
"style": {
|
| 141 |
+
"line-width": 4 if call_sequence.count(tgt) > 1 else 2
|
| 142 |
+
}
|
| 143 |
+
})
|
| 144 |
+
call_index += 1
|
| 145 |
+
|
| 146 |
+
return nodes, edges
|