Boray commited on
Commit
2d120d9
·
0 Parent(s):

first commit

Browse files
Files changed (9) hide show
  1. .gitattributes +35 -0
  2. .gitignore +62 -0
  3. .streamlit/config.toml +7 -0
  4. Dockerfile +20 -0
  5. README.md +200 -0
  6. app.py +680 -0
  7. requirements.txt +4 -0
  8. src/sample_report.json +308 -0
  9. src/streamlit_app.py +680 -0
.gitattributes ADDED
@@ -0,0 +1,35 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ *.7z filter=lfs diff=lfs merge=lfs -text
2
+ *.arrow filter=lfs diff=lfs merge=lfs -text
3
+ *.bin filter=lfs diff=lfs merge=lfs -text
4
+ *.bz2 filter=lfs diff=lfs merge=lfs -text
5
+ *.ckpt filter=lfs diff=lfs merge=lfs -text
6
+ *.ftz filter=lfs diff=lfs merge=lfs -text
7
+ *.gz filter=lfs diff=lfs merge=lfs -text
8
+ *.h5 filter=lfs diff=lfs merge=lfs -text
9
+ *.joblib filter=lfs diff=lfs merge=lfs -text
10
+ *.lfs.* filter=lfs diff=lfs merge=lfs -text
11
+ *.mlmodel filter=lfs diff=lfs merge=lfs -text
12
+ *.model filter=lfs diff=lfs merge=lfs -text
13
+ *.msgpack filter=lfs diff=lfs merge=lfs -text
14
+ *.npy filter=lfs diff=lfs merge=lfs -text
15
+ *.npz filter=lfs diff=lfs merge=lfs -text
16
+ *.onnx filter=lfs diff=lfs merge=lfs -text
17
+ *.ot filter=lfs diff=lfs merge=lfs -text
18
+ *.parquet filter=lfs diff=lfs merge=lfs -text
19
+ *.pb filter=lfs diff=lfs merge=lfs -text
20
+ *.pickle filter=lfs diff=lfs merge=lfs -text
21
+ *.pkl filter=lfs diff=lfs merge=lfs -text
22
+ *.pt filter=lfs diff=lfs merge=lfs -text
23
+ *.pth filter=lfs diff=lfs merge=lfs -text
24
+ *.rar filter=lfs diff=lfs merge=lfs -text
25
+ *.safetensors filter=lfs diff=lfs merge=lfs -text
26
+ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
27
+ *.tar.* filter=lfs diff=lfs merge=lfs -text
28
+ *.tar filter=lfs diff=lfs merge=lfs -text
29
+ *.tflite filter=lfs diff=lfs merge=lfs -text
30
+ *.tgz filter=lfs diff=lfs merge=lfs -text
31
+ *.wasm filter=lfs diff=lfs merge=lfs -text
32
+ *.xz filter=lfs diff=lfs merge=lfs -text
33
+ *.zip filter=lfs diff=lfs merge=lfs -text
34
+ *.zst filter=lfs diff=lfs merge=lfs -text
35
+ *tfevents* filter=lfs diff=lfs merge=lfs -text
.gitignore ADDED
@@ -0,0 +1,62 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Byte-compiled / optimized / DLL files
2
+ __pycache__/
3
+ *.py[cod]
4
+ *$py.class
5
+
6
+ # C extensions
7
+ *.so
8
+
9
+ # Distribution / packaging
10
+ .Python
11
+ build/
12
+ develop-eggs/
13
+ dist/
14
+ downloads/
15
+ eggs/
16
+ .eggs/
17
+ lib/
18
+ lib64/
19
+ parts/
20
+ sdist/
21
+ var/
22
+ wheels/
23
+ pip-wheel-metadata/
24
+ share/python-wheels/
25
+ *.egg-info/
26
+ .installed.cfg
27
+ *.egg
28
+ MANIFEST
29
+
30
+ # PyInstaller
31
+ *.manifest
32
+ *.spec
33
+
34
+ # Streamlit
35
+ # .streamlit/
36
+
37
+ # Virtual environments
38
+ venv/
39
+ ENV/
40
+ env/
41
+
42
+ # IDE
43
+ .vscode/
44
+ .idea/
45
+ *.swp
46
+ *.swo
47
+ *~
48
+
49
+ # OS
50
+ .DS_Store
51
+ .DS_Store?
52
+ ._*
53
+ .Spotlight-V100
54
+ .Trashes
55
+ ehthumbs.db
56
+ Thumbs.db
57
+
58
+ # Logs
59
+ *.log
60
+
61
+ # Reports
62
+ report.json
.streamlit/config.toml ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ [server]
2
+ enableCORS = false
3
+ enableXsrfProtection = false
4
+ maxUploadSize = 200
5
+
6
+ [browser]
7
+ gatherUsageStats = false
Dockerfile ADDED
@@ -0,0 +1,20 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM python:3.13.5-slim
2
+
3
+ WORKDIR /app
4
+
5
+ RUN apt-get update && apt-get install -y \
6
+ build-essential \
7
+ curl \
8
+ git \
9
+ && rm -rf /var/lib/apt/lists/*
10
+
11
+ COPY requirements.txt ./
12
+ COPY src/ ./src/
13
+
14
+ RUN pip3 install -r requirements.txt
15
+
16
+ EXPOSE 8501
17
+
18
+ HEALTHCHECK CMD curl --fail http://localhost:8501/_stcore/health
19
+
20
+ ENTRYPOINT ["streamlit", "run", "src/streamlit_app.py", "--server.port=8501", "--server.address=0.0.0.0"]
README.md ADDED
@@ -0,0 +1,200 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ title: Radon CC Analysis
3
+ emoji: 📊
4
+ colorFrom: blue
5
+ colorTo: green
6
+ sdk: streamlit
7
+ sdk_version: "1.40.0"
8
+ app_file: app.py
9
+ pinned: false
10
+ ---
11
+
12
+ # Radon CC Reader - Cyclomatic Complexity Analyzer
13
+
14
+ A modern Streamlit web application for analyzing and visualizing Python cyclomatic complexity reports from the [radon](https://radon.readthedocs.io/) library.
15
+
16
+ ## Features
17
+
18
+ ### 📈 Overview Tab
19
+ - Quick metrics: total items, files, average and max complexity
20
+ - Complexity distribution chart
21
+ - Grade distribution chart
22
+ - Scatter plot of complexity by file and grade
23
+
24
+ ### 🔍 Analysis Tab
25
+ - **Filtering**: Filter by type, grade, complexity range, and files
26
+ - **Sorting**: Sort by complexity, grade, name, file, or line number
27
+ - **Search**: Search for specific functions/classes by name
28
+ - **Color Coding**: Visual indicators for grades and complexity levels
29
+ - Grade A (Green) → F (Dark Red)
30
+ - Simple (Green) → Very Complex (Dark Red)
31
+
32
+ ### ⚠️ Warnings Tab
33
+ - Identifies items requiring investigation
34
+ - Configurable complexity threshold
35
+ - Configurable risk grades
36
+ - Detailed recommendations for each risky item
37
+ - Expandable item details with severity indicators
38
+
39
+ ### 📋 Details Tab
40
+ - Detailed analysis of individual items
41
+ - Full metrics and location information
42
+ - Automated recommendations based on complexity
43
+ - Quality assessment
44
+
45
+ ## Installation
46
+
47
+ 1. **Clone or navigate to the repository:**
48
+ ```bash
49
+ cd c:\repos\radoncc-reader
50
+ ```
51
+
52
+ 2. **Install dependencies:**
53
+ ```bash
54
+ pip install -r requirements.txt
55
+ ```
56
+
57
+ ## Usage
58
+
59
+ ### Generate a Radon Report
60
+
61
+ First, generate a cyclomatic complexity report from your Python project using radon:
62
+
63
+ ```bash
64
+ # Generate JSON report for a single file
65
+ radon cc path/to/file.py -j > report.json
66
+
67
+ # Generate JSON report for entire project
68
+ radon cc path/to/project/ -j > report.json
69
+ ```
70
+
71
+ ### Run the App
72
+
73
+ ```bash
74
+ streamlit run app.py
75
+ ```
76
+
77
+ The app will open in your browser at `http://localhost:8501`
78
+
79
+ ### Upload Report
80
+
81
+ 1. Click the file uploader in the left sidebar
82
+ 2. Select your JSON report file
83
+ 3. The app will parse and display the data
84
+
85
+ ### Or Try Sample Data
86
+
87
+ Click the "Load Sample Report" button on the home page to see a demo with sample data.
88
+
89
+ ## JSON Report Structure
90
+
91
+ The app expects a JSON file in the following structure:
92
+
93
+ ```json
94
+ {
95
+ "file/path.py": [
96
+ {
97
+ "type": "class",
98
+ "rank": "A",
99
+ "lineno": 7,
100
+ "complexity": 1,
101
+ "endline": 8,
102
+ "name": "ClassName",
103
+ "col_offset": 0,
104
+ "methods": [
105
+ {
106
+ "type": "method",
107
+ "rank": "A",
108
+ "lineno": 10,
109
+ "classname": "ClassName",
110
+ "complexity": 3,
111
+ "endline": 20,
112
+ "name": "method_name",
113
+ "col_offset": 4,
114
+ "closures": []
115
+ }
116
+ ]
117
+ },
118
+ {
119
+ "type": "function",
120
+ "rank": "B",
121
+ "lineno": 30,
122
+ "complexity": 5,
123
+ "endline": 40,
124
+ "name": "function_name",
125
+ "col_offset": 0
126
+ }
127
+ ]
128
+ }
129
+ ```
130
+
131
+ ## Complexity Grades
132
+
133
+ - **A**: Low complexity (1-3) - Simple and easy to maintain
134
+ - **B**: Low complexity (4-7) - Moderately complex
135
+ - **C**: Moderate complexity - Review recommended
136
+ - **D**: High complexity - Refactoring recommended
137
+ - **F**: Very high complexity (10+) - Critical refactoring needed
138
+
139
+ ## Features & Functionality
140
+
141
+ ### Color Coding System
142
+ - **Grades**: A (Green) → B (Orange) → C-D (Red) → F (Dark Red)
143
+ - **Complexity**: 1-3 (Green) → 4-7 (Orange) → 8-10 (Red) → 10+ (Dark Red)
144
+
145
+ ### Filtering Capabilities
146
+ - Filter by item type (class, function, method, closure)
147
+ - Filter by grade (A, B, C, D, F)
148
+ - Filter by complexity range with slider
149
+ - Filter by specific files
150
+ - Search by name with text input
151
+
152
+ ### Sorting Options
153
+ - Complexity (High→Low or Low→High)
154
+ - Grade (Best→Worst)
155
+ - Name (A→Z)
156
+ - File Path
157
+ - Line Number
158
+
159
+ ### Warnings System
160
+ - Automatic identification of risky code
161
+ - Configurable thresholds
162
+ - Severity-based recommendations
163
+ - Critical items highlighted
164
+ - Up to 20 most critical items shown with detailed analysis
165
+
166
+ ### Detailed Analysis
167
+ - Individual item inspection
168
+ - Full metrics display
169
+ - Location and file information
170
+ - Automated recommendations
171
+ - Quality assessment
172
+
173
+ ## Tips
174
+
175
+ 1. **Code Quality Focus**: Use the Warnings tab to find the most complex code that needs refactoring
176
+ 2. **Batch Refactoring**: Sort by complexity to systematically address the most problematic code
177
+ 3. **Grade Tracking**: Monitor grade improvements as you refactor
178
+ 4. **Search for Patterns**: Use the search function to find all methods matching a pattern
179
+
180
+ ## Technical Stack
181
+
182
+ - **Streamlit**: Web app framework
183
+ - **Pandas**: Data manipulation and filtering
184
+ - **Plotly**: Interactive visualizations
185
+ - **Python 3.7+**
186
+
187
+ ## Notes
188
+
189
+ - This app runs entirely locally - your code reports are never sent anywhere
190
+ - The app handles nested methods and closures automatically
191
+ - Large reports (1000+ items) may take a moment to load and filter
192
+ - All filtering and sorting happens in-memory
193
+
194
+ ## License
195
+
196
+ MIT
197
+
198
+ ---
199
+
200
+ **Made for Python developers who care about code quality!** 🚀
app.py ADDED
@@ -0,0 +1,680 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import json
2
+ from pathlib import Path
3
+ from typing import Any, Dict, List
4
+
5
+ import pandas as pd
6
+ import plotly.express as px
7
+ import plotly.graph_objects as go
8
+ import streamlit as st
9
+
10
+ st.set_page_config(
11
+ page_title="Radon Complexity Analyzer",
12
+ page_icon="📊",
13
+ layout="wide",
14
+ initial_sidebar_state="expanded",
15
+ )
16
+
17
+ # Custom CSS for better styling
18
+ st.markdown(
19
+ """
20
+ <style>
21
+ .grade-A { color: #2ecc71; font-weight: bold; }
22
+ .grade-B { color: #f39c12; font-weight: bold; }
23
+ .grade-C { color: #e74c3c; font-weight: bold; }
24
+ .grade-D { color: #e67e22; font-weight: bold; }
25
+ .grade-F { color: #c0392b; font-weight: bold; }
26
+ .metric-high { background-color: #ffe6e6; }
27
+ .metric-medium { background-color: #fff3cd; }
28
+ .metric-low { background-color: #d4edda; }
29
+ </style>
30
+ """,
31
+ unsafe_allow_html=True,
32
+ )
33
+
34
+
35
+ def get_grade_color(grade: str) -> str:
36
+ """Get color for grade"""
37
+ colors = {
38
+ "A": "#2ecc71", # Green
39
+ "B": "#f39c12", # Orange
40
+ "C": "#e74c3c", # Red
41
+ "D": "#e67e22", # Dark Orange
42
+ "E": "#d35400", # Darker Orange
43
+ "F": "#c0392b", # Dark Red
44
+ }
45
+ return colors.get(grade, "#95a5a6")
46
+
47
+
48
+ def get_complexity_color(complexity: int, high_threshold: int = 10) -> str:
49
+ """Get color based on complexity value"""
50
+ if complexity <= 3:
51
+ return "#2ecc71" # Green - Simple
52
+ elif complexity <= 7:
53
+ return "#f39c12" # Orange - Moderate
54
+ elif complexity <= high_threshold:
55
+ return "#e74c3c" # Red - Complex
56
+ else:
57
+ return "#c0392b" # Dark Red - Very Complex
58
+
59
+
60
+ def flatten_report(report: Dict[str, List[Dict]]) -> pd.DataFrame:
61
+ """Convert nested JSON report to flattened DataFrame"""
62
+ rows = []
63
+
64
+ for filepath, items in report.items():
65
+ if not isinstance(items, list):
66
+ continue
67
+
68
+ for item in items:
69
+ row = {
70
+ "filepath": filepath,
71
+ "type": item.get("type", "N/A"),
72
+ "name": item.get("name", "N/A"),
73
+ "classname": item.get("classname", ""),
74
+ "complexity": item.get("complexity", 0),
75
+ "rank": item.get("rank", "N/A"),
76
+ "lineno": item.get("lineno", 0),
77
+ "endline": item.get("endline", 0),
78
+ "col_offset": item.get("col_offset", 0),
79
+ }
80
+ rows.append(row)
81
+
82
+ # Add nested methods/closures
83
+ if item.get("methods"):
84
+ for method in item["methods"]:
85
+ method_row = row.copy()
86
+ method_row.update(
87
+ {
88
+ "type": method.get("type", "method"),
89
+ "name": method.get("name", "N/A"),
90
+ "complexity": method.get("complexity", 0),
91
+ "rank": method.get("rank", "N/A"),
92
+ "lineno": method.get("lineno", 0),
93
+ "endline": method.get("endline", 0),
94
+ "col_offset": method.get("col_offset", 0),
95
+ "parent_name": item.get("name", ""),
96
+ }
97
+ )
98
+ rows.append(method_row)
99
+
100
+ if item.get("closures"):
101
+ for closure in item["closures"]:
102
+ closure_row = row.copy()
103
+ closure_row.update(
104
+ {
105
+ "type": closure.get("type", "closure"),
106
+ "name": closure.get("name", "N/A"),
107
+ "complexity": closure.get("complexity", 0),
108
+ "rank": closure.get("rank", "N/A"),
109
+ "lineno": closure.get("lineno", 0),
110
+ "endline": closure.get("endline", 0),
111
+ "col_offset": closure.get("col_offset", 0),
112
+ "parent_name": item.get("name", ""),
113
+ }
114
+ )
115
+ rows.append(closure_row)
116
+
117
+ return pd.DataFrame(rows)
118
+
119
+
120
+ def display_grade_badge(grade: str) -> str:
121
+ """Create colored grade badge"""
122
+ color = get_grade_color(grade)
123
+ return f'<span style="background-color: {color}; color: white; padding: 4px 8px; border-radius: 4px; font-weight: bold;">{grade}</span>'
124
+
125
+
126
+ def identify_risky_items(
127
+ df: pd.DataFrame, complexity_threshold: int = 10, risky_grades: List[str] = None
128
+ ) -> pd.DataFrame:
129
+ """Identify items that need investigation"""
130
+ if risky_grades is None:
131
+ risky_grades = ["D", "E", "F"]
132
+
133
+ risky = df[
134
+ (df["complexity"] >= complexity_threshold) | (df["rank"].isin(risky_grades))
135
+ ]
136
+ return risky.sort_values("complexity", ascending=False)
137
+
138
+
139
+ def create_complexity_chart(df: pd.DataFrame):
140
+ """Create a chart showing complexity distribution"""
141
+ complexity_dist = df["complexity"].value_counts().sort_index()
142
+ fig = go.Figure(data=[go.Bar(x=complexity_dist.index, y=complexity_dist.values)])
143
+ fig.update_layout(
144
+ title="Complexity Distribution",
145
+ xaxis_title="Complexity Level",
146
+ yaxis_title="Count",
147
+ hovermode="x unified",
148
+ )
149
+ return fig
150
+
151
+
152
+ def create_grade_chart(df: pd.DataFrame):
153
+ """Create a chart showing grade distribution"""
154
+ grade_dist = df["rank"].value_counts()
155
+ grade_order = ["A", "B", "C", "D", "E", "F"]
156
+ grade_dist = grade_dist.reindex(
157
+ [g for g in grade_order if g in grade_dist.index], fill_value=0
158
+ )
159
+
160
+ colors = [get_grade_color(g) for g in grade_dist.index]
161
+ fig = go.Figure(
162
+ data=[go.Bar(x=grade_dist.index, y=grade_dist.values, marker_color=colors)]
163
+ )
164
+ fig.update_layout(
165
+ title="Grade Distribution",
166
+ xaxis_title="Grade",
167
+ yaxis_title="Count",
168
+ hovermode="x unified",
169
+ )
170
+ return fig
171
+
172
+
173
+ def create_scatter_plot(df: pd.DataFrame):
174
+ """Create scatter plot of complexity vs files"""
175
+ # Add a column with just the filename for display
176
+ df_plot = df.copy()
177
+ df_plot["filename"] = df_plot["filepath"].apply(lambda x: x.split("/")[-1])
178
+
179
+ fig = px.scatter(
180
+ df_plot,
181
+ x="filename",
182
+ y="complexity",
183
+ color="rank",
184
+ hover_data=["name", "type", "lineno", "filepath"],
185
+ title="Complexity by File and Grade",
186
+ color_discrete_map={g: get_grade_color(g) for g in df_plot["rank"].unique()},
187
+ height=600,
188
+ )
189
+ fig.update_layout(xaxis_tickangle=-45, xaxis_title="File")
190
+ return fig
191
+
192
+
193
+ # Initialize session state
194
+ if "report_data" not in st.session_state:
195
+ st.session_state.report_data = None
196
+ if "df" not in st.session_state:
197
+ st.session_state.df = None
198
+
199
+ # Sidebar for file upload
200
+ st.sidebar.title("📊 Radon Report Analyzer")
201
+
202
+ # File upload
203
+ uploaded_file = st.sidebar.file_uploader(
204
+ "Upload JSON Report",
205
+ type=["json"],
206
+ help="Upload the cyclomatic complexity report from radon library",
207
+ )
208
+
209
+ if uploaded_file:
210
+ try:
211
+ report_data = json.load(uploaded_file)
212
+ st.session_state.report_data = report_data
213
+ st.session_state.df = flatten_report(report_data)
214
+ st.sidebar.success("✅ Report loaded successfully!")
215
+ except json.JSONDecodeError:
216
+ st.sidebar.error("❌ Invalid JSON file")
217
+ except Exception as e:
218
+ st.sidebar.error(f"❌ Error loading file: {str(e)}")
219
+
220
+ # Main app logic
221
+ if st.session_state.df is not None and len(st.session_state.df) > 0:
222
+ df = st.session_state.df.copy()
223
+
224
+ # drop duplicate rows by name (drop the one with NaN parrent)
225
+ df = df.drop_duplicates(subset=["name", "filepath", "lineno"], keep="first")
226
+
227
+ # Create tabs
228
+ tab1, tab2, tab3, tab4 = st.tabs(
229
+ ["📈 Overview", "🔍 Analysis", "⚠️ Warnings", "📋 Details"]
230
+ )
231
+
232
+ # ===== TAB 1: OVERVIEW =====
233
+ with tab1:
234
+ col1, col2, col3, col4 = st.columns(4)
235
+
236
+ with col1:
237
+ st.metric("Total Items", len(df))
238
+ with col2:
239
+ st.metric("Total Files", df["filepath"].nunique())
240
+ with col3:
241
+ avg_complexity = df["complexity"].mean()
242
+ st.metric("Avg Complexity", f"{avg_complexity:.2f}")
243
+ with col4:
244
+ max_complexity = df["complexity"].max()
245
+ st.metric("Max Complexity", max_complexity)
246
+
247
+ st.divider()
248
+
249
+ col1, col2 = st.columns(2)
250
+ with col1:
251
+ st.plotly_chart(create_complexity_chart(df), use_container_width=True)
252
+ with col2:
253
+ st.plotly_chart(create_grade_chart(df), use_container_width=True)
254
+
255
+ st.divider()
256
+ st.subheader("📍 Complexity by File and Grade")
257
+
258
+ # File filter for scatter plot - show only filename, not full path
259
+ filepath_to_filename = {fp: fp.split("/")[-1] for fp in df["filepath"].unique()}
260
+ filename_to_filepath = {v: k for k, v in filepath_to_filename.items()}
261
+
262
+ # Initialize selected files in session state if not exists
263
+ if "selected_scatter_files" not in st.session_state:
264
+ st.session_state.selected_scatter_files = sorted(
265
+ filepath_to_filename.values()
266
+ )
267
+
268
+ # Select all / Remove all buttons
269
+ col_btn1, col_btn2, col_spacer = st.columns([1, 1, 6])
270
+ with col_btn1:
271
+ if st.button("Select All", use_container_width=True):
272
+ st.session_state.selected_scatter_files = sorted(
273
+ filepath_to_filename.values()
274
+ )
275
+ st.rerun()
276
+ with col_btn2:
277
+ if st.button("Remove All", use_container_width=True):
278
+ st.session_state.selected_scatter_files = []
279
+ st.rerun()
280
+
281
+ st.write("**Select files to display:**")
282
+ scatter_file_filter_display = st.pills(
283
+ "Filter files",
284
+ options=sorted(filepath_to_filename.values()),
285
+ selection_mode="multi",
286
+ default=st.session_state.selected_scatter_files,
287
+ label_visibility="collapsed",
288
+ key="scatter_plot_file_filter",
289
+ )
290
+
291
+ # Update session state
292
+ st.session_state.selected_scatter_files = (
293
+ scatter_file_filter_display if scatter_file_filter_display else []
294
+ )
295
+
296
+ # Convert selected filenames back to full paths
297
+ scatter_file_filter = [
298
+ filename_to_filepath[fn] for fn in (scatter_file_filter_display or [])
299
+ ]
300
+
301
+ # Apply file filter for scatter plot
302
+ if scatter_file_filter:
303
+ scatter_df = df[df["filepath"].isin(scatter_file_filter)]
304
+ else:
305
+ scatter_df = pd.DataFrame() # Empty dataframe when no files selected
306
+
307
+ if len(scatter_df) > 0:
308
+ st.plotly_chart(create_scatter_plot(scatter_df), use_container_width=True)
309
+ else:
310
+ st.info("No data to display. Please select at least one file.")
311
+
312
+ # ===== TAB 2: ANALYSIS WITH FILTERS =====
313
+ with tab2:
314
+ st.subheader("Filter & Sort Data")
315
+
316
+ col1, col2, col3, col4 = st.columns(4)
317
+
318
+ with col1:
319
+ type_filter = st.multiselect(
320
+ "Type",
321
+ options=df["type"].unique(),
322
+ default=df["type"].unique(),
323
+ help="Filter by item type",
324
+ )
325
+
326
+ with col2:
327
+ grade_filter = st.multiselect(
328
+ "Grade",
329
+ options=sorted(df["rank"].unique()),
330
+ default=sorted(df["rank"].unique()),
331
+ help="Filter by grade",
332
+ )
333
+
334
+ with col3:
335
+ complexity_range = st.slider(
336
+ "Complexity Range",
337
+ min_value=int(df["complexity"].min()),
338
+ max_value=int(df["complexity"].max()),
339
+ value=(int(df["complexity"].min()), int(df["complexity"].max())),
340
+ help="Filter by complexity level",
341
+ )
342
+
343
+ with col4:
344
+ filepath_filter = st.multiselect(
345
+ "Files",
346
+ options=sorted(df["filepath"].unique()),
347
+ default=sorted(df["filepath"].unique()),
348
+ help="Filter by file",
349
+ )
350
+
351
+ # Apply filters
352
+ filtered_df = df[
353
+ (df["type"].isin(type_filter))
354
+ & (df["rank"].isin(grade_filter))
355
+ & (df["complexity"] >= complexity_range[0])
356
+ & (df["complexity"] <= complexity_range[1])
357
+ & (df["filepath"].isin(filepath_filter))
358
+ ]
359
+
360
+ col1, col2 = st.columns(2)
361
+ with col1:
362
+ sort_by = st.selectbox(
363
+ "Sort by",
364
+ options=[
365
+ "Complexity (High→Low)",
366
+ "Complexity (Low→High)",
367
+ "Grade (Best→Worst)",
368
+ "Name (A→Z)",
369
+ "File Path",
370
+ "Line Number",
371
+ ],
372
+ help="Sort the filtered results",
373
+ )
374
+
375
+ with col2:
376
+ search_term = st.text_input(
377
+ "Search by name", help="Search for specific function/class names"
378
+ )
379
+
380
+ # Apply sorting
381
+ if sort_by == "Complexity (High→Low)":
382
+ filtered_df = filtered_df.sort_values("complexity", ascending=False)
383
+ elif sort_by == "Complexity (Low→High)":
384
+ filtered_df = filtered_df.sort_values("complexity", ascending=True)
385
+ elif sort_by == "Grade (Best→Worst)":
386
+ grade_order = {"A": 1, "B": 2, "C": 3, "D": 4, "F": 5}
387
+ filtered_df = filtered_df.sort_values(
388
+ "rank", key=lambda x: x.map(grade_order)
389
+ )
390
+ elif sort_by == "Name (A→Z)":
391
+ filtered_df = filtered_df.sort_values("name")
392
+ elif sort_by == "File Path":
393
+ filtered_df = filtered_df.sort_values("filepath")
394
+ elif sort_by == "Line Number":
395
+ filtered_df = filtered_df.sort_values("lineno")
396
+
397
+ # Apply search
398
+ if search_term:
399
+ filtered_df = filtered_df[
400
+ filtered_df["name"].str.contains(search_term, case=False, na=False)
401
+ ]
402
+
403
+ st.info(f"Showing {len(filtered_df)} of {len(df)} items")
404
+
405
+ # Display table with color coding
406
+ def style_dataframe(val, column):
407
+ if column == "rank":
408
+ color = get_grade_color(val)
409
+ return f"background-color: {color}; color: white; font-weight: bold;"
410
+ elif column == "complexity":
411
+ color = get_complexity_color(int(val))
412
+ return f"background-color: {color}; color: white;"
413
+ return ""
414
+
415
+ display_df = filtered_df[
416
+ ["filepath", "type", "name", "complexity", "rank", "lineno", "endline"]
417
+ ].copy()
418
+ display_df = display_df.reset_index(drop=True)
419
+
420
+ st.dataframe(
421
+ display_df,
422
+ use_container_width=True,
423
+ column_config={
424
+ "complexity": st.column_config.NumberColumn(width="small"),
425
+ "rank": st.column_config.TextColumn(width="small"),
426
+ "lineno": st.column_config.NumberColumn(width="small"),
427
+ "endline": st.column_config.NumberColumn(width="small"),
428
+ "type": st.column_config.TextColumn(width="small"),
429
+ },
430
+ )
431
+
432
+ # ===== TAB 3: WARNINGS =====
433
+ with tab3:
434
+ st.subheader("⚠️ Items Requiring Investigation")
435
+
436
+ col1, col2 = st.columns(2)
437
+ with col1:
438
+ complexity_threshold = st.slider(
439
+ "Complexity Threshold",
440
+ min_value=1,
441
+ max_value=int(df["complexity"].max()),
442
+ value=10,
443
+ help="Items with complexity >= this value will be flagged",
444
+ )
445
+
446
+ with col2:
447
+ risky_grades = st.multiselect(
448
+ "Risky Grades",
449
+ options=["A", "B", "C", "D", "E", "F"],
450
+ default=["D", "E", "F"],
451
+ help="Grades considered risky",
452
+ )
453
+
454
+ risky_df = identify_risky_items(df, complexity_threshold, risky_grades)
455
+
456
+ if len(risky_df) > 0:
457
+ st.warning(f"⚠️ Found {len(risky_df)} items that need investigation")
458
+
459
+ # Group by severity
460
+ col1, col2 = st.columns(2)
461
+ with col1:
462
+ high_risk = risky_df[risky_df["complexity"] >= complexity_threshold + 5]
463
+ st.metric("High Risk (Very High Complexity)", len(high_risk))
464
+
465
+ with col2:
466
+ bad_grade = risky_df[risky_df["rank"].isin(["D", "F"])]
467
+ st.metric("Bad Grade Items", len(bad_grade))
468
+
469
+ st.divider()
470
+
471
+ # Detailed view of risky items
472
+ for idx, (_, row) in enumerate(risky_df.head(20).iterrows(), 1):
473
+ with st.expander(
474
+ f"🚨 {row['name']} (Complexity: {row['complexity']}, Grade: {row['rank']})",
475
+ expanded=(idx == 1),
476
+ ):
477
+ col1, col2, col3, col4 = st.columns(4)
478
+ with col1:
479
+ st.metric("Complexity", row["complexity"])
480
+ with col2:
481
+ st.write(f"**Grade:** {row['rank']}")
482
+ with col3:
483
+ st.write(f"**Type:** {row['type']}")
484
+ with col4:
485
+ st.write(f"**Lines:** {row['lineno']}-{row['endline']}")
486
+
487
+ st.write(f"**File:** `{row['filepath']}`")
488
+ full_name = (
489
+ f"{row['classname']}.{row['name']}"
490
+ if row["type"] == "method"
491
+ else row["name"]
492
+ )
493
+ st.write(f"**Full Name:** `{full_name}`")
494
+
495
+ # Recommendation
496
+ if row["complexity"] >= complexity_threshold + 5:
497
+ st.error("🔴 **CRITICAL:** This needs immediate refactoring")
498
+ elif row["complexity"] >= complexity_threshold:
499
+ st.warning(
500
+ "🟠 **HIGH:** Consider breaking this into smaller functions"
501
+ )
502
+
503
+ if row["rank"] in ["D", "E", "F"]:
504
+ st.warning(
505
+ f"**Grade {row['rank']}:** Code quality is poor, refactoring recommended"
506
+ )
507
+ else:
508
+ st.success("✅ No risky items found! Your code looks good.")
509
+
510
+ # ===== TAB 4: DETAILED VIEW =====
511
+ with tab4:
512
+ st.subheader("Detailed Item Analysis")
513
+
514
+ # Select item to analyze
515
+ df_display = df.copy()
516
+ df_display["display_name"] = df_display.apply(
517
+ lambda x: f"{x['name']} ({x['type']}) - {x['filepath'].split('/')[-1]}",
518
+ axis=1,
519
+ )
520
+
521
+ selected_item = st.selectbox(
522
+ "Select an item to analyze",
523
+ options=df_display.index,
524
+ format_func=lambda x: df_display.loc[x, "display_name"],
525
+ )
526
+
527
+ if selected_item is not None:
528
+ item = df.iloc[selected_item]
529
+
530
+ # Header with grade badge
531
+ col1, col2 = st.columns([3, 1])
532
+ with col1:
533
+ st.title(item["name"])
534
+ with col2:
535
+ grade_html = display_grade_badge(item["rank"])
536
+ st.markdown(grade_html, unsafe_allow_html=True)
537
+
538
+ st.divider()
539
+
540
+ # Detailed metrics
541
+ col1, col2, col3, col4, col5 = st.columns(5)
542
+ with col1:
543
+ st.metric("Complexity", item["complexity"])
544
+ with col2:
545
+ st.metric("Type", item["type"])
546
+ with col3:
547
+ st.metric("Start Line", int(item["lineno"]))
548
+ with col4:
549
+ st.metric("End Line", int(item["endline"]))
550
+ with col5:
551
+ st.metric("Lines of Code", int(item["endline"] - item["lineno"] + 1))
552
+
553
+ st.divider()
554
+
555
+ # File and location info
556
+ col1, col2 = st.columns(2)
557
+ with col1:
558
+ st.write("**File Path:**")
559
+ st.code(item["filepath"], language="text")
560
+ with col2:
561
+ st.write("**Location:**")
562
+ st.code(
563
+ f"Line {int(item['lineno'])} to {int(item['endline'])}, Column {int(item['col_offset'])}",
564
+ language="text",
565
+ )
566
+
567
+ if item["classname"]:
568
+ st.write("**Class Name:**")
569
+ st.code(item["classname"], language="text")
570
+
571
+ st.divider()
572
+
573
+ # Recommendations
574
+ st.subheader("💡 Recommendations")
575
+
576
+ complexity = int(item["complexity"])
577
+ if complexity <= 3:
578
+ st.success(
579
+ "✅ **Simple:** This code is easy to understand and maintain."
580
+ )
581
+ elif complexity <= 7:
582
+ st.info(
583
+ "ℹ️ **Moderate:** Code is reasonably complex. Consider breaking into smaller functions if it exceeds 7."
584
+ )
585
+ elif complexity <= 10:
586
+ st.warning(
587
+ "⚠️ **Complex:** This code is complex and may be difficult to maintain. Consider refactoring."
588
+ )
589
+ else:
590
+ st.error(
591
+ "🔴 **Very Complex:** This code needs immediate refactoring. Break it into smaller, testable units."
592
+ )
593
+
594
+ if item["rank"] in ["D", "E", "F"]:
595
+ st.error(
596
+ f"📉 **Grade {item['rank']}:** Code quality needs improvement."
597
+ )
598
+
599
+ else:
600
+ # Landing page
601
+ st.title("📊 Radon Complexity Analyzer")
602
+ st.markdown(
603
+ """
604
+ Welcome to the Radon Cyclomatic Complexity Analyzer!
605
+
606
+ This tool helps you analyze and visualize Python code complexity reports from the **radon** library.
607
+
608
+ ### Features:
609
+ - 📈 **Overview:** See complexity distribution across your codebase
610
+ - 🔍 **Analysis:** Filter, sort, and search for specific functions/classes
611
+ - ⚠️ **Warnings:** Identify items that need immediate attention
612
+ - 📋 **Details:** Get detailed analysis and recommendations for each item
613
+
614
+ ### How to use:
615
+ 1. Generate a radon complexity report as JSON:
616
+ ```bash
617
+ radon cc your_project/ -j > report.json
618
+ ```
619
+ 2. Upload the JSON file using the sidebar
620
+ 3. Explore and analyze your code complexity!
621
+ """
622
+ )
623
+
624
+ # Create sample data for demonstration
625
+ st.divider()
626
+ st.subheader("Or try with sample data:")
627
+
628
+ if st.button("Load Sample Report"):
629
+ sample_report = {
630
+ "example/settings.py": [
631
+ {
632
+ "type": "class",
633
+ "rank": "A",
634
+ "lineno": 7,
635
+ "complexity": 1,
636
+ "endline": 8,
637
+ "name": "DBSettings",
638
+ "col_offset": 0,
639
+ "methods": [],
640
+ },
641
+ {
642
+ "type": "class",
643
+ "rank": "B",
644
+ "lineno": 11,
645
+ "complexity": 5,
646
+ "endline": 13,
647
+ "name": "ComplexSettings",
648
+ "col_offset": 0,
649
+ "methods": [
650
+ {
651
+ "type": "method",
652
+ "rank": "C",
653
+ "lineno": 12,
654
+ "classname": "ComplexSettings",
655
+ "complexity": 8,
656
+ "endline": 13,
657
+ "name": "validate",
658
+ "col_offset": 4,
659
+ "closures": [],
660
+ }
661
+ ],
662
+ },
663
+ ],
664
+ "example/base.py": [
665
+ {
666
+ "type": "function",
667
+ "rank": "F",
668
+ "lineno": 1,
669
+ "complexity": 15,
670
+ "endline": 50,
671
+ "name": "complex_function",
672
+ "col_offset": 0,
673
+ }
674
+ ],
675
+ }
676
+
677
+ st.session_state.report_data = sample_report
678
+ st.session_state.df = flatten_report(sample_report)
679
+ st.success("✅ Sample data loaded! Refresh the page to see the analysis.")
680
+ st.rerun()
requirements.txt ADDED
@@ -0,0 +1,4 @@
 
 
 
 
 
1
+ altair
2
+ streamlit==1.40.0
3
+ pandas
4
+ plotly==5.17.0
src/sample_report.json ADDED
@@ -0,0 +1,308 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "project/config/settings.py": [
3
+ {
4
+ "type": "class",
5
+ "rank": "A",
6
+ "lineno": 5,
7
+ "complexity": 1,
8
+ "endline": 8,
9
+ "name": "DatabaseConfig",
10
+ "col_offset": 0,
11
+ "methods": []
12
+ },
13
+ {
14
+ "type": "class",
15
+ "rank": "A",
16
+ "lineno": 12,
17
+ "complexity": 2,
18
+ "endline": 18,
19
+ "name": "AppConfig",
20
+ "col_offset": 0,
21
+ "methods": []
22
+ },
23
+ {
24
+ "type": "function",
25
+ "rank": "A",
26
+ "lineno": 22,
27
+ "complexity": 3,
28
+ "endline": 28,
29
+ "name": "load_settings",
30
+ "col_offset": 0
31
+ }
32
+ ],
33
+ "project/core/base.py": [
34
+ {
35
+ "type": "class",
36
+ "rank": "B",
37
+ "lineno": 10,
38
+ "complexity": 5,
39
+ "endline": 85,
40
+ "name": "BaseHandler",
41
+ "col_offset": 0,
42
+ "methods": [
43
+ {
44
+ "type": "method",
45
+ "rank": "A",
46
+ "lineno": 15,
47
+ "classname": "BaseHandler",
48
+ "complexity": 1,
49
+ "endline": 25,
50
+ "name": "__init__",
51
+ "col_offset": 4,
52
+ "closures": []
53
+ },
54
+ {
55
+ "type": "method",
56
+ "rank": "B",
57
+ "lineno": 27,
58
+ "classname": "BaseHandler",
59
+ "complexity": 5,
60
+ "endline": 45,
61
+ "name": "validate_input",
62
+ "col_offset": 4,
63
+ "closures": []
64
+ },
65
+ {
66
+ "type": "method",
67
+ "rank": "A",
68
+ "lineno": 47,
69
+ "classname": "BaseHandler",
70
+ "complexity": 2,
71
+ "endline": 55,
72
+ "name": "process",
73
+ "col_offset": 4,
74
+ "closures": []
75
+ }
76
+ ]
77
+ }
78
+ ],
79
+ "project/utils/data_sampler.py": [
80
+ {
81
+ "type": "class",
82
+ "rank": "A",
83
+ "lineno": 1,
84
+ "complexity": 2,
85
+ "endline": 50,
86
+ "name": "sample_data",
87
+ "col_offset": 0,
88
+ "methods": [
89
+ {
90
+ "type": "method",
91
+ "rank": "A",
92
+ "lineno": 5,
93
+ "classname": "Sampler",
94
+ "complexity": 2,
95
+ "endline": 20,
96
+ "name": "Sample",
97
+ "col_offset": 4,
98
+ "closures": []
99
+ }
100
+ ]
101
+ }
102
+ ],
103
+ "project/utils/data_processor.py": [
104
+ {
105
+ "type": "function",
106
+ "rank": "C",
107
+ "lineno": 5,
108
+ "complexity": 8,
109
+ "endline": 45,
110
+ "name": "validate_data",
111
+ "col_offset": 0
112
+ },
113
+ {
114
+ "type": "function",
115
+ "rank": "D",
116
+ "lineno": 50,
117
+ "complexity": 12,
118
+ "endline": 120,
119
+ "name": "process_complex_data",
120
+ "col_offset": 0,
121
+ "closures": [
122
+ {
123
+ "type": "function",
124
+ "rank": "B",
125
+ "lineno": 65,
126
+ "complexity": 5,
127
+ "endline": 85,
128
+ "name": "filter_items",
129
+ "col_offset": 4,
130
+ "closures": []
131
+ },
132
+ {
133
+ "type": "function",
134
+ "rank": "C",
135
+ "lineno": 90,
136
+ "complexity": 7,
137
+ "endline": 110,
138
+ "name": "transform_items",
139
+ "col_offset": 4,
140
+ "closures": []
141
+ }
142
+ ]
143
+ }
144
+ ],
145
+ "project/legacy/old_parser.py": [
146
+ {
147
+ "type": "function",
148
+ "rank": "F",
149
+ "lineno": 10,
150
+ "complexity": 22,
151
+ "endline": 150,
152
+ "name": "legacy_xml_parser",
153
+ "col_offset": 0
154
+ },
155
+ {
156
+ "type": "function",
157
+ "rank": "E",
158
+ "lineno": 155,
159
+ "complexity": 15,
160
+ "endline": 220,
161
+ "name": "migrate_old_format",
162
+ "col_offset": 0
163
+ },
164
+ {
165
+ "type": "class",
166
+ "rank": "D",
167
+ "lineno": 225,
168
+ "complexity": 11,
169
+ "endline": 350,
170
+ "name": "LegacyHandler",
171
+ "col_offset": 0,
172
+ "methods": [
173
+ {
174
+ "type": "method",
175
+ "rank": "D",
176
+ "lineno": 230,
177
+ "classname": "LegacyHandler",
178
+ "complexity": 11,
179
+ "endline": 280,
180
+ "name": "parse_config",
181
+ "col_offset": 4,
182
+ "closures": []
183
+ },
184
+ {
185
+ "type": "method",
186
+ "rank": "C",
187
+ "lineno": 285,
188
+ "classname": "LegacyHandler",
189
+ "complexity": 9,
190
+ "endline": 320,
191
+ "name": "convert_format",
192
+ "col_offset": 4,
193
+ "closures": []
194
+ }
195
+ ]
196
+ }
197
+ ],
198
+ "project/api/handlers.py": [
199
+ {
200
+ "type": "function",
201
+ "rank": "B",
202
+ "lineno": 8,
203
+ "complexity": 6,
204
+ "endline": 25,
205
+ "name": "handle_request",
206
+ "col_offset": 0
207
+ },
208
+ {
209
+ "type": "function",
210
+ "rank": "A",
211
+ "lineno": 30,
212
+ "complexity": 4,
213
+ "endline": 40,
214
+ "name": "validate_token",
215
+ "col_offset": 0
216
+ },
217
+ {
218
+ "type": "class",
219
+ "rank": "C",
220
+ "lineno": 45,
221
+ "complexity": 8,
222
+ "endline": 120,
223
+ "name": "APIHandler",
224
+ "col_offset": 0,
225
+ "methods": [
226
+ {
227
+ "type": "method",
228
+ "rank": "B",
229
+ "lineno": 50,
230
+ "classname": "APIHandler",
231
+ "complexity": 5,
232
+ "endline": 70,
233
+ "name": "authenticate",
234
+ "col_offset": 4,
235
+ "closures": []
236
+ },
237
+ {
238
+ "type": "method",
239
+ "rank": "C",
240
+ "lineno": 75,
241
+ "classname": "APIHandler",
242
+ "complexity": 8,
243
+ "endline": 100,
244
+ "name": "process_request",
245
+ "col_offset": 4,
246
+ "closures": []
247
+ },
248
+ {
249
+ "type": "method",
250
+ "rank": "A",
251
+ "lineno": 105,
252
+ "classname": "APIHandler",
253
+ "complexity": 3,
254
+ "endline": 115,
255
+ "name": "send_response",
256
+ "col_offset": 4,
257
+ "closures": []
258
+ }
259
+ ]
260
+ }
261
+ ],
262
+ "project/models/user.py": [
263
+ {
264
+ "type": "class",
265
+ "rank": "B",
266
+ "lineno": 5,
267
+ "complexity": 6,
268
+ "endline": 80,
269
+ "name": "User",
270
+ "col_offset": 0,
271
+ "methods": [
272
+ {
273
+ "type": "method",
274
+ "rank": "A",
275
+ "lineno": 10,
276
+ "classname": "User",
277
+ "complexity": 1,
278
+ "endline": 20,
279
+ "name": "__init__",
280
+ "col_offset": 4,
281
+ "closures": []
282
+ },
283
+ {
284
+ "type": "method",
285
+ "rank": "B",
286
+ "lineno": 25,
287
+ "classname": "User",
288
+ "complexity": 6,
289
+ "endline": 50,
290
+ "name": "update_profile",
291
+ "col_offset": 4,
292
+ "closures": []
293
+ },
294
+ {
295
+ "type": "method",
296
+ "rank": "A",
297
+ "lineno": 55,
298
+ "classname": "User",
299
+ "complexity": 2,
300
+ "endline": 65,
301
+ "name": "is_active",
302
+ "col_offset": 4,
303
+ "closures": []
304
+ }
305
+ ]
306
+ }
307
+ ]
308
+ }
src/streamlit_app.py ADDED
@@ -0,0 +1,680 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import json
2
+ from pathlib import Path
3
+ from typing import Any, Dict, List
4
+
5
+ import pandas as pd
6
+ import plotly.express as px
7
+ import plotly.graph_objects as go
8
+ import streamlit as st
9
+
10
+ st.set_page_config(
11
+ page_title="Radon Complexity Analyzer",
12
+ page_icon="📊",
13
+ layout="wide",
14
+ initial_sidebar_state="expanded",
15
+ )
16
+
17
+ # Custom CSS for better styling
18
+ st.markdown(
19
+ """
20
+ <style>
21
+ .grade-A { color: #2ecc71; font-weight: bold; }
22
+ .grade-B { color: #f39c12; font-weight: bold; }
23
+ .grade-C { color: #e74c3c; font-weight: bold; }
24
+ .grade-D { color: #e67e22; font-weight: bold; }
25
+ .grade-F { color: #c0392b; font-weight: bold; }
26
+ .metric-high { background-color: #ffe6e6; }
27
+ .metric-medium { background-color: #fff3cd; }
28
+ .metric-low { background-color: #d4edda; }
29
+ </style>
30
+ """,
31
+ unsafe_allow_html=True,
32
+ )
33
+
34
+
35
+ def get_grade_color(grade: str) -> str:
36
+ """Get color for grade"""
37
+ colors = {
38
+ "A": "#2ecc71", # Green
39
+ "B": "#f39c12", # Orange
40
+ "C": "#e74c3c", # Red
41
+ "D": "#e67e22", # Dark Orange
42
+ "E": "#d35400", # Darker Orange
43
+ "F": "#c0392b", # Dark Red
44
+ }
45
+ return colors.get(grade, "#95a5a6")
46
+
47
+
48
+ def get_complexity_color(complexity: int, high_threshold: int = 10) -> str:
49
+ """Get color based on complexity value"""
50
+ if complexity <= 3:
51
+ return "#2ecc71" # Green - Simple
52
+ elif complexity <= 7:
53
+ return "#f39c12" # Orange - Moderate
54
+ elif complexity <= high_threshold:
55
+ return "#e74c3c" # Red - Complex
56
+ else:
57
+ return "#c0392b" # Dark Red - Very Complex
58
+
59
+
60
+ def flatten_report(report: Dict[str, List[Dict]]) -> pd.DataFrame:
61
+ """Convert nested JSON report to flattened DataFrame"""
62
+ rows = []
63
+
64
+ for filepath, items in report.items():
65
+ if not isinstance(items, list):
66
+ continue
67
+
68
+ for item in items:
69
+ row = {
70
+ "filepath": filepath,
71
+ "type": item.get("type", "N/A"),
72
+ "name": item.get("name", "N/A"),
73
+ "classname": item.get("classname", ""),
74
+ "complexity": item.get("complexity", 0),
75
+ "rank": item.get("rank", "N/A"),
76
+ "lineno": item.get("lineno", 0),
77
+ "endline": item.get("endline", 0),
78
+ "col_offset": item.get("col_offset", 0),
79
+ }
80
+ rows.append(row)
81
+
82
+ # Add nested methods/closures
83
+ if item.get("methods"):
84
+ for method in item["methods"]:
85
+ method_row = row.copy()
86
+ method_row.update(
87
+ {
88
+ "type": method.get("type", "method"),
89
+ "name": method.get("name", "N/A"),
90
+ "complexity": method.get("complexity", 0),
91
+ "rank": method.get("rank", "N/A"),
92
+ "lineno": method.get("lineno", 0),
93
+ "endline": method.get("endline", 0),
94
+ "col_offset": method.get("col_offset", 0),
95
+ "parent_name": item.get("name", ""),
96
+ }
97
+ )
98
+ rows.append(method_row)
99
+
100
+ if item.get("closures"):
101
+ for closure in item["closures"]:
102
+ closure_row = row.copy()
103
+ closure_row.update(
104
+ {
105
+ "type": closure.get("type", "closure"),
106
+ "name": closure.get("name", "N/A"),
107
+ "complexity": closure.get("complexity", 0),
108
+ "rank": closure.get("rank", "N/A"),
109
+ "lineno": closure.get("lineno", 0),
110
+ "endline": closure.get("endline", 0),
111
+ "col_offset": closure.get("col_offset", 0),
112
+ "parent_name": item.get("name", ""),
113
+ }
114
+ )
115
+ rows.append(closure_row)
116
+
117
+ return pd.DataFrame(rows)
118
+
119
+
120
+ def display_grade_badge(grade: str) -> str:
121
+ """Create colored grade badge"""
122
+ color = get_grade_color(grade)
123
+ return f'<span style="background-color: {color}; color: white; padding: 4px 8px; border-radius: 4px; font-weight: bold;">{grade}</span>'
124
+
125
+
126
+ def identify_risky_items(
127
+ df: pd.DataFrame, complexity_threshold: int = 10, risky_grades: List[str] = None
128
+ ) -> pd.DataFrame:
129
+ """Identify items that need investigation"""
130
+ if risky_grades is None:
131
+ risky_grades = ["D", "E", "F"]
132
+
133
+ risky = df[
134
+ (df["complexity"] >= complexity_threshold) | (df["rank"].isin(risky_grades))
135
+ ]
136
+ return risky.sort_values("complexity", ascending=False)
137
+
138
+
139
+ def create_complexity_chart(df: pd.DataFrame):
140
+ """Create a chart showing complexity distribution"""
141
+ complexity_dist = df["complexity"].value_counts().sort_index()
142
+ fig = go.Figure(data=[go.Bar(x=complexity_dist.index, y=complexity_dist.values)])
143
+ fig.update_layout(
144
+ title="Complexity Distribution",
145
+ xaxis_title="Complexity Level",
146
+ yaxis_title="Count",
147
+ hovermode="x unified",
148
+ )
149
+ return fig
150
+
151
+
152
+ def create_grade_chart(df: pd.DataFrame):
153
+ """Create a chart showing grade distribution"""
154
+ grade_dist = df["rank"].value_counts()
155
+ grade_order = ["A", "B", "C", "D", "E", "F"]
156
+ grade_dist = grade_dist.reindex(
157
+ [g for g in grade_order if g in grade_dist.index], fill_value=0
158
+ )
159
+
160
+ colors = [get_grade_color(g) for g in grade_dist.index]
161
+ fig = go.Figure(
162
+ data=[go.Bar(x=grade_dist.index, y=grade_dist.values, marker_color=colors)]
163
+ )
164
+ fig.update_layout(
165
+ title="Grade Distribution",
166
+ xaxis_title="Grade",
167
+ yaxis_title="Count",
168
+ hovermode="x unified",
169
+ )
170
+ return fig
171
+
172
+
173
+ def create_scatter_plot(df: pd.DataFrame):
174
+ """Create scatter plot of complexity vs files"""
175
+ # Add a column with just the filename for display
176
+ df_plot = df.copy()
177
+ df_plot["filename"] = df_plot["filepath"].apply(lambda x: x.split("/")[-1])
178
+
179
+ fig = px.scatter(
180
+ df_plot,
181
+ x="filename",
182
+ y="complexity",
183
+ color="rank",
184
+ hover_data=["name", "type", "lineno", "filepath"],
185
+ title="Complexity by File and Grade",
186
+ color_discrete_map={g: get_grade_color(g) for g in df_plot["rank"].unique()},
187
+ height=600,
188
+ )
189
+ fig.update_layout(xaxis_tickangle=-45, xaxis_title="File")
190
+ return fig
191
+
192
+
193
+ # Initialize session state
194
+ if "report_data" not in st.session_state:
195
+ st.session_state.report_data = None
196
+ if "df" not in st.session_state:
197
+ st.session_state.df = None
198
+
199
+ # Sidebar for file upload
200
+ st.sidebar.title("📊 Radon Report Analyzer")
201
+
202
+ # File upload
203
+ uploaded_file = st.sidebar.file_uploader(
204
+ "Upload JSON Report",
205
+ type=["json"],
206
+ help="Upload the cyclomatic complexity report from radon library",
207
+ )
208
+
209
+ if uploaded_file:
210
+ try:
211
+ report_data = json.load(uploaded_file)
212
+ st.session_state.report_data = report_data
213
+ st.session_state.df = flatten_report(report_data)
214
+ st.sidebar.success("✅ Report loaded successfully!")
215
+ except json.JSONDecodeError:
216
+ st.sidebar.error("❌ Invalid JSON file")
217
+ except Exception as e:
218
+ st.sidebar.error(f"❌ Error loading file: {str(e)}")
219
+
220
+ # Main app logic
221
+ if st.session_state.df is not None and len(st.session_state.df) > 0:
222
+ df = st.session_state.df.copy()
223
+
224
+ # drop duplicate rows by name (drop the one with NaN parrent)
225
+ df = df.drop_duplicates(subset=["name", "filepath", "lineno"], keep="first")
226
+
227
+ # Create tabs
228
+ tab1, tab2, tab3, tab4 = st.tabs(
229
+ ["📈 Overview", "🔍 Analysis", "⚠️ Warnings", "📋 Details"]
230
+ )
231
+
232
+ # ===== TAB 1: OVERVIEW =====
233
+ with tab1:
234
+ col1, col2, col3, col4 = st.columns(4)
235
+
236
+ with col1:
237
+ st.metric("Total Items", len(df))
238
+ with col2:
239
+ st.metric("Total Files", df["filepath"].nunique())
240
+ with col3:
241
+ avg_complexity = df["complexity"].mean()
242
+ st.metric("Avg Complexity", f"{avg_complexity:.2f}")
243
+ with col4:
244
+ max_complexity = df["complexity"].max()
245
+ st.metric("Max Complexity", max_complexity)
246
+
247
+ st.divider()
248
+
249
+ col1, col2 = st.columns(2)
250
+ with col1:
251
+ st.plotly_chart(create_complexity_chart(df), use_container_width=True)
252
+ with col2:
253
+ st.plotly_chart(create_grade_chart(df), use_container_width=True)
254
+
255
+ st.divider()
256
+ st.subheader("📍 Complexity by File and Grade")
257
+
258
+ # File filter for scatter plot - show only filename, not full path
259
+ filepath_to_filename = {fp: fp.split("/")[-1] for fp in df["filepath"].unique()}
260
+ filename_to_filepath = {v: k for k, v in filepath_to_filename.items()}
261
+
262
+ # Initialize selected files in session state if not exists
263
+ if "selected_scatter_files" not in st.session_state:
264
+ st.session_state.selected_scatter_files = sorted(
265
+ filepath_to_filename.values()
266
+ )
267
+
268
+ # Select all / Remove all buttons
269
+ col_btn1, col_btn2, col_spacer = st.columns([1, 1, 6])
270
+ with col_btn1:
271
+ if st.button("Select All", use_container_width=True):
272
+ st.session_state.selected_scatter_files = sorted(
273
+ filepath_to_filename.values()
274
+ )
275
+ st.rerun()
276
+ with col_btn2:
277
+ if st.button("Remove All", use_container_width=True):
278
+ st.session_state.selected_scatter_files = []
279
+ st.rerun()
280
+
281
+ st.write("**Select files to display:**")
282
+ scatter_file_filter_display = st.pills(
283
+ "Filter files",
284
+ options=sorted(filepath_to_filename.values()),
285
+ selection_mode="multi",
286
+ default=st.session_state.selected_scatter_files,
287
+ label_visibility="collapsed",
288
+ key="scatter_plot_file_filter",
289
+ )
290
+
291
+ # Update session state
292
+ st.session_state.selected_scatter_files = (
293
+ scatter_file_filter_display if scatter_file_filter_display else []
294
+ )
295
+
296
+ # Convert selected filenames back to full paths
297
+ scatter_file_filter = [
298
+ filename_to_filepath[fn] for fn in (scatter_file_filter_display or [])
299
+ ]
300
+
301
+ # Apply file filter for scatter plot
302
+ if scatter_file_filter:
303
+ scatter_df = df[df["filepath"].isin(scatter_file_filter)]
304
+ else:
305
+ scatter_df = pd.DataFrame() # Empty dataframe when no files selected
306
+
307
+ if len(scatter_df) > 0:
308
+ st.plotly_chart(create_scatter_plot(scatter_df), use_container_width=True)
309
+ else:
310
+ st.info("No data to display. Please select at least one file.")
311
+
312
+ # ===== TAB 2: ANALYSIS WITH FILTERS =====
313
+ with tab2:
314
+ st.subheader("Filter & Sort Data")
315
+
316
+ col1, col2, col3, col4 = st.columns(4)
317
+
318
+ with col1:
319
+ type_filter = st.multiselect(
320
+ "Type",
321
+ options=df["type"].unique(),
322
+ default=df["type"].unique(),
323
+ help="Filter by item type",
324
+ )
325
+
326
+ with col2:
327
+ grade_filter = st.multiselect(
328
+ "Grade",
329
+ options=sorted(df["rank"].unique()),
330
+ default=sorted(df["rank"].unique()),
331
+ help="Filter by grade",
332
+ )
333
+
334
+ with col3:
335
+ complexity_range = st.slider(
336
+ "Complexity Range",
337
+ min_value=int(df["complexity"].min()),
338
+ max_value=int(df["complexity"].max()),
339
+ value=(int(df["complexity"].min()), int(df["complexity"].max())),
340
+ help="Filter by complexity level",
341
+ )
342
+
343
+ with col4:
344
+ filepath_filter = st.multiselect(
345
+ "Files",
346
+ options=sorted(df["filepath"].unique()),
347
+ default=sorted(df["filepath"].unique()),
348
+ help="Filter by file",
349
+ )
350
+
351
+ # Apply filters
352
+ filtered_df = df[
353
+ (df["type"].isin(type_filter))
354
+ & (df["rank"].isin(grade_filter))
355
+ & (df["complexity"] >= complexity_range[0])
356
+ & (df["complexity"] <= complexity_range[1])
357
+ & (df["filepath"].isin(filepath_filter))
358
+ ]
359
+
360
+ col1, col2 = st.columns(2)
361
+ with col1:
362
+ sort_by = st.selectbox(
363
+ "Sort by",
364
+ options=[
365
+ "Complexity (High→Low)",
366
+ "Complexity (Low→High)",
367
+ "Grade (Best→Worst)",
368
+ "Name (A→Z)",
369
+ "File Path",
370
+ "Line Number",
371
+ ],
372
+ help="Sort the filtered results",
373
+ )
374
+
375
+ with col2:
376
+ search_term = st.text_input(
377
+ "Search by name", help="Search for specific function/class names"
378
+ )
379
+
380
+ # Apply sorting
381
+ if sort_by == "Complexity (High→Low)":
382
+ filtered_df = filtered_df.sort_values("complexity", ascending=False)
383
+ elif sort_by == "Complexity (Low→High)":
384
+ filtered_df = filtered_df.sort_values("complexity", ascending=True)
385
+ elif sort_by == "Grade (Best→Worst)":
386
+ grade_order = {"A": 1, "B": 2, "C": 3, "D": 4, "F": 5}
387
+ filtered_df = filtered_df.sort_values(
388
+ "rank", key=lambda x: x.map(grade_order)
389
+ )
390
+ elif sort_by == "Name (A→Z)":
391
+ filtered_df = filtered_df.sort_values("name")
392
+ elif sort_by == "File Path":
393
+ filtered_df = filtered_df.sort_values("filepath")
394
+ elif sort_by == "Line Number":
395
+ filtered_df = filtered_df.sort_values("lineno")
396
+
397
+ # Apply search
398
+ if search_term:
399
+ filtered_df = filtered_df[
400
+ filtered_df["name"].str.contains(search_term, case=False, na=False)
401
+ ]
402
+
403
+ st.info(f"Showing {len(filtered_df)} of {len(df)} items")
404
+
405
+ # Display table with color coding
406
+ def style_dataframe(val, column):
407
+ if column == "rank":
408
+ color = get_grade_color(val)
409
+ return f"background-color: {color}; color: white; font-weight: bold;"
410
+ elif column == "complexity":
411
+ color = get_complexity_color(int(val))
412
+ return f"background-color: {color}; color: white;"
413
+ return ""
414
+
415
+ display_df = filtered_df[
416
+ ["filepath", "type", "name", "complexity", "rank", "lineno", "endline"]
417
+ ].copy()
418
+ display_df = display_df.reset_index(drop=True)
419
+
420
+ st.dataframe(
421
+ display_df,
422
+ use_container_width=True,
423
+ column_config={
424
+ "complexity": st.column_config.NumberColumn(width="small"),
425
+ "rank": st.column_config.TextColumn(width="small"),
426
+ "lineno": st.column_config.NumberColumn(width="small"),
427
+ "endline": st.column_config.NumberColumn(width="small"),
428
+ "type": st.column_config.TextColumn(width="small"),
429
+ },
430
+ )
431
+
432
+ # ===== TAB 3: WARNINGS =====
433
+ with tab3:
434
+ st.subheader("⚠️ Items Requiring Investigation")
435
+
436
+ col1, col2 = st.columns(2)
437
+ with col1:
438
+ complexity_threshold = st.slider(
439
+ "Complexity Threshold",
440
+ min_value=1,
441
+ max_value=int(df["complexity"].max()),
442
+ value=10,
443
+ help="Items with complexity >= this value will be flagged",
444
+ )
445
+
446
+ with col2:
447
+ risky_grades = st.multiselect(
448
+ "Risky Grades",
449
+ options=["A", "B", "C", "D", "E", "F"],
450
+ default=["D", "E", "F"],
451
+ help="Grades considered risky",
452
+ )
453
+
454
+ risky_df = identify_risky_items(df, complexity_threshold, risky_grades)
455
+
456
+ if len(risky_df) > 0:
457
+ st.warning(f"⚠️ Found {len(risky_df)} items that need investigation")
458
+
459
+ # Group by severity
460
+ col1, col2 = st.columns(2)
461
+ with col1:
462
+ high_risk = risky_df[risky_df["complexity"] >= complexity_threshold + 5]
463
+ st.metric("High Risk (Very High Complexity)", len(high_risk))
464
+
465
+ with col2:
466
+ bad_grade = risky_df[risky_df["rank"].isin(["D", "F"])]
467
+ st.metric("Bad Grade Items", len(bad_grade))
468
+
469
+ st.divider()
470
+
471
+ # Detailed view of risky items
472
+ for idx, (_, row) in enumerate(risky_df.head(20).iterrows(), 1):
473
+ with st.expander(
474
+ f"🚨 {row['name']} (Complexity: {row['complexity']}, Grade: {row['rank']})",
475
+ expanded=(idx == 1),
476
+ ):
477
+ col1, col2, col3, col4 = st.columns(4)
478
+ with col1:
479
+ st.metric("Complexity", row["complexity"])
480
+ with col2:
481
+ st.write(f"**Grade:** {row['rank']}")
482
+ with col3:
483
+ st.write(f"**Type:** {row['type']}")
484
+ with col4:
485
+ st.write(f"**Lines:** {row['lineno']}-{row['endline']}")
486
+
487
+ st.write(f"**File:** `{row['filepath']}`")
488
+ full_name = (
489
+ f"{row['classname']}.{row['name']}"
490
+ if row["type"] == "method"
491
+ else row["name"]
492
+ )
493
+ st.write(f"**Full Name:** `{full_name}`")
494
+
495
+ # Recommendation
496
+ if row["complexity"] >= complexity_threshold + 5:
497
+ st.error("🔴 **CRITICAL:** This needs immediate refactoring")
498
+ elif row["complexity"] >= complexity_threshold:
499
+ st.warning(
500
+ "🟠 **HIGH:** Consider breaking this into smaller functions"
501
+ )
502
+
503
+ if row["rank"] in ["D", "E", "F"]:
504
+ st.warning(
505
+ f"**Grade {row['rank']}:** Code quality is poor, refactoring recommended"
506
+ )
507
+ else:
508
+ st.success("✅ No risky items found! Your code looks good.")
509
+
510
+ # ===== TAB 4: DETAILED VIEW =====
511
+ with tab4:
512
+ st.subheader("Detailed Item Analysis")
513
+
514
+ # Select item to analyze
515
+ df_display = df.copy()
516
+ df_display["display_name"] = df_display.apply(
517
+ lambda x: f"{x['name']} ({x['type']}) - {x['filepath'].split('/')[-1]}",
518
+ axis=1,
519
+ )
520
+
521
+ selected_item = st.selectbox(
522
+ "Select an item to analyze",
523
+ options=df_display.index,
524
+ format_func=lambda x: df_display.loc[x, "display_name"],
525
+ )
526
+
527
+ if selected_item is not None:
528
+ item = df.iloc[selected_item]
529
+
530
+ # Header with grade badge
531
+ col1, col2 = st.columns([3, 1])
532
+ with col1:
533
+ st.title(item["name"])
534
+ with col2:
535
+ grade_html = display_grade_badge(item["rank"])
536
+ st.markdown(grade_html, unsafe_allow_html=True)
537
+
538
+ st.divider()
539
+
540
+ # Detailed metrics
541
+ col1, col2, col3, col4, col5 = st.columns(5)
542
+ with col1:
543
+ st.metric("Complexity", item["complexity"])
544
+ with col2:
545
+ st.metric("Type", item["type"])
546
+ with col3:
547
+ st.metric("Start Line", int(item["lineno"]))
548
+ with col4:
549
+ st.metric("End Line", int(item["endline"]))
550
+ with col5:
551
+ st.metric("Lines of Code", int(item["endline"] - item["lineno"] + 1))
552
+
553
+ st.divider()
554
+
555
+ # File and location info
556
+ col1, col2 = st.columns(2)
557
+ with col1:
558
+ st.write("**File Path:**")
559
+ st.code(item["filepath"], language="text")
560
+ with col2:
561
+ st.write("**Location:**")
562
+ st.code(
563
+ f"Line {int(item['lineno'])} to {int(item['endline'])}, Column {int(item['col_offset'])}",
564
+ language="text",
565
+ )
566
+
567
+ if item["classname"]:
568
+ st.write("**Class Name:**")
569
+ st.code(item["classname"], language="text")
570
+
571
+ st.divider()
572
+
573
+ # Recommendations
574
+ st.subheader("💡 Recommendations")
575
+
576
+ complexity = int(item["complexity"])
577
+ if complexity <= 3:
578
+ st.success(
579
+ "✅ **Simple:** This code is easy to understand and maintain."
580
+ )
581
+ elif complexity <= 7:
582
+ st.info(
583
+ "ℹ️ **Moderate:** Code is reasonably complex. Consider breaking into smaller functions if it exceeds 7."
584
+ )
585
+ elif complexity <= 10:
586
+ st.warning(
587
+ "⚠️ **Complex:** This code is complex and may be difficult to maintain. Consider refactoring."
588
+ )
589
+ else:
590
+ st.error(
591
+ "🔴 **Very Complex:** This code needs immediate refactoring. Break it into smaller, testable units."
592
+ )
593
+
594
+ if item["rank"] in ["D", "E", "F"]:
595
+ st.error(
596
+ f"📉 **Grade {item['rank']}:** Code quality needs improvement."
597
+ )
598
+
599
+ else:
600
+ # Landing page
601
+ st.title("📊 Radon Complexity Analyzer")
602
+ st.markdown(
603
+ """
604
+ Welcome to the Radon Cyclomatic Complexity Analyzer!
605
+
606
+ This tool helps you analyze and visualize Python code complexity reports from the **radon** library.
607
+
608
+ ### Features:
609
+ - 📈 **Overview:** See complexity distribution across your codebase
610
+ - 🔍 **Analysis:** Filter, sort, and search for specific functions/classes
611
+ - ⚠️ **Warnings:** Identify items that need immediate attention
612
+ - 📋 **Details:** Get detailed analysis and recommendations for each item
613
+
614
+ ### How to use:
615
+ 1. Generate a radon complexity report as JSON:
616
+ ```bash
617
+ radon cc your_project/ -j > report.json
618
+ ```
619
+ 2. Upload the JSON file using the sidebar
620
+ 3. Explore and analyze your code complexity!
621
+ """
622
+ )
623
+
624
+ # Create sample data for demonstration
625
+ st.divider()
626
+ st.subheader("Or try with sample data:")
627
+
628
+ if st.button("Load Sample Report"):
629
+ sample_report = {
630
+ "example/settings.py": [
631
+ {
632
+ "type": "class",
633
+ "rank": "A",
634
+ "lineno": 7,
635
+ "complexity": 1,
636
+ "endline": 8,
637
+ "name": "DBSettings",
638
+ "col_offset": 0,
639
+ "methods": [],
640
+ },
641
+ {
642
+ "type": "class",
643
+ "rank": "B",
644
+ "lineno": 11,
645
+ "complexity": 5,
646
+ "endline": 13,
647
+ "name": "ComplexSettings",
648
+ "col_offset": 0,
649
+ "methods": [
650
+ {
651
+ "type": "method",
652
+ "rank": "C",
653
+ "lineno": 12,
654
+ "classname": "ComplexSettings",
655
+ "complexity": 8,
656
+ "endline": 13,
657
+ "name": "validate",
658
+ "col_offset": 4,
659
+ "closures": [],
660
+ }
661
+ ],
662
+ },
663
+ ],
664
+ "example/base.py": [
665
+ {
666
+ "type": "function",
667
+ "rank": "F",
668
+ "lineno": 1,
669
+ "complexity": 15,
670
+ "endline": 50,
671
+ "name": "complex_function",
672
+ "col_offset": 0,
673
+ }
674
+ ],
675
+ }
676
+
677
+ st.session_state.report_data = sample_report
678
+ st.session_state.df = flatten_report(sample_report)
679
+ st.success("✅ Sample data loaded! Refresh the page to see the analysis.")
680
+ st.rerun()