nrrb/code_graph
0
1import base642import dash3from dash import html, dcc, dash_table4import dash_cytoscape as cyto5from dash.dependencies import Input, Output, State6import pandas as pd7 8from utility import parse_functions_from_files, get_reachable_functions, get_backtrace_functions, build_nodes_and_edges9from introduction import get_intro_markdown10 11# Load extra layouts12cyto.load_extra_layouts()13 14app = dash.Dash(__name__, suppress_callback_exceptions=True)15server = app.server16 17 18app.layout = html.Div([19 html.H2("Function Dependency Visualizer (AST-Based)", style={20 "textAlign": "center",21 "marginTop": "20px",22 "fontSize": "28px",23 "color": "#333"24 }),25 dcc.Store(id="parsed-functions-store"),26 27 html.Div([28 html.Div([29 html.Label("Upload Files"),30 dcc.Upload(31 id="upload",32 children=html.Div("๐ค Drag and drop or click to upload Python files", style={33 "border": "2px dashed #ccc",34 "padding": "10px",35 "textAlign": "center",36 "cursor": "pointer",37 "color": "#555",38 "fontSize": "14px"39 }),40 multiple=True41 )42 ], style={"width": "24%", "display": "inline-block", "verticalAlign": "top", "marginRight": "1%"}),43 44 html.Div([45 html.Label("Main Function"),46 dcc.Dropdown(id="main-function", style={"width": "100%"})47 ], style={"width": "24%", "display": "inline-block", "verticalAlign": "top", "marginRight": "1%"}),48 49 html.Div([50 html.Label("Layout"),51 dcc.Dropdown(52 id="layout-type",53 options=[54 {"label": "Preset", "value": "preset"},55 {"label": "CoSE", "value": "cose"},56 {"label": "CoSE Bilkent", "value": "cose-bilkent"},57 {"label": "Cola", "value": "cola"},58 {"label": "Spread", "value": "spread"},59 {"label": "Dagre", "value": "dagre"},60 {"label": "Klay", "value": "klay"}61 ],62 value="preset",63 style={"width": "100%"}64 )65 ], style={"width": "24%", "display": "inline-block", "verticalAlign": "top", "marginRight": "1%"}),66 67 html.Div([68 html.Label("Max Depth"),69 dcc.Dropdown(70 id="max-depth",71 options=[{"label": str(i), "value": i} for i in range(1, 11)],72 value=10,73 clearable=False,74 style={"width": "100%"}75 )76 ], style={"width": "24%", "display": "inline-block", "verticalAlign": "top", "marginRight": "1%"}),77 78 html.Div([79 html.Label("Options"),80 dcc.Checklist(81 id="backtrace-toggle",82 options=[{"label": "Backtrace Mode", "value": "backtrace"}],83 value=[],84 labelStyle={"display": "block", "fontWeight": "normal"}85 ),86 dcc.Checklist(87 id="function-detail-toggle",88 options=[{"label": "Show Function-Level Detail in Summaray tab", "value": "show"}],89 value=[],90 labelStyle={"display": "block", "fontWeight": "normal"},91 style={"marginTop": "5px"}92 )93 ], style={"width": "24%", "display": "inline-block", "verticalAlign": "top"})94 ], style={95 "backgroundColor": "#f9f9f9",96 "padding": "15px",97 "borderRadius": "10px",98 "margin": "20px auto",99 "width": "95%",100 "boxShadow": "0 2px 6px rgba(0,0,0,0.1)"101 }),102 103 104 dcc.Tabs(id="tab-selector", value="intro", children=[105 dcc.Tab(label="๐ Introduction", value="intro"),106 dcc.Tab(label="๐ Graph Explorer", value="graph"),107 dcc.Tab(label="๐ File Summary", value="summary")108 ], style={"margin": "0 20px"}),109 110 html.Div(html.Div(id="intro-tab", style={"margin": "0 20px"}), id="intro-tab-container"),111 html.Div(id="graph-tab-container", style={"margin": "0 20px"}),112 html.Div(id="summary-tab-container", style={"margin": "0 20px"}),113 114 115 html.Footer("Built by Tomas Larsson โข MIT Licensed", style={116 "textAlign": "center",117 "fontSize": "14px",118 "marginTop": "40px",119 "marginBottom": "20px",120 "color": "#888"121 })122])123 124 125@app.callback(126 Output("main-function", "options"),127 Output("main-function", "value"),128 Output("parsed-functions-store", "data"),129 Input("upload", "contents"),130 State("upload", "filename")131)132def handle_upload(contents, filenames):133 print("=== handle_upload triggered ===")134 if not contents:135 return [], None, {}136 uploaded_files = {}137 for content, name in zip(contents, filenames):138 _, content_string = content.split(",")139 uploaded_files[name] = base64.b64decode(content_string).decode("utf-8")140 parsed = parse_functions_from_files(uploaded_files)141 142 143# options = [{"label": fn, "value": fn} for fn in parsed if parsed[fn]["calls"]]144 options = [{"label": fn, "value": fn} for fn in parsed]145 return options, options[0]["value"] if options else None, parsed146 147@app.callback(148 Output("intro-tab-container", "style"),149 Output("graph-tab-container", "style"),150 Output("summary-tab-container", "style"),151 Input("tab-selector", "value")152)153def toggle_tabs(tab):154 return (155 {"display": "block"} if tab == "intro" else {"display": "none"},156 {"display": "block"} if tab == "graph" else {"display": "none"},157 {"display": "block"} if tab == "summary" else {"display": "none"}158 )159 160@app.callback(Output("intro-tab", "children"), Input("tab-selector", "value"))161def show_intro(_):162 return get_intro_markdown()163 164from collections import deque, defaultdict165 166@app.callback(167 Output("graph-tab-container", "children"),168 Input("main-function", "value"),169 Input("backtrace-toggle", "value"),170 Input("max-depth", "value"),171 Input("layout-type", "value"),172 State("parsed-functions-store", "data")173)174def update_graph(main_func, backtrace_mode, max_depth, layout_type, parsed):175 if not parsed or not main_func:176 return html.Div("Upload files and select a main function.")177 178 graph = {k: v["calls"] for k, v in parsed.items()}179 reachable = get_backtrace_functions(main_func, graph) if "backtrace" in backtrace_mode else get_reachable_functions(main_func, graph)180 181 nodes, edges = build_nodes_and_edges(parsed, main_func, reachable, reverse="backtrace" in backtrace_mode, max_depth=max_depth)182 183 184 return cyto.Cytoscape(185 id="cytoscape-graph",186 layout={"name": layout_type},187 style={"width": "100%", "height": "600px"},188 elements=nodes + edges,189 stylesheet=[190 {"selector": "node", "style": {191 "label": "data(label)",192 "text-wrap": "wrap",193 "text-valign": "bottom",194 "text-halign": "center"195 }},196 {"selector": ".main", "style": {197 "background-color": "blue",198 "line-color": "blue",199 "color": "black",200 "label": "data(label)",201 "text-wrap": "wrap",202 "text-valign": "bottom",203 "text-halign": "center"204 }},205 {"selector": "edge", "style": {206 "label": "data(label)",207 "curve-style": "bezier",208 "target-arrow-shape": "triangle",209 "target-arrow-color": "#888",210 "line-color": "#888",211 "arrow-scale": 2,212 "font-size": "14px",213 "text-margin-y": -10,214 "text-margin-x": 10,215 }}216 ],217 userZoomingEnabled=True,218 userPanningEnabled=True,219 minZoom=0.2,220 maxZoom=2,221 wheelSensitivity=0.1222 )223 224 225@app.callback(226 Output("summary-tab-container", "children"),227 Input("parsed-functions-store", "data"),228 Input("function-detail-toggle", "value")229)230def update_summary(parsed, toggle):231 if not parsed:232 return html.Div("No summary available.")233 reverse_calls = {k: [] for k in parsed}234 for caller, meta in parsed.items():235 for callee in meta["calls"]:236 reverse_calls[callee].append(caller)237 238 if "show" in toggle:239 df = pd.DataFrame([{240 "Function": fn,241 "File": parsed[fn]["filename"],242 "Arguments": ", ".join(parsed[fn]["args"]),243 "Returns": ", ".join(parsed[fn]["returns"]),244 "Reads State": ", ".join(parsed[fn]["reads_state"]),245 "Writes State": ", ".join(parsed[fn]["writes_state"]),246 "Called By": len(reverse_calls[fn])247 } for fn in parsed])248 else:249 file_summary = {}250 for func, meta in parsed.items():251 fname = meta["filename"]252 file_summary.setdefault(fname, {"Total": 0, "Calls Others": 0, "Called By Others": 0, "Unused": 0})253 file_summary[fname]["Total"] += 1254 if meta["calls"]: file_summary[fname]["Calls Others"] += 1255 if reverse_calls[func]: file_summary[fname]["Called By Others"] += 1256 if not meta["calls"] and not reverse_calls[func]: file_summary[fname]["Unused"] += 1257 df = pd.DataFrame([{"File": f, **stats} for f, stats in file_summary.items()])258 259 return dash_table.DataTable(260 data=df.to_dict("records"),261 columns=[{"name": c, "id": c} for c in df.columns],262 style_table={"overflowX": "auto"},263 style_cell={"whiteSpace": "normal", "textAlign": "left", "padding": "5px", "maxWidth": 300}264 )265 266if __name__ == "__main__":267 app.run(host="0.0.0.0", port=7860)