Spaces:
Sleeping
Sleeping
Kaveh commited on
Update app.py
Browse files
app.py
CHANGED
|
@@ -1,7 +1,7 @@
|
|
| 1 |
# -*- coding: utf-8 -*-
|
| 2 |
# =============================================
|
| 3 |
# Gradio App for Chess Game Analysis - Lichess API Version
|
| 4 |
-
# v18:
|
| 5 |
# =============================================
|
| 6 |
|
| 7 |
import gradio as gr
|
|
@@ -28,35 +28,69 @@ ECO_CSV_PATH = "eco_to_opening.csv"
|
|
| 28 |
TITLES_TO_ANALYZE = ['GM', 'IM', 'FM', 'CM', 'WGM', 'WIM', 'WFM', 'WCM', 'NM']
|
| 29 |
|
| 30 |
# =============================================
|
| 31 |
-
# Helper Function: Categorize Time Control
|
| 32 |
# =============================================
|
| 33 |
-
# ... (Function identical to v17 - Assumed correct now) ...
|
| 34 |
def categorize_time_control(tc_str, speed_info):
|
| 35 |
-
|
| 36 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 37 |
if '+' in tc_str:
|
| 38 |
-
|
| 39 |
-
|
| 40 |
-
|
| 41 |
-
|
| 42 |
-
|
| 43 |
-
|
| 44 |
-
|
| 45 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 46 |
return 'Unknown'
|
|
|
|
|
|
|
| 47 |
else:
|
| 48 |
-
|
| 49 |
-
|
| 50 |
-
|
| 51 |
-
|
| 52 |
-
|
| 53 |
-
|
| 54 |
-
|
| 55 |
-
|
| 56 |
-
|
| 57 |
-
|
| 58 |
-
|
| 59 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 60 |
|
| 61 |
# =============================================
|
| 62 |
# Helper Function: Load ECO Mapping (Unchanged)
|
|
@@ -72,14 +106,14 @@ except FileNotFoundError: print(f"WARN: ECO file '{ECO_CSV_PATH}' not found.")
|
|
| 72 |
except Exception as e: print(f"WARN: Error loading ECO file: {e}")
|
| 73 |
|
| 74 |
# =============================================
|
| 75 |
-
# API Data Loading and Processing Function (
|
| 76 |
# =============================================
|
| 77 |
-
|
| 78 |
-
def load_from_lichess_api(username: str, time_period_key: str, perf_type: str, rated: bool, eco_map: dict, progress=None):
|
| 79 |
-
|
| 80 |
if not username: return pd.DataFrame(), "⚠️ Enter username."
|
| 81 |
if not perf_type: return pd.DataFrame(), "⚠️ Select game type."
|
| 82 |
-
if progress: progress(0, desc="Initializing...")
|
| 83 |
username_lower=username.lower(); status_message=f"Fetching {perf_type} games..."
|
| 84 |
if progress: progress(0.1, desc=status_message);
|
| 85 |
since_timestamp_ms=None; time_delta=TIME_PERIOD_OPTIONS.get(time_period_key)
|
|
@@ -91,19 +125,10 @@ def load_from_lichess_api(username: str, time_period_key: str, perf_type: str, r
|
|
| 91 |
try:
|
| 92 |
response=requests.get(api_url, params=api_params, headers=headers, stream=True); response.raise_for_status()
|
| 93 |
if progress: progress(0.3, desc="Processing stream...")
|
| 94 |
-
# Estimate total iterations for progress (difficult for streams, use a large number or steps)
|
| 95 |
-
# Let's update every N lines instead.
|
| 96 |
-
update_interval = 50 # Update progress every 50 games processed
|
| 97 |
-
|
| 98 |
for line in response.iter_lines():
|
| 99 |
if line:
|
| 100 |
lines_processed += 1; game_data_raw=line.decode('utf-8'); game_data=None;
|
| 101 |
-
|
| 102 |
-
if progress and lines_processed % update_interval == 0:
|
| 103 |
-
# Simple pulsing progress indication
|
| 104 |
-
progress(0.3 + (lines_processed % (update_interval * 10)) / (update_interval * 20.0),
|
| 105 |
-
desc=f"Processing game ~{lines_processed}...")
|
| 106 |
-
# --- End Manual Progress ---
|
| 107 |
try:
|
| 108 |
game_data=json.loads(game_data_raw); white_info=game_data.get('players',{}).get('white',{}); black_info=game_data.get('players',{}).get('black',{})
|
| 109 |
white_user=white_info.get('user',{}); black_user=black_info.get('user',{}); opening_info=game_data.get('opening',{}); clock_info=game_data.get('clock')
|
|
@@ -149,7 +174,7 @@ def load_from_lichess_api(username: str, time_period_key: str, perf_type: str, r
|
|
| 149 |
if df.empty: return df, "⚠️ No games with valid dates."
|
| 150 |
df['Year']=df['Date'].dt.year; df['Month']=df['Date'].dt.month; df['Day']=df['Date'].dt.day; df['Hour']=df['Date'].dt.hour; df['DayOfWeekNum']=df['Date'].dt.dayofweek; df['DayOfWeekName']=df['Date'].dt.day_name()
|
| 151 |
df['PlayerElo']=df['PlayerElo'].astype(int); df['OpponentElo']=df['OpponentElo'].astype(int)
|
| 152 |
-
df['EloDiff']=df['PlayerElo']-df['OpponentElo']; df['TimeControl_Category']=df.apply(lambda r: categorize_time_control(r['TimeControl'], r['Speed']), axis=1)
|
| 153 |
df=df.sort_values(by='Date').reset_index(drop=True)
|
| 154 |
if progress: progress(1, desc="Complete!")
|
| 155 |
return df, status_message
|
|
@@ -293,16 +318,15 @@ def filter_and_analyze_time_forfeits(df):
|
|
| 293 |
return tf_games, wins_tf, losses_tf
|
| 294 |
|
| 295 |
# =============================================
|
| 296 |
-
# Gradio Main Analysis Function (
|
| 297 |
# =============================================
|
| 298 |
# ... (Function identical to v15) ...
|
| 299 |
-
def perform_full_analysis(username, time_period_key, perf_type, selected_titles_list, progress=gr.Progress(track_tqdm=True)):
|
| 300 |
df, status_msg = load_from_lichess_api(username, time_period_key, perf_type, DEFAULT_RATED_ONLY, ECO_MAPPING, progress)
|
| 301 |
-
num_outputs =
|
| 302 |
if not isinstance(df, pd.DataFrame) or df.empty:
|
| 303 |
-
return status_msg, pd.DataFrame(), *( [None] * (num_outputs - 2) )
|
| 304 |
try:
|
| 305 |
-
# Generate all plots and data...
|
| 306 |
fig_pie=plot_win_loss_pie(df,username); fig_color=plot_win_loss_by_color(df); fig_rating=plot_rating_trend(df,username); fig_elo_diff=plot_performance_vs_opponent_elo(df)
|
| 307 |
total_g=len(df); w=len(df[df['PlayerResultNumeric']==1]); l=len(df[df['PlayerResultNumeric']==0]); d=len(df[df['PlayerResultNumeric']==0.5])
|
| 308 |
wr=(w/total_g*100) if total_g>0 else 0; avg_opp=df['OpponentElo'].mean(); overview_stats_md=f"**Total:** {total_g:,} | **WR:** {wr:.1f}% | **W/L/D:** {w}/{l}/{d} | **Avg Opp:** {avg_opp:.0f if not pd.isna(avg_opp) else 'N/A'}"
|
|
@@ -317,7 +341,6 @@ def perform_full_analysis(username, time_period_key, perf_type, selected_titles_
|
|
| 317 |
df_tf_list=tf_games[['Date','OpponentName','PlayerColor','PlayerResultString','TimeControl','PlyCount','Termination']].sort_values('Date',ascending=False).head(20) if not tf_games.empty else pd.DataFrame()
|
| 318 |
term_counts=df['Termination'].value_counts(); fig_term_all=px.bar(term_counts,x=term_counts.index,y=term_counts.values,title="Overall Termination Reasons",labels={'x':'Reason','y':'Count'},text=term_counts.values)
|
| 319 |
fig_term_all.update_layout(dragmode=False); fig_term_all.update_traces(textposition='outside')
|
| 320 |
-
# Generate Titled Player analysis...
|
| 321 |
titled_status_msg = ""; fig_titled_pie, fig_titled_color, fig_titled_rating, df_titled_h2h = go.Figure(), go.Figure(), go.Figure(), pd.DataFrame()
|
| 322 |
if selected_titles_list:
|
| 323 |
titled_games = filter_and_analyze_titled(df, selected_titles_list)
|
|
@@ -332,16 +355,9 @@ def perform_full_analysis(username, time_period_key, perf_type, selected_titles_
|
|
| 332 |
df_titled_h2h = h2h.sort_values('Total', ascending=False).reset_index()
|
| 333 |
else: titled_status_msg = f"ℹ️ No games found vs selected titles ({', '.join(selected_titles_list)})."
|
| 334 |
else: titled_status_msg = "ℹ️ Select titles from the sidebar to analyze."
|
| 335 |
-
# Return
|
| 336 |
-
|
| 337 |
-
|
| 338 |
-
# print(f"DEBUG: Returning {len(return_tuple)} items from perform_full_analysis") # Check length if needed
|
| 339 |
-
return return_tuple
|
| 340 |
-
except Exception as e:
|
| 341 |
-
error_msg = f"🚨 Error generating results: {e}\n{traceback.format_exc()}";
|
| 342 |
-
num_outputs = 32 # Updated count based on return tuple above
|
| 343 |
-
return error_msg, pd.DataFrame(), *( [None] * (num_outputs - 2) )
|
| 344 |
-
|
| 345 |
|
| 346 |
# =============================================
|
| 347 |
# Gradio Interface Definition (Corrected UI Syntax)
|
|
@@ -353,12 +369,22 @@ with gr.Blocks(theme=gr.themes.Soft(), css=css) as demo:
|
|
| 353 |
|
| 354 |
with gr.Row():
|
| 355 |
with gr.Column(scale=1, min_width=250): # Sidebar
|
| 356 |
-
gr.Markdown("## ⚙️ Settings")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 357 |
with gr.Column(scale=4): # Main Content
|
| 358 |
# Define Output Components - Order Matters! Match return tuple exactly.
|
| 359 |
overview_plot_pie=gr.Plot(label="Overall Results"); overview_stats_md_out=gr.Markdown(); overview_plot_color=gr.Plot(label="Results by Color"); overview_plot_rating=gr.Plot(label="Rating Trend"); overview_plot_elo_diff=gr.Plot(label="Elo Advantage vs. Result")
|
| 360 |
time_plot_games_yr=gr.Plot(label="Games per Year"); time_plot_wr_yr=gr.Plot(label="Win Rate per Year")
|
| 361 |
-
color_plot_placeholder=gr.Markdown()
|
| 362 |
time_plot_games_dow=gr.Plot(label="Games by Day of Week"); time_plot_wr_dow=gr.Plot(label="Win Rate by Day of Week"); time_plot_games_hod=gr.Plot(label="Games by Hour (UTC)"); time_plot_wr_hod=gr.Plot(label="Win Rate by Hour (UTC)"); time_plot_games_dom=gr.Plot(label="Games by Day of Month"); time_plot_wr_dom=gr.Plot(label="Win Rate by Day of Month"); time_plot_perf_tc=gr.Plot(label="Performance by Time Control")
|
| 363 |
eco_plot_freq_api=gr.Plot(label="Opening Frequency (API)"); eco_plot_wr_api=gr.Plot(label="Opening Win Rate (API)"); eco_plot_freq_cust=gr.Plot(label="Opening Frequency (Custom)"); eco_plot_wr_cust=gr.Plot(label="Opening Win Rate (Custom)")
|
| 364 |
opp_plot_freq=gr.Plot(label="Frequent Opponents"); opp_df_list=gr.DataFrame(label="Top Opponents List", wrap=True); opp_plot_elo=gr.Plot(label="Elo Advantage vs Result")
|
|
@@ -370,108 +396,104 @@ with gr.Blocks(theme=gr.themes.Soft(), css=css) as demo:
|
|
| 370 |
with gr.TabItem("1. Overview", id=0):
|
| 371 |
overview_stats_md_out # Display metrics
|
| 372 |
with gr.Row():
|
| 373 |
-
|
| 374 |
-
|
| 375 |
overview_plot_rating
|
| 376 |
overview_plot_elo_diff
|
| 377 |
|
| 378 |
with gr.TabItem("2. Perf. Over Time", id=1):
|
| 379 |
-
|
| 380 |
-
|
| 381 |
-
|
| 382 |
-
time_plot_wr_yr
|
| 383 |
|
| 384 |
with gr.TabItem("3. Perf. by Color", id=2):
|
| 385 |
-
|
| 386 |
-
|
| 387 |
|
| 388 |
with gr.TabItem("4. Time & Date", id=3):
|
| 389 |
-
|
| 390 |
-
|
| 391 |
-
|
| 392 |
-
|
| 393 |
-
|
| 394 |
-
|
| 395 |
-
|
| 396 |
-
|
| 397 |
-
|
| 398 |
-
|
| 399 |
-
|
| 400 |
-
|
| 401 |
-
|
| 402 |
-
|
| 403 |
|
| 404 |
with gr.TabItem("5. ECO & Openings", id=4):
|
| 405 |
-
|
| 406 |
-
|
| 407 |
-
|
| 408 |
-
|
| 409 |
-
|
| 410 |
-
|
| 411 |
-
|
| 412 |
-
|
| 413 |
-
|
| 414 |
-
|
|
|
|
| 415 |
|
| 416 |
with gr.TabItem("6. Opponents", id=5):
|
| 417 |
-
|
| 418 |
-
|
| 419 |
-
|
| 420 |
|
| 421 |
with gr.TabItem("7. vs Titled", id=6):
|
| 422 |
-
|
| 423 |
-
|
| 424 |
-
|
| 425 |
-
|
| 426 |
-
|
| 427 |
-
|
| 428 |
-
|
| 429 |
|
| 430 |
with gr.TabItem("8. Termination", id=7):
|
| 431 |
-
|
| 432 |
-
|
| 433 |
-
|
| 434 |
-
|
| 435 |
-
|
| 436 |
-
|
| 437 |
-
|
| 438 |
|
| 439 |
# Define the list of output components in the exact order
|
| 440 |
outputs_list = [
|
| 441 |
-
status_output, df_state, # Status and State
|
| 442 |
overview_plot_pie, overview_stats_md_out, overview_plot_color, overview_plot_rating, overview_plot_elo_diff, # Tab 1
|
| 443 |
time_plot_games_yr, time_plot_wr_yr, # Tab 2
|
| 444 |
color_plot_placeholder, # Tab 3
|
| 445 |
time_plot_games_dow, time_plot_wr_dow, time_plot_games_hod, time_plot_wr_hod, time_plot_games_dom, time_plot_wr_dom, time_plot_perf_tc, # Tab 4
|
| 446 |
eco_plot_freq_api, eco_plot_wr_api, eco_plot_freq_cust, eco_plot_wr_cust, # Tab 5
|
| 447 |
opp_plot_freq, opp_df_list, opp_plot_elo, # Tab 6
|
| 448 |
-
titled_status, titled_plot_pie, titled_plot_color, titled_plot_rating, titled_df_h2h_comp, # Tab 7
|
| 449 |
term_plot_tf_summary, term_plot_tf_tc, term_df_tf_list, term_plot_all # Tab 8
|
| 450 |
]
|
| 451 |
-
# Calculate length dynamically
|
| 452 |
expected_outputs_count = len(outputs_list)
|
| 453 |
-
print(f"DEBUG: Expected number of outputs for Gradio: {expected_outputs_count}") # Log this!
|
| 454 |
|
| 455 |
-
# Modify the
|
| 456 |
def perform_full_analysis_wrapper(username, time_period_key, perf_type, selected_titles_list, progress=gr.Progress(track_tqdm=True)):
|
| 457 |
results = perform_full_analysis(username, time_period_key, perf_type, selected_titles_list, progress)
|
| 458 |
-
#
|
| 459 |
-
if len(results)
|
| 460 |
-
|
| 461 |
-
|
| 462 |
-
|
| 463 |
-
results = tuple(list(results) + [None] * (expected_outputs_count - len(results)))
|
| 464 |
-
else:
|
| 465 |
-
results = results[:expected_outputs_count]
|
| 466 |
return results
|
| 467 |
|
| 468 |
# Connect button click to the wrapper function
|
| 469 |
analyze_btn.click(
|
| 470 |
-
fn=perform_full_analysis_wrapper,
|
| 471 |
inputs=[username_input, time_period_input, perf_type_input, titled_player_select],
|
| 472 |
outputs=outputs_list
|
| 473 |
)
|
| 474 |
|
| 475 |
# --- Launch the Gradio App ---
|
| 476 |
if __name__ == "__main__":
|
| 477 |
-
demo.launch(debug=True)
|
|
|
|
| 1 |
# -*- coding: utf-8 -*-
|
| 2 |
# =============================================
|
| 3 |
# Gradio App for Chess Game Analysis - Lichess API Version
|
| 4 |
+
# v18: ABSOLUTELY FINAL fix for SyntaxError in categorize_time_control.
|
| 5 |
# =============================================
|
| 6 |
|
| 7 |
import gradio as gr
|
|
|
|
| 28 |
TITLES_TO_ANALYZE = ['GM', 'IM', 'FM', 'CM', 'WGM', 'WIM', 'WFM', 'WCM', 'NM']
|
| 29 |
|
| 30 |
# =============================================
|
| 31 |
+
# Helper Function: Categorize Time Control *** FINAL REWRITE ***
|
| 32 |
# =============================================
|
|
|
|
| 33 |
def categorize_time_control(tc_str, speed_info):
|
| 34 |
+
"""Categorizes time control based on speed info or parsed string."""
|
| 35 |
+
# 1. Prioritize speed info from API
|
| 36 |
+
if isinstance(speed_info, str) and speed_info in ['bullet', 'blitz', 'rapid', 'classical', 'correspondence']:
|
| 37 |
+
return speed_info.capitalize()
|
| 38 |
+
|
| 39 |
+
# 2. Handle invalid or special tc_str inputs
|
| 40 |
+
if not isinstance(tc_str, str) or tc_str in ['-', '?', 'Unknown']:
|
| 41 |
+
return 'Unknown'
|
| 42 |
+
if tc_str == 'Correspondence':
|
| 43 |
+
return 'Correspondence'
|
| 44 |
+
|
| 45 |
+
# 3. Handle format like "180+2"
|
| 46 |
if '+' in tc_str:
|
| 47 |
+
base = None
|
| 48 |
+
increment = None
|
| 49 |
+
try: # *** TRY block ONLY for splitting and converting ***
|
| 50 |
+
parts = tc_str.split('+')
|
| 51 |
+
if len(parts) == 2:
|
| 52 |
+
base = int(parts[0])
|
| 53 |
+
increment = int(parts[1])
|
| 54 |
+
else:
|
| 55 |
+
# If format is wrong (e.g., "180++2"), this might not raise error but base/increment remain None
|
| 56 |
+
pass # Continue to check if base/increment are valid outside try block
|
| 57 |
+
except (ValueError, IndexError):
|
| 58 |
+
# If int() conversion fails or split produces wrong number of parts leading to index error
|
| 59 |
+
return 'Unknown'
|
| 60 |
+
|
| 61 |
+
# *** Classification happens AFTER try-except, ONLY if conversion succeeded ***
|
| 62 |
+
if base is not None and increment is not None:
|
| 63 |
+
total = base + 40 * increment
|
| 64 |
+
if total >= 1500: return 'Classical'
|
| 65 |
+
if total >= 480: return 'Rapid'
|
| 66 |
+
if total >= 180: return 'Blitz'
|
| 67 |
+
if total > 0 : return 'Bullet'
|
| 68 |
+
# If conversion failed or classification didn't match, return Unknown
|
| 69 |
return 'Unknown'
|
| 70 |
+
|
| 71 |
+
# 4. Handle format like "300" (only base time)
|
| 72 |
else:
|
| 73 |
+
base = None
|
| 74 |
+
try: # *** TRY block ONLY for integer conversion ***
|
| 75 |
+
base = int(tc_str)
|
| 76 |
+
except ValueError: # *** EXCEPT block ONLY for integer conversion failure ***
|
| 77 |
+
# Fallback to keywords only if integer conversion fails
|
| 78 |
+
tc_lower = tc_str.lower()
|
| 79 |
+
if 'classical' in tc_lower: return 'Classical'
|
| 80 |
+
if 'rapid' in tc_lower: return 'Rapid'
|
| 81 |
+
if 'blitz' in tc_lower: return 'Blitz'
|
| 82 |
+
if 'bullet' in tc_lower: return 'Bullet'
|
| 83 |
+
return 'Unknown' # Failed conversion and keyword match
|
| 84 |
+
|
| 85 |
+
# *** Classification happens AFTER try-except, ONLY if conversion succeeded ***
|
| 86 |
+
if base is not None: # Check if conversion was successful
|
| 87 |
+
if base >= 1500: return 'Classical'
|
| 88 |
+
if base >= 480: return 'Rapid'
|
| 89 |
+
if base >= 180: return 'Blitz'
|
| 90 |
+
if base > 0 : return 'Bullet'
|
| 91 |
+
# If conversion failed (caught by except) or base is invalid (e.g., 0), return Unknown
|
| 92 |
+
return 'Unknown'
|
| 93 |
+
|
| 94 |
|
| 95 |
# =============================================
|
| 96 |
# Helper Function: Load ECO Mapping (Unchanged)
|
|
|
|
| 106 |
except Exception as e: print(f"WARN: Error loading ECO file: {e}")
|
| 107 |
|
| 108 |
# =============================================
|
| 109 |
+
# API Data Loading and Processing Function (Unchanged)
|
| 110 |
# =============================================
|
| 111 |
+
@gr.Progress(track_tqdm=True)
|
| 112 |
+
def load_from_lichess_api(username: str, time_period_key: str, perf_type: str, rated: bool, eco_map: dict, progress=None):
|
| 113 |
+
# ... (Code identical to version 15/16/17 - calls the fixed helper now) ...
|
| 114 |
if not username: return pd.DataFrame(), "⚠️ Enter username."
|
| 115 |
if not perf_type: return pd.DataFrame(), "⚠️ Select game type."
|
| 116 |
+
if progress: progress(0, desc="Initializing...");
|
| 117 |
username_lower=username.lower(); status_message=f"Fetching {perf_type} games..."
|
| 118 |
if progress: progress(0.1, desc=status_message);
|
| 119 |
since_timestamp_ms=None; time_delta=TIME_PERIOD_OPTIONS.get(time_period_key)
|
|
|
|
| 125 |
try:
|
| 126 |
response=requests.get(api_url, params=api_params, headers=headers, stream=True); response.raise_for_status()
|
| 127 |
if progress: progress(0.3, desc="Processing stream...")
|
|
|
|
|
|
|
|
|
|
|
|
|
| 128 |
for line in response.iter_lines():
|
| 129 |
if line:
|
| 130 |
lines_processed += 1; game_data_raw=line.decode('utf-8'); game_data=None;
|
| 131 |
+
if progress and lines_processed % 100 == 0: progress(0.3 + (lines_processed % 1000 / 2000), desc=f"Processing game {lines_processed}...")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 132 |
try:
|
| 133 |
game_data=json.loads(game_data_raw); white_info=game_data.get('players',{}).get('white',{}); black_info=game_data.get('players',{}).get('black',{})
|
| 134 |
white_user=white_info.get('user',{}); black_user=black_info.get('user',{}); opening_info=game_data.get('opening',{}); clock_info=game_data.get('clock')
|
|
|
|
| 174 |
if df.empty: return df, "⚠️ No games with valid dates."
|
| 175 |
df['Year']=df['Date'].dt.year; df['Month']=df['Date'].dt.month; df['Day']=df['Date'].dt.day; df['Hour']=df['Date'].dt.hour; df['DayOfWeekNum']=df['Date'].dt.dayofweek; df['DayOfWeekName']=df['Date'].dt.day_name()
|
| 176 |
df['PlayerElo']=df['PlayerElo'].astype(int); df['OpponentElo']=df['OpponentElo'].astype(int)
|
| 177 |
+
df['EloDiff']=df['PlayerElo']-df['OpponentElo']; df['TimeControl_Category']=df.apply(lambda r: categorize_time_control(r['TimeControl'], r['Speed']), axis=1) # Calls corrected func
|
| 178 |
df=df.sort_values(by='Date').reset_index(drop=True)
|
| 179 |
if progress: progress(1, desc="Complete!")
|
| 180 |
return df, status_message
|
|
|
|
| 318 |
return tf_games, wins_tf, losses_tf
|
| 319 |
|
| 320 |
# =============================================
|
| 321 |
+
# Gradio Main Analysis Function (Unchanged)
|
| 322 |
# =============================================
|
| 323 |
# ... (Function identical to v15) ...
|
| 324 |
+
def perform_full_analysis(username, time_period_key, perf_type, selected_titles_list, progress=gr.Progress(track_tqdm=True)):
|
| 325 |
df, status_msg = load_from_lichess_api(username, time_period_key, perf_type, DEFAULT_RATED_ONLY, ECO_MAPPING, progress)
|
| 326 |
+
num_outputs = 32 # Recalculated based on outputs list below
|
| 327 |
if not isinstance(df, pd.DataFrame) or df.empty:
|
| 328 |
+
return status_msg, pd.DataFrame(), *( [None] * (num_outputs - 2) )
|
| 329 |
try:
|
|
|
|
| 330 |
fig_pie=plot_win_loss_pie(df,username); fig_color=plot_win_loss_by_color(df); fig_rating=plot_rating_trend(df,username); fig_elo_diff=plot_performance_vs_opponent_elo(df)
|
| 331 |
total_g=len(df); w=len(df[df['PlayerResultNumeric']==1]); l=len(df[df['PlayerResultNumeric']==0]); d=len(df[df['PlayerResultNumeric']==0.5])
|
| 332 |
wr=(w/total_g*100) if total_g>0 else 0; avg_opp=df['OpponentElo'].mean(); overview_stats_md=f"**Total:** {total_g:,} | **WR:** {wr:.1f}% | **W/L/D:** {w}/{l}/{d} | **Avg Opp:** {avg_opp:.0f if not pd.isna(avg_opp) else 'N/A'}"
|
|
|
|
| 341 |
df_tf_list=tf_games[['Date','OpponentName','PlayerColor','PlayerResultString','TimeControl','PlyCount','Termination']].sort_values('Date',ascending=False).head(20) if not tf_games.empty else pd.DataFrame()
|
| 342 |
term_counts=df['Termination'].value_counts(); fig_term_all=px.bar(term_counts,x=term_counts.index,y=term_counts.values,title="Overall Termination Reasons",labels={'x':'Reason','y':'Count'},text=term_counts.values)
|
| 343 |
fig_term_all.update_layout(dragmode=False); fig_term_all.update_traces(textposition='outside')
|
|
|
|
| 344 |
titled_status_msg = ""; fig_titled_pie, fig_titled_color, fig_titled_rating, df_titled_h2h = go.Figure(), go.Figure(), go.Figure(), pd.DataFrame()
|
| 345 |
if selected_titles_list:
|
| 346 |
titled_games = filter_and_analyze_titled(df, selected_titles_list)
|
|
|
|
| 355 |
df_titled_h2h = h2h.sort_values('Total', ascending=False).reset_index()
|
| 356 |
else: titled_status_msg = f"ℹ️ No games found vs selected titles ({', '.join(selected_titles_list)})."
|
| 357 |
else: titled_status_msg = "ℹ️ Select titles from the sidebar to analyze."
|
| 358 |
+
# Return tuple - Ensure order and count matches outputs_list below (should be 32 items)
|
| 359 |
+
return ( status_msg, df, fig_pie, overview_stats_md, fig_color, fig_rating, fig_elo_diff, fig_games_yr, fig_wr_yr, "(Results by color shown in Overview)", fig_games_dow, fig_wr_dow, fig_games_hod, fig_wr_hod, fig_games_dom, fig_wr_dom, fig_perf_tc, fig_open_freq_api, fig_open_wr_api, fig_open_freq_cust, fig_open_wr_cust, fig_opp_freq, df_opp_list, fig_opp_elo, titled_status_msg, fig_titled_pie, fig_titled_color, fig_titled_rating, df_titled_h2h, fig_tf_summary, fig_tf_tc, df_tf_list, fig_term_all )
|
| 360 |
+
except Exception as e: error_msg = f"🚨 Error generating results: {e}\n{traceback.format_exc()}"; num_outputs = 32; return error_msg, pd.DataFrame(), *( [None] * (num_outputs - 2) )
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 361 |
|
| 362 |
# =============================================
|
| 363 |
# Gradio Interface Definition (Corrected UI Syntax)
|
|
|
|
| 369 |
|
| 370 |
with gr.Row():
|
| 371 |
with gr.Column(scale=1, min_width=250): # Sidebar
|
| 372 |
+
gr.Markdown("## ⚙️ Settings")
|
| 373 |
+
username_input=gr.Textbox(label="Lichess Username", placeholder="e.g., DrNykterstein", elem_id="username_box")
|
| 374 |
+
time_period_input=gr.Dropdown(label="Time Period", choices=list(TIME_PERIOD_OPTIONS.keys()), value=DEFAULT_TIME_PERIOD)
|
| 375 |
+
perf_type_input=gr.Dropdown(label="Game Type", choices=PERF_TYPE_OPTIONS_SINGLE, value=DEFAULT_PERF_TYPE)
|
| 376 |
+
analyze_btn=gr.Button("Analyze Games", variant="primary")
|
| 377 |
+
status_output=gr.Markdown("")
|
| 378 |
+
gr.Markdown("---")
|
| 379 |
+
gr.Markdown("### Analyze vs Titled Players")
|
| 380 |
+
titled_player_select=gr.CheckboxGroup(label="Select Opponent Titles", choices=TITLES_TO_ANALYZE, value=['GM', 'IM'], elem_id="titled_select")
|
| 381 |
+
gr.Markdown("*(Analysis updates on 'Analyze Games' click)*")
|
| 382 |
+
|
| 383 |
with gr.Column(scale=4): # Main Content
|
| 384 |
# Define Output Components - Order Matters! Match return tuple exactly.
|
| 385 |
overview_plot_pie=gr.Plot(label="Overall Results"); overview_stats_md_out=gr.Markdown(); overview_plot_color=gr.Plot(label="Results by Color"); overview_plot_rating=gr.Plot(label="Rating Trend"); overview_plot_elo_diff=gr.Plot(label="Elo Advantage vs. Result")
|
| 386 |
time_plot_games_yr=gr.Plot(label="Games per Year"); time_plot_wr_yr=gr.Plot(label="Win Rate per Year")
|
| 387 |
+
color_plot_placeholder=gr.Markdown() # Component for Tab 3 placeholder
|
| 388 |
time_plot_games_dow=gr.Plot(label="Games by Day of Week"); time_plot_wr_dow=gr.Plot(label="Win Rate by Day of Week"); time_plot_games_hod=gr.Plot(label="Games by Hour (UTC)"); time_plot_wr_hod=gr.Plot(label="Win Rate by Hour (UTC)"); time_plot_games_dom=gr.Plot(label="Games by Day of Month"); time_plot_wr_dom=gr.Plot(label="Win Rate by Day of Month"); time_plot_perf_tc=gr.Plot(label="Performance by Time Control")
|
| 389 |
eco_plot_freq_api=gr.Plot(label="Opening Frequency (API)"); eco_plot_wr_api=gr.Plot(label="Opening Win Rate (API)"); eco_plot_freq_cust=gr.Plot(label="Opening Frequency (Custom)"); eco_plot_wr_cust=gr.Plot(label="Opening Win Rate (Custom)")
|
| 390 |
opp_plot_freq=gr.Plot(label="Frequent Opponents"); opp_df_list=gr.DataFrame(label="Top Opponents List", wrap=True); opp_plot_elo=gr.Plot(label="Elo Advantage vs Result")
|
|
|
|
| 396 |
with gr.TabItem("1. Overview", id=0):
|
| 397 |
overview_stats_md_out # Display metrics
|
| 398 |
with gr.Row():
|
| 399 |
+
overview_plot_pie
|
| 400 |
+
overview_plot_color
|
| 401 |
overview_plot_rating
|
| 402 |
overview_plot_elo_diff
|
| 403 |
|
| 404 |
with gr.TabItem("2. Perf. Over Time", id=1):
|
| 405 |
+
overview_plot_rating # Reuse rating trend plot
|
| 406 |
+
time_plot_games_yr
|
| 407 |
+
time_plot_wr_yr
|
|
|
|
| 408 |
|
| 409 |
with gr.TabItem("3. Perf. by Color", id=2):
|
| 410 |
+
overview_plot_color # Reuse color plot
|
| 411 |
+
color_plot_placeholder # Display placeholder text
|
| 412 |
|
| 413 |
with gr.TabItem("4. Time & Date", id=3):
|
| 414 |
+
gr.Markdown("### Day of Week")
|
| 415 |
+
with gr.Row():
|
| 416 |
+
time_plot_games_dow
|
| 417 |
+
time_plot_wr_dow
|
| 418 |
+
gr.Markdown("### Hour of Day (UTC)")
|
| 419 |
+
with gr.Row():
|
| 420 |
+
time_plot_games_hod
|
| 421 |
+
time_plot_wr_hod
|
| 422 |
+
gr.Markdown("### Day of Month")
|
| 423 |
+
with gr.Row():
|
| 424 |
+
time_plot_games_dom
|
| 425 |
+
time_plot_wr_dom
|
| 426 |
+
gr.Markdown("### Time Control Category")
|
| 427 |
+
time_plot_perf_tc
|
| 428 |
|
| 429 |
with gr.TabItem("5. ECO & Openings", id=4):
|
| 430 |
+
gr.Markdown("#### Based on Lichess API Opening Names")
|
| 431 |
+
eco_plot_freq_api
|
| 432 |
+
eco_plot_wr_api
|
| 433 |
+
gr.Markdown("---")
|
| 434 |
+
gr.Markdown("#### Based on Custom ECO Map")
|
| 435 |
+
# Conditional rendering in layout is tricky, render both but one might be empty
|
| 436 |
+
eco_plot_freq_cust
|
| 437 |
+
eco_plot_wr_cust
|
| 438 |
+
if not ECO_MAPPING: # Add a note if map not loaded
|
| 439 |
+
gr.Markdown("*(Custom map file not loaded)*")
|
| 440 |
+
|
| 441 |
|
| 442 |
with gr.TabItem("6. Opponents", id=5):
|
| 443 |
+
opp_plot_freq
|
| 444 |
+
opp_df_list
|
| 445 |
+
opp_plot_elo
|
| 446 |
|
| 447 |
with gr.TabItem("7. vs Titled", id=6):
|
| 448 |
+
gr.Markdown("Analysis based on titles selected in the sidebar.")
|
| 449 |
+
titled_status # Show status message
|
| 450 |
+
with gr.Row():
|
| 451 |
+
titled_plot_pie
|
| 452 |
+
titled_plot_color
|
| 453 |
+
titled_plot_rating
|
| 454 |
+
titled_df_h2h_comp # Show H2H table using the component
|
| 455 |
|
| 456 |
with gr.TabItem("8. Termination", id=7):
|
| 457 |
+
gr.Markdown("### Time Forfeit")
|
| 458 |
+
term_plot_tf_summary
|
| 459 |
+
term_plot_tf_tc
|
| 460 |
+
with gr.Accordion("View Recent TF Games", open=False):
|
| 461 |
+
term_df_tf_list
|
| 462 |
+
gr.Markdown("### Overall Termination")
|
| 463 |
+
term_plot_all
|
| 464 |
|
| 465 |
# Define the list of output components in the exact order
|
| 466 |
outputs_list = [
|
| 467 |
+
status_output, df_state, # Status and State
|
| 468 |
overview_plot_pie, overview_stats_md_out, overview_plot_color, overview_plot_rating, overview_plot_elo_diff, # Tab 1
|
| 469 |
time_plot_games_yr, time_plot_wr_yr, # Tab 2
|
| 470 |
color_plot_placeholder, # Tab 3
|
| 471 |
time_plot_games_dow, time_plot_wr_dow, time_plot_games_hod, time_plot_wr_hod, time_plot_games_dom, time_plot_wr_dom, time_plot_perf_tc, # Tab 4
|
| 472 |
eco_plot_freq_api, eco_plot_wr_api, eco_plot_freq_cust, eco_plot_wr_cust, # Tab 5
|
| 473 |
opp_plot_freq, opp_df_list, opp_plot_elo, # Tab 6
|
| 474 |
+
titled_status, titled_plot_pie, titled_plot_color, titled_plot_rating, titled_df_h2h_comp, # Tab 7
|
| 475 |
term_plot_tf_summary, term_plot_tf_tc, term_df_tf_list, term_plot_all # Tab 8
|
| 476 |
]
|
| 477 |
+
# Calculate length dynamically
|
| 478 |
expected_outputs_count = len(outputs_list)
|
|
|
|
| 479 |
|
| 480 |
+
# Modify the wrapper to handle potential None returns correctly
|
| 481 |
def perform_full_analysis_wrapper(username, time_period_key, perf_type, selected_titles_list, progress=gr.Progress(track_tqdm=True)):
|
| 482 |
results = perform_full_analysis(username, time_period_key, perf_type, selected_titles_list, progress)
|
| 483 |
+
# Pad with Nones if the function returned fewer items (e.g., due to error)
|
| 484 |
+
if len(results) < expected_outputs_count:
|
| 485 |
+
results = tuple(list(results) + [None] * (expected_outputs_count - len(results)))
|
| 486 |
+
elif len(results) > expected_outputs_count: # Should not happen, but safeguard
|
| 487 |
+
results = results[:expected_outputs_count]
|
|
|
|
|
|
|
|
|
|
| 488 |
return results
|
| 489 |
|
| 490 |
# Connect button click to the wrapper function
|
| 491 |
analyze_btn.click(
|
| 492 |
+
fn=perform_full_analysis_wrapper,
|
| 493 |
inputs=[username_input, time_period_input, perf_type_input, titled_player_select],
|
| 494 |
outputs=outputs_list
|
| 495 |
)
|
| 496 |
|
| 497 |
# --- Launch the Gradio App ---
|
| 498 |
if __name__ == "__main__":
|
| 499 |
+
demo.launch(debug=True)
|