Boray
bugfix on upload data
8023e92
import json
from pathlib import Path
from typing import Any, Dict, List
import pandas as pd
import plotly.express as px
import plotly.graph_objects as go
import streamlit as st
st.set_page_config(
page_title="Radon Complexity Analyzer",
page_icon="📊",
layout="wide",
initial_sidebar_state="expanded",
)
# Custom CSS for better styling
st.markdown(
"""
<style>
.grade-A { color: #2ecc71; font-weight: bold; }
.grade-B { color: #f39c12; font-weight: bold; }
.grade-C { color: #e74c3c; font-weight: bold; }
.grade-D { color: #e67e22; font-weight: bold; }
.grade-F { color: #c0392b; font-weight: bold; }
.metric-high { background-color: #ffe6e6; }
.metric-medium { background-color: #fff3cd; }
.metric-low { background-color: #d4edda; }
</style>
""",
unsafe_allow_html=True,
)
def get_grade_color(grade: str) -> str:
"""Get color for grade"""
colors = {
"A": "#2ecc71", # Green
"B": "#f39c12", # Orange
"C": "#e74c3c", # Red
"D": "#e67e22", # Dark Orange
"E": "#d35400", # Darker Orange
"F": "#c0392b", # Dark Red
}
return colors.get(grade, "#95a5a6")
def get_complexity_color(complexity: int, high_threshold: int = 10) -> str:
"""Get color based on complexity value"""
if complexity <= 3:
return "#2ecc71" # Green - Simple
elif complexity <= 7:
return "#f39c12" # Orange - Moderate
elif complexity <= high_threshold:
return "#e74c3c" # Red - Complex
else:
return "#c0392b" # Dark Red - Very Complex
def flatten_report(report: Dict[str, List[Dict]]) -> pd.DataFrame:
"""Convert nested JSON report to flattened DataFrame"""
rows = []
for filepath, items in report.items():
if not isinstance(items, list):
continue
for item in items:
row = {
"filepath": filepath,
"type": item.get("type", "N/A"),
"name": item.get("name", "N/A"),
"classname": item.get("classname", ""),
"complexity": item.get("complexity", 0),
"rank": item.get("rank", "N/A"),
"lineno": item.get("lineno", 0),
"endline": item.get("endline", 0),
"col_offset": item.get("col_offset", 0),
}
rows.append(row)
# Add nested methods/closures
if item.get("methods"):
for method in item["methods"]:
method_row = row.copy()
method_row.update(
{
"type": method.get("type", "method"),
"name": method.get("name", "N/A"),
"complexity": method.get("complexity", 0),
"rank": method.get("rank", "N/A"),
"lineno": method.get("lineno", 0),
"endline": method.get("endline", 0),
"col_offset": method.get("col_offset", 0),
"parent_name": item.get("name", ""),
}
)
rows.append(method_row)
if item.get("closures"):
for closure in item["closures"]:
closure_row = row.copy()
closure_row.update(
{
"type": closure.get("type", "closure"),
"name": closure.get("name", "N/A"),
"complexity": closure.get("complexity", 0),
"rank": closure.get("rank", "N/A"),
"lineno": closure.get("lineno", 0),
"endline": closure.get("endline", 0),
"col_offset": closure.get("col_offset", 0),
"parent_name": item.get("name", ""),
}
)
rows.append(closure_row)
return pd.DataFrame(rows)
def display_grade_badge(grade: str) -> str:
"""Create colored grade badge"""
color = get_grade_color(grade)
return f'<span style="background-color: {color}; color: white; padding: 4px 8px; border-radius: 4px; font-weight: bold;">{grade}</span>'
def identify_risky_items(
df: pd.DataFrame, complexity_threshold: int = 10, risky_grades: List[str] = None
) -> pd.DataFrame:
"""Identify items that need investigation"""
if risky_grades is None:
risky_grades = ["D", "E", "F"]
risky = df[
(df["complexity"] >= complexity_threshold) | (df["rank"].isin(risky_grades))
]
return risky.sort_values("complexity", ascending=False)
def create_complexity_chart(df: pd.DataFrame):
"""Create a chart showing complexity distribution"""
complexity_dist = df["complexity"].value_counts().sort_index()
fig = go.Figure(data=[go.Bar(x=complexity_dist.index, y=complexity_dist.values)])
fig.update_layout(
title="Complexity Distribution",
xaxis_title="Complexity Level",
yaxis_title="Count",
hovermode="x unified",
)
return fig
def create_grade_chart(df: pd.DataFrame):
"""Create a chart showing grade distribution"""
grade_dist = df["rank"].value_counts()
grade_order = ["A", "B", "C", "D", "E", "F"]
grade_dist = grade_dist.reindex(
[g for g in grade_order if g in grade_dist.index], fill_value=0
)
colors = [get_grade_color(g) for g in grade_dist.index]
fig = go.Figure(
data=[go.Bar(x=grade_dist.index, y=grade_dist.values, marker_color=colors)]
)
fig.update_layout(
title="Grade Distribution",
xaxis_title="Grade",
yaxis_title="Count",
hovermode="x unified",
)
return fig
def create_scatter_plot(df: pd.DataFrame):
"""Create scatter plot of complexity vs files"""
# Add a column with just the filename for display
df_plot = df.copy()
df_plot["filename"] = df_plot["filepath"].apply(lambda x: x.split("/")[-1])
fig = px.scatter(
df_plot,
x="filename",
y="complexity",
color="rank",
hover_data=["name", "type", "lineno", "filepath"],
title="Complexity by File and Grade",
color_discrete_map={g: get_grade_color(g) for g in df_plot["rank"].unique()},
height=600,
)
fig.update_layout(xaxis_tickangle=-45, xaxis_title="File")
return fig
# Initialize session state
if "report_data" not in st.session_state:
st.session_state.report_data = None
if "df" not in st.session_state:
st.session_state.df = None
# Sidebar for file upload
st.sidebar.title("📊 Radon Report Analyzer")
# File upload
uploaded_file = st.sidebar.file_uploader(
"Upload JSON Report",
type=["json"],
help="Upload the cyclomatic complexity report from radon library",
)
if uploaded_file:
try:
report_data = json.load(uploaded_file)
st.session_state.report_data = report_data
st.session_state.df = flatten_report(report_data)
# Reset scatter plot file selection when new file is uploaded
if "selected_scatter_files" in st.session_state:
del st.session_state.selected_scatter_files
st.sidebar.success("✅ Report loaded successfully!")
except json.JSONDecodeError:
st.sidebar.error("❌ Invalid JSON file")
except Exception as e:
st.sidebar.error(f"❌ Error loading file: {str(e)}")
# Main app logic
if st.session_state.df is not None and len(st.session_state.df) > 0:
df = st.session_state.df.copy()
# drop duplicate rows by name (drop the one with NaN parrent)
df = df.drop_duplicates(subset=["name", "filepath", "lineno"], keep="first")
# Create tabs
tab1, tab2, tab3, tab4 = st.tabs(
["📈 Overview", "🔍 Analysis", "⚠️ Warnings", "📋 Details"]
)
# ===== TAB 1: OVERVIEW =====
with tab1:
col1, col2, col3, col4 = st.columns(4)
with col1:
st.metric("Total Items", len(df))
with col2:
st.metric("Total Files", df["filepath"].nunique())
with col3:
avg_complexity = df["complexity"].mean()
st.metric("Avg Complexity", f"{avg_complexity:.2f}")
with col4:
max_complexity = df["complexity"].max()
st.metric("Max Complexity", max_complexity)
st.divider()
col1, col2 = st.columns(2)
with col1:
st.plotly_chart(create_complexity_chart(df), use_container_width=True)
with col2:
st.plotly_chart(create_grade_chart(df), use_container_width=True)
st.divider()
st.subheader("📍 Complexity by File and Grade")
# File filter for scatter plot - show only filename, not full path
filepath_to_filename = {fp: fp.split("/")[-1] for fp in df["filepath"].unique()}
filename_to_filepath = {v: k for k, v in filepath_to_filename.items()}
# Initialize selected files in session state if not exists
if "selected_scatter_files" not in st.session_state:
st.session_state.selected_scatter_files = sorted(
filepath_to_filename.values()
)
# Select all / Remove all buttons
col_btn1, col_btn2, col_spacer = st.columns([1, 1, 6])
with col_btn1:
if st.button("Select All", use_container_width=True):
st.session_state.selected_scatter_files = sorted(
filepath_to_filename.values()
)
st.rerun()
with col_btn2:
if st.button("Remove All", use_container_width=True):
st.session_state.selected_scatter_files = []
st.rerun()
st.write("**Select files to display:**")
scatter_file_filter_display = st.pills(
"Filter files",
options=sorted(filepath_to_filename.values()),
selection_mode="multi",
default=st.session_state.selected_scatter_files,
label_visibility="collapsed",
key="scatter_plot_file_filter",
)
# Update session state
st.session_state.selected_scatter_files = (
scatter_file_filter_display if scatter_file_filter_display else []
)
# Convert selected filenames back to full paths
scatter_file_filter = [
filename_to_filepath[fn] for fn in (scatter_file_filter_display or [])
]
# Apply file filter for scatter plot
if scatter_file_filter:
scatter_df = df[df["filepath"].isin(scatter_file_filter)]
else:
scatter_df = pd.DataFrame() # Empty dataframe when no files selected
if len(scatter_df) > 0:
st.plotly_chart(create_scatter_plot(scatter_df), use_container_width=True)
else:
st.info("No data to display. Please select at least one file.")
# ===== TAB 2: ANALYSIS WITH FILTERS =====
with tab2:
st.subheader("Filter & Sort Data")
col1, col2, col3, col4 = st.columns(4)
with col1:
type_filter = st.multiselect(
"Type",
options=df["type"].unique(),
default=df["type"].unique(),
help="Filter by item type",
)
with col2:
grade_filter = st.multiselect(
"Grade",
options=sorted(df["rank"].unique()),
default=sorted(df["rank"].unique()),
help="Filter by grade",
)
with col3:
complexity_range = st.slider(
"Complexity Range",
min_value=int(df["complexity"].min()),
max_value=int(df["complexity"].max()),
value=(int(df["complexity"].min()), int(df["complexity"].max())),
help="Filter by complexity level",
)
with col4:
filepath_filter = st.multiselect(
"Files",
options=sorted(df["filepath"].unique()),
default=sorted(df["filepath"].unique()),
help="Filter by file",
)
# Apply filters
filtered_df = df[
(df["type"].isin(type_filter))
& (df["rank"].isin(grade_filter))
& (df["complexity"] >= complexity_range[0])
& (df["complexity"] <= complexity_range[1])
& (df["filepath"].isin(filepath_filter))
]
col1, col2 = st.columns(2)
with col1:
sort_by = st.selectbox(
"Sort by",
options=[
"Complexity (High→Low)",
"Complexity (Low→High)",
"Grade (Best→Worst)",
"Name (A→Z)",
"File Path",
"Line Number",
],
help="Sort the filtered results",
)
with col2:
search_term = st.text_input(
"Search by name", help="Search for specific function/class names"
)
# Apply sorting
if sort_by == "Complexity (High→Low)":
filtered_df = filtered_df.sort_values("complexity", ascending=False)
elif sort_by == "Complexity (Low→High)":
filtered_df = filtered_df.sort_values("complexity", ascending=True)
elif sort_by == "Grade (Best→Worst)":
grade_order = {"A": 1, "B": 2, "C": 3, "D": 4, "F": 5}
filtered_df = filtered_df.sort_values(
"rank", key=lambda x: x.map(grade_order)
)
elif sort_by == "Name (A→Z)":
filtered_df = filtered_df.sort_values("name")
elif sort_by == "File Path":
filtered_df = filtered_df.sort_values("filepath")
elif sort_by == "Line Number":
filtered_df = filtered_df.sort_values("lineno")
# Apply search
if search_term:
filtered_df = filtered_df[
filtered_df["name"].str.contains(search_term, case=False, na=False)
]
st.info(f"Showing {len(filtered_df)} of {len(df)} items")
# Display table with color coding
def style_dataframe(val, column):
if column == "rank":
color = get_grade_color(val)
return f"background-color: {color}; color: white; font-weight: bold;"
elif column == "complexity":
color = get_complexity_color(int(val))
return f"background-color: {color}; color: white;"
return ""
display_df = filtered_df[
["filepath", "type", "name", "complexity", "rank", "lineno", "endline"]
].copy()
display_df = display_df.reset_index(drop=True)
st.dataframe(
display_df,
use_container_width=True,
column_config={
"complexity": st.column_config.NumberColumn(width="small"),
"rank": st.column_config.TextColumn(width="small"),
"lineno": st.column_config.NumberColumn(width="small"),
"endline": st.column_config.NumberColumn(width="small"),
"type": st.column_config.TextColumn(width="small"),
},
)
# ===== TAB 3: WARNINGS =====
with tab3:
st.subheader("⚠️ Items Requiring Investigation")
col1, col2 = st.columns(2)
with col1:
complexity_threshold = st.slider(
"Complexity Threshold",
min_value=1,
max_value=int(df["complexity"].max()),
value=10,
help="Items with complexity >= this value will be flagged",
)
with col2:
risky_grades = st.multiselect(
"Risky Grades",
options=["A", "B", "C", "D", "E", "F"],
default=["D", "E", "F"],
help="Grades considered risky",
)
risky_df = identify_risky_items(df, complexity_threshold, risky_grades)
if len(risky_df) > 0:
st.warning(f"⚠️ Found {len(risky_df)} items that need investigation")
# Group by severity
col1, col2 = st.columns(2)
with col1:
high_risk = risky_df[risky_df["complexity"] >= complexity_threshold + 5]
st.metric("High Risk (Very High Complexity)", len(high_risk))
with col2:
bad_grade = risky_df[risky_df["rank"].isin(["D", "F"])]
st.metric("Bad Grade Items", len(bad_grade))
st.divider()
# Detailed view of risky items
for idx, (_, row) in enumerate(risky_df.head(20).iterrows(), 1):
with st.expander(
f"🚨 {row['name']} (Complexity: {row['complexity']}, Grade: {row['rank']})",
expanded=(idx == 1),
):
col1, col2, col3, col4 = st.columns(4)
with col1:
st.metric("Complexity", row["complexity"])
with col2:
st.write(f"**Grade:** {row['rank']}")
with col3:
st.write(f"**Type:** {row['type']}")
with col4:
st.write(f"**Lines:** {row['lineno']}-{row['endline']}")
st.write(f"**File:** `{row['filepath']}`")
full_name = (
f"{row['classname']}.{row['name']}"
if row["type"] == "method"
else row["name"]
)
st.write(f"**Full Name:** `{full_name}`")
# Recommendation
if row["complexity"] >= complexity_threshold + 5:
st.error("🔴 **CRITICAL:** This needs immediate refactoring")
elif row["complexity"] >= complexity_threshold:
st.warning(
"🟠 **HIGH:** Consider breaking this into smaller functions"
)
if row["rank"] in ["D", "E", "F"]:
st.warning(
f"**Grade {row['rank']}:** Code quality is poor, refactoring recommended"
)
else:
st.success("✅ No risky items found! Your code looks good.")
# ===== TAB 4: DETAILED VIEW =====
with tab4:
st.subheader("Detailed Item Analysis")
# Select item to analyze
df_display = df.copy()
df_display["display_name"] = df_display.apply(
lambda x: f"{x['name']} ({x['type']}) - {x['filepath'].split('/')[-1]}",
axis=1,
)
selected_item = st.selectbox(
"Select an item to analyze",
options=df_display.index,
format_func=lambda x: df_display.loc[x, "display_name"],
)
if selected_item is not None:
item = df.iloc[selected_item]
# Header with grade badge
col1, col2 = st.columns([3, 1])
with col1:
st.title(item["name"])
with col2:
grade_html = display_grade_badge(item["rank"])
st.markdown(grade_html, unsafe_allow_html=True)
st.divider()
# Detailed metrics
col1, col2, col3, col4, col5 = st.columns(5)
with col1:
st.metric("Complexity", item["complexity"])
with col2:
st.metric("Type", item["type"])
with col3:
st.metric("Start Line", int(item["lineno"]))
with col4:
st.metric("End Line", int(item["endline"]))
with col5:
st.metric("Lines of Code", int(item["endline"] - item["lineno"] + 1))
st.divider()
# File and location info
col1, col2 = st.columns(2)
with col1:
st.write("**File Path:**")
st.code(item["filepath"], language="text")
with col2:
st.write("**Location:**")
st.code(
f"Line {int(item['lineno'])} to {int(item['endline'])}, Column {int(item['col_offset'])}",
language="text",
)
if item["classname"]:
st.write("**Class Name:**")
st.code(item["classname"], language="text")
st.divider()
# Recommendations
st.subheader("💡 Recommendations")
complexity = int(item["complexity"])
if complexity <= 3:
st.success(
"✅ **Simple:** This code is easy to understand and maintain."
)
elif complexity <= 7:
st.info(
"ℹ️ **Moderate:** Code is reasonably complex. Consider breaking into smaller functions if it exceeds 7."
)
elif complexity <= 10:
st.warning(
"⚠️ **Complex:** This code is complex and may be difficult to maintain. Consider refactoring."
)
else:
st.error(
"🔴 **Very Complex:** This code needs immediate refactoring. Break it into smaller, testable units."
)
if item["rank"] in ["D", "E", "F"]:
st.error(
f"📉 **Grade {item['rank']}:** Code quality needs improvement."
)
else:
# Landing page
st.title("📊 Radon Complexity Analyzer")
st.markdown(
"""
Welcome to the Radon Cyclomatic Complexity Analyzer!
This tool helps you analyze and visualize Python code complexity reports from the **radon** library.
### Features:
- 📈 **Overview:** See complexity distribution across your codebase
- 🔍 **Analysis:** Filter, sort, and search for specific functions/classes
- ⚠️ **Warnings:** Identify items that need immediate attention
- 📋 **Details:** Get detailed analysis and recommendations for each item
### How to use:
1. Generate a radon complexity report as JSON:
```bash
radon cc your_project/ -j > report.json
```
2. Upload the JSON file using the sidebar
3. Explore and analyze your code complexity!
"""
)
# Create sample data for demonstration
st.divider()
st.subheader("Or try with sample data:")
if st.button("Load Sample Report"):
sample_file_path = Path(__file__).parent / "sample_report.json"
try:
with open(sample_file_path, "r") as f:
sample_report = json.load(f)
st.session_state.report_data = sample_report
st.session_state.df = flatten_report(sample_report)
# Reset scatter plot file selection when sample is loaded
if "selected_scatter_files" in st.session_state:
del st.session_state.selected_scatter_files
st.success("✅ Sample data loaded! Refresh the page to see the analysis.")
st.rerun()
except FileNotFoundError:
st.error("❌ Sample report file not found. Please upload your own report.")
except Exception as e:
st.error(f"❌ Error loading sample data: {str(e)}")
# sample_report = {
# "example/settings.py": [
# {
# "type": "class",
# "rank": "A",
# "lineno": 7,
# "complexity": 1,
# "endline": 8,
# "name": "DBSettings",
# "col_offset": 0,
# "methods": [],
# },
# {
# "type": "class",
# "rank": "B",
# "lineno": 11,
# "complexity": 5,
# "endline": 13,
# "name": "ComplexSettings",
# "col_offset": 0,
# "methods": [
# {
# "type": "method",
# "rank": "C",
# "lineno": 12,
# "classname": "ComplexSettings",
# "complexity": 8,
# "endline": 13,
# "name": "validate",
# "col_offset": 4,
# "closures": [],
# }
# ],
# },
# ],
# "example/base.py": [
# {
# "type": "function",
# "rank": "F",
# "lineno": 1,
# "complexity": 15,
# "endline": 50,
# "name": "complex_function",
# "col_offset": 0,
# }
# ],
# }
# st.session_state.report_data = sample_report
# st.session_state.df = flatten_report(sample_report)
# st.success("✅ Sample data loaded! Refresh the page to see the analysis.")
# st.rerun()