|
|
import plotly.graph_objects as go |
|
|
import pandas as pd |
|
|
|
|
|
def create_verdict_donut(df: pd.DataFrame) -> go.Figure: |
|
|
""" |
|
|
Creates a High-Fidelity Donut Chart. |
|
|
""" |
|
|
if df.empty: |
|
|
return _create_empty_figure("No Data") |
|
|
|
|
|
color_map = { |
|
|
'resolved': '#10B981', |
|
|
'possibly_resolved': '#34D399', |
|
|
'duplicate': '#F59E0B', |
|
|
'unresolved': '#64748B', |
|
|
'error': '#EF4444', |
|
|
'unknown': '#CBD5E1' |
|
|
} |
|
|
|
|
|
colors = [color_map.get(v, '#999') for v in df['verdict']] |
|
|
total = df['count'].sum() |
|
|
|
|
|
fig = go.Figure(data=[go.Pie( |
|
|
labels=df['verdict'], |
|
|
values=df['count'], |
|
|
hole=0.7, |
|
|
marker=dict(colors=colors, line=dict(color='#1F2937', width=0)), |
|
|
textinfo='value', |
|
|
hoverinfo='label+percent', |
|
|
textfont=dict(size=14, color='white') |
|
|
)]) |
|
|
|
|
|
fig.update_layout( |
|
|
title=dict( |
|
|
text="Analysis Distribution", |
|
|
font=dict(size=14, color="#6B7280"), |
|
|
x=0.5, |
|
|
xanchor='center' |
|
|
), |
|
|
|
|
|
showlegend=True, |
|
|
legend=dict( |
|
|
orientation="h", |
|
|
yanchor="top", |
|
|
y=-0.1, |
|
|
xanchor="center", |
|
|
x=0.5, |
|
|
font=dict(color="#9CA3AF", size=11) |
|
|
), |
|
|
|
|
|
margin=dict(t=40, b=60, l=20, r=20), |
|
|
height=250, |
|
|
paper_bgcolor='rgba(0,0,0,0)', |
|
|
plot_bgcolor='rgba(0,0,0,0)', |
|
|
|
|
|
annotations=[dict(text=str(total), x=0.5, y=0.5, font_size=32, showarrow=False, font_color="#E5E7EB")], |
|
|
|
|
|
modebar_remove=['zoom', 'pan', 'select', 'lasso2d', 'zoomIn2d', 'zoomOut2d', 'autoScale2d', 'resetScale2d', 'toImage'] |
|
|
) |
|
|
|
|
|
return fig |
|
|
|
|
|
def create_timeline_chart(full_df: pd.DataFrame) -> go.Figure: |
|
|
""" |
|
|
Creates a Timeline showing activity over time. |
|
|
""" |
|
|
if full_df.empty: |
|
|
return _create_empty_figure("No Timeline Data") |
|
|
|
|
|
df = full_df.copy() |
|
|
|
|
|
df['date'] = pd.to_datetime(df['updated_at'], errors='coerce').dt.date |
|
|
daily_counts = df.groupby('date').size().reset_index(name='count') |
|
|
|
|
|
fig = go.Figure() |
|
|
|
|
|
fig.add_trace(go.Scatter( |
|
|
x=daily_counts['date'], |
|
|
y=daily_counts['count'], |
|
|
mode='lines+markers', |
|
|
name='Activity', |
|
|
line=dict(color='#3B82F6', width=2, shape='spline'), |
|
|
marker=dict(size=4, color='#60A5FA', line=dict(width=0)), |
|
|
fill='tozeroy', |
|
|
fillcolor='rgba(59, 130, 246, 0.1)' |
|
|
)) |
|
|
|
|
|
fig.update_layout( |
|
|
title=dict( |
|
|
text="Activity Timeline (Last Updates)", |
|
|
font=dict(size=14, color="#6B7280"), |
|
|
x=0.5, |
|
|
xanchor='center' |
|
|
), |
|
|
xaxis=dict( |
|
|
showgrid=False, |
|
|
color="#9CA3AF", |
|
|
gridcolor="rgba(255,255,255,0.1)", |
|
|
tickformat="%b %d" |
|
|
), |
|
|
yaxis=dict( |
|
|
showgrid=True, |
|
|
gridcolor="rgba(255,255,255,0.05)", |
|
|
color="#9CA3AF", |
|
|
zeroline=False |
|
|
), |
|
|
margin=dict(t=40, b=20, l=30, r=10), |
|
|
height=250, |
|
|
paper_bgcolor='rgba(0,0,0,0)', |
|
|
plot_bgcolor='rgba(0,0,0,0)', |
|
|
showlegend=False, |
|
|
|
|
|
modebar_remove=['zoom', 'pan', 'select', 'lasso2d', 'zoomIn2d', 'zoomOut2d', 'autoScale2d', 'resetScale2d', 'toImage'] |
|
|
) |
|
|
|
|
|
return fig |
|
|
|
|
|
def _create_empty_figure(text): |
|
|
fig = go.Figure() |
|
|
fig.add_annotation(text=text, showarrow=False, font=dict(color="gray")) |
|
|
fig.update_layout( |
|
|
height=250, |
|
|
paper_bgcolor='rgba(0,0,0,0)', |
|
|
plot_bgcolor='rgba(0,0,0,0)', |
|
|
xaxis=dict(visible=False), |
|
|
yaxis=dict(visible=False), |
|
|
modebar_remove=['zoom', 'pan', 'select', 'lasso2d', 'zoomIn2d', 'zoomOut2d', 'autoScale2d', 'resetScale2d', 'toImage'] |
|
|
) |
|
|
return fig |
|
|
|
|
|
def create_efficiency_funnel(stats_df: pd.DataFrame, total_open_issues: int) -> go.Figure: |
|
|
""" |
|
|
Creates a Funnel Chart showing how the AI filters the noise. |
|
|
""" |
|
|
if stats_df.empty: |
|
|
return _create_empty_figure("No Data") |
|
|
|
|
|
total_backlog = total_open_issues |
|
|
total_analyzed = stats_df['count'].sum() |
|
|
|
|
|
ai_solved = stats_df[stats_df['verdict'].isin(['resolved', 'duplicate', 'possibly_resolved'])]['count'].sum() |
|
|
|
|
|
|
|
|
values = [total_backlog, total_analyzed, ai_solved] |
|
|
stages = ["Total Backlog", "Analyzed by AI", "Actionable Findings"] |
|
|
|
|
|
fig = go.Figure(go.Funnel( |
|
|
y=stages, |
|
|
x=values, |
|
|
textinfo="value+percent initial", |
|
|
|
|
|
textfont=dict(color="#e5e5e5", size=12), |
|
|
marker=dict(color=["#64748B", "#4486F0", "#10B981"]), |
|
|
connector=dict(line=dict(color="rgba(128,128,128,0.5)", width=1)) |
|
|
)) |
|
|
|
|
|
fig.update_layout( |
|
|
title=dict( |
|
|
text="AI Efficiency Funnel", |
|
|
font=dict(size=14, color="#6B7280"), |
|
|
x=0.5, xanchor='center' |
|
|
), |
|
|
xaxis=dict( |
|
|
showgrid=False, |
|
|
color="#9CA3AF", |
|
|
gridcolor="rgba(255,255,255,0.1)", |
|
|
tickformat="%b %d" |
|
|
), |
|
|
yaxis=dict( |
|
|
showgrid=True, |
|
|
gridcolor="rgba(255,255,255,0.1)", |
|
|
color="#9CA3AF", |
|
|
zeroline=False |
|
|
), |
|
|
margin=dict(t=40, b=20, l=100, r=10), |
|
|
height=250, |
|
|
paper_bgcolor='rgba(0,0,0,0)', |
|
|
plot_bgcolor='rgba(0,0,0,0)', |
|
|
showlegend=False, |
|
|
modebar_remove=['pan', 'select', 'lasso2d', 'autoScale2d', 'resetScale2d'] |
|
|
) |
|
|
|
|
|
return fig |