Tom-Dev-space/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 11app = dash.Dash(__name__, suppress_callback_exceptions=True)12server = app.server13 14 15app.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=True38 )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 in Summaray tab", "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"),107 Output("parsed-functions-store", "data"),108 Input("upload", "contents"),109 State("upload", "filename")110)111def handle_upload(contents, filenames):112 print("=== handle_upload triggered ===")113 if not contents:114 return [], None, {}115 uploaded_files = {}116 for content, name in zip(contents, filenames):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 121 122# options = [{"label": fn, "value": fn} for fn in parsed if parsed[fn]["calls"]]123 options = [{"label": fn, "value": fn} for fn in parsed]124 return options, options[0]["value"] if options else None, parsed125 126@app.callback(127 Output("intro-tab-container", "style"),128 Output("graph-tab-container", "style"),129 Output("summary-tab-container", "style"),130 Input("tab-selector", "value")131)132def toggle_tabs(tab):133 return (134 {"display": "block"} if tab == "intro" else {"display": "none"},135 {"display": "block"} if tab == "graph" else {"display": "none"},136 {"display": "block"} if tab == "summary" else {"display": "none"}137 )138 139@app.callback(Output("intro-tab", "children"), Input("tab-selector", "value"))140def show_intro(_):141 return get_intro_markdown()142 143from collections import deque, defaultdict144 145@app.callback(146 Output("graph-tab-container", "children"),147 Input("main-function", "value"),148 Input("backtrace-toggle", "value"),149 Input("max-depth", "value"),150 State("parsed-functions-store", "data")151)152def update_graph(main_func, backtrace_mode, max_depth, parsed):153 if not parsed or not main_func:154 return html.Div("Upload files and select a main function.")155 156 graph = {k: v["calls"] for k, v in parsed.items()}157 reachable = get_backtrace_functions(main_func, graph) if "backtrace" in backtrace_mode else get_reachable_functions(main_func, graph)158 159 nodes, edges = build_nodes_and_edges(parsed, main_func, reachable, reverse="backtrace" in backtrace_mode, max_depth=max_depth)160 161 162 return cyto.Cytoscape(163 id="cytoscape-graph",164 layout={"name": "preset"},165 style={"width": "100%", "height": "600px"},166 elements=nodes + edges,167 stylesheet=[168 {"selector": "node", "style": {169 "label": "data(label)",170 "text-wrap": "wrap",171 "text-valign": "bottom",172 "text-halign": "center"173 }},174 {"selector": ".main", "style": {175 "background-color": "blue",176 "line-color": "blue",177 "color": "black",178 "label": "data(label)",179 "text-wrap": "wrap",180 "text-valign": "bottom",181 "text-halign": "center"182 }},183 {"selector": "edge", "style": {184 "label": "data(label)",185 "curve-style": "bezier",186 "target-arrow-shape": "triangle",187 "target-arrow-color": "#888",188 "line-color": "#888",189 "arrow-scale": 2,190 "font-size": "14px",191 "text-margin-y": -10,192 "text-margin-x": 10,193 }}194 ],195 userZoomingEnabled=True,196 userPanningEnabled=True,197 minZoom=0.2,198 maxZoom=2,199 wheelSensitivity=0.1200 )201 202 203@app.callback(204 Output("summary-tab-container", "children"),205 Input("parsed-functions-store", "data"),206 Input("function-detail-toggle", "value")207)208def update_summary(parsed, toggle):209 if not parsed:210 return html.Div("No summary available.")211 reverse_calls = {k: [] for k in parsed}212 for caller, meta in parsed.items():213 for callee in meta["calls"]:214 reverse_calls[callee].append(caller)215 216 if "show" in toggle:217 df = pd.DataFrame([{218 "Function": fn,219 "File": parsed[fn]["filename"],220 "Arguments": ", ".join(parsed[fn]["args"]),221 "Returns": ", ".join(parsed[fn]["returns"]),222 "Reads State": ", ".join(parsed[fn]["reads_state"]),223 "Writes State": ", ".join(parsed[fn]["writes_state"]),224 "Called By": len(reverse_calls[fn])225 } for fn in parsed])226 else:227 file_summary = {}228 for func, meta in parsed.items():229 fname = meta["filename"]230 file_summary.setdefault(fname, {"Total": 0, "Calls Others": 0, "Called By Others": 0, "Unused": 0})231 file_summary[fname]["Total"] += 1232 if meta["calls"]: file_summary[fname]["Calls Others"] += 1233 if reverse_calls[func]: file_summary[fname]["Called By Others"] += 1234 if not meta["calls"] and not reverse_calls[func]: file_summary[fname]["Unused"] += 1235 df = pd.DataFrame([{"File": f, **stats} for f, stats in file_summary.items()])236 237 return dash_table.DataTable(238 data=df.to_dict("records"),239 columns=[{"name": c, "id": c} for c in df.columns],240 style_table={"overflowX": "auto"},241 style_cell={"whiteSpace": "normal", "textAlign": "left", "padding": "5px", "maxWidth": 300}242 )243 244if __name__ == "__main__":245 app.run(host="0.0.0.0", port=7860)