Kaveh commited on
Commit
8abee02
·
unverified ·
1 Parent(s): 906df6b

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +145 -123
app.py CHANGED
@@ -1,7 +1,7 @@
1
  # -*- coding: utf-8 -*-
2
  # =============================================
3
  # Gradio App for Chess Game Analysis - Lichess API Version
4
- # v18: Manual progress updates instead of track_tqdm to fix IndexError.
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 (Correct)
32
  # =============================================
33
- # ... (Function identical to v17 - Assumed correct now) ...
34
  def categorize_time_control(tc_str, speed_info):
35
- if isinstance(speed_info, str) and speed_info in ['bullet', 'blitz', 'rapid', 'classical', 'correspondence']: return speed_info.capitalize()
36
- if not isinstance(tc_str, str) or tc_str in ['-', '?', 'Unknown','Correspondence']: return 'Unknown' if tc_str!='Correspondence' else 'Correspondence'
 
 
 
 
 
 
 
 
 
 
37
  if '+' in tc_str:
38
- try: parts=tc_str.split('+');
39
- if len(parts)==2: base=int(parts[0]); increment=int(parts[1]); total=base+40*increment
40
- else: return 'Unknown'
41
- except(ValueError,IndexError): return 'Unknown'
42
- if total>=1500: return 'Classical';
43
- if total>=480: return 'Rapid';
44
- if total>=180: return 'Blitz';
45
- if total>0 : return 'Bullet';
 
 
 
 
 
 
 
 
 
 
 
 
 
 
46
  return 'Unknown'
 
 
47
  else:
48
- try: base=int(tc_str)
49
- if base>=1500: return 'Classical';
50
- if base>=480: return 'Rapid';
51
- if base>=180: return 'Blitz';
52
- if base>0 : return 'Bullet';
53
- return 'Unknown'
54
- except ValueError: tc_lower=tc_str.lower();
55
- if 'classical' in tc_lower: return 'Classical';
56
- if 'rapid' in tc_lower: return 'Rapid';
57
- if 'blitz' in tc_lower: return 'Blitz';
58
- if 'bullet' in tc_lower: return 'Bullet';
59
- return 'Unknown'
 
 
 
 
 
 
 
 
 
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 (Manual Progress Update)
76
  # =============================================
77
- # Removed @gr.Progress decorator here, will pass progress object manually
78
- def load_from_lichess_api(username: str, time_period_key: str, perf_type: str, rated: bool, eco_map: dict, progress=None): # Changed progress to optional default None
79
- """ Fetches and processes Lichess games with MANUAL progress updates. """
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...") # Check if progress object exists
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
- # --- Manual Progress Update ---
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 (Correct)
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)): # Added track_tqdm back
300
  df, status_msg = load_from_lichess_api(username, time_period_key, perf_type, DEFAULT_RATED_ONLY, ECO_MAPPING, progress)
301
- num_outputs = 30 # Recalculate based on the exact number of output components below
302
  if not isinstance(df, pd.DataFrame) or df.empty:
303
- return status_msg, pd.DataFrame(), *( [None] * (num_outputs - 2) ) # Return Nones for plot/df components
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 all results... MUST match outputs_list order
336
- # Recalculate num_outputs based on this exact return statement
337
- return_tuple = ( 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 )
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"); username_input=gr.Textbox(label="Lichess Username", placeholder="e.g., DrNykterstein", elem_id="username_box"); time_period_input=gr.Dropdown(label="Time Period", choices=list(TIME_PERIOD_OPTIONS.keys()), value=DEFAULT_TIME_PERIOD); perf_type_input=gr.Dropdown(label="Game Type", choices=PERF_TYPE_OPTIONS_SINGLE, value=DEFAULT_PERF_TYPE); analyze_btn=gr.Button("Analyze Games", variant="primary"); status_output=gr.Markdown(""); gr.Markdown("---"); gr.Markdown("### Analyze vs Titled Players"); titled_player_select=gr.CheckboxGroup(label="Select Opponent Titles", choices=TITLES_TO_ANALYZE, value=['GM', 'IM'], elem_id="titled_select"); gr.Markdown("*(Analysis updates on 'Analyze Games' click)*");
 
 
 
 
 
 
 
 
 
 
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
- overview_plot_pie
374
- overview_plot_color
375
  overview_plot_rating
376
  overview_plot_elo_diff
377
 
378
  with gr.TabItem("2. Perf. Over Time", id=1):
379
- # Note: Rating Trend is defined in Overview, reference it here if needed
380
- # For clarity, let's just place the year-based plots
381
- time_plot_games_yr
382
- time_plot_wr_yr
383
 
384
  with gr.TabItem("3. Perf. by Color", id=2):
385
- overview_plot_color # Reuse color plot
386
- color_plot_placeholder # Display placeholder text
387
 
388
  with gr.TabItem("4. Time & Date", id=3):
389
- gr.Markdown("### Day of Week")
390
- with gr.Row():
391
- time_plot_games_dow
392
- time_plot_wr_dow
393
- gr.Markdown("### Hour of Day (UTC)")
394
- with gr.Row():
395
- time_plot_games_hod
396
- time_plot_wr_hod
397
- gr.Markdown("### Day of Month")
398
- with gr.Row():
399
- time_plot_games_dom
400
- time_plot_wr_dom
401
- gr.Markdown("### Time Control Category")
402
- time_plot_perf_tc
403
 
404
  with gr.TabItem("5. ECO & Openings", id=4):
405
- gr.Markdown("#### Based on Lichess API Opening Names")
406
- eco_plot_freq_api
407
- eco_plot_wr_api
408
- gr.Markdown("---")
409
- gr.Markdown("#### Based on Custom ECO Map")
410
- if not ECO_MAPPING:
411
- gr.Markdown("⚠️ Custom ECO map file not loaded.")
412
- else:
413
- eco_plot_freq_cust
414
- eco_plot_wr_cust
 
415
 
416
  with gr.TabItem("6. Opponents", id=5):
417
- opp_plot_freq
418
- opp_df_list
419
- opp_plot_elo
420
 
421
  with gr.TabItem("7. vs Titled", id=6):
422
- gr.Markdown("Analysis based on titles selected in the sidebar.")
423
- titled_status # Show status message
424
- with gr.Row():
425
- titled_plot_pie
426
- titled_plot_color
427
- titled_plot_rating
428
- titled_df_h2h_comp # Show H2H table using the component
429
 
430
  with gr.TabItem("8. Termination", id=7):
431
- gr.Markdown("### Time Forfeit")
432
- term_plot_tf_summary
433
- term_plot_tf_tc
434
- with gr.Accordion("View Recent TF Games", open=False):
435
- term_df_tf_list
436
- gr.Markdown("### Overall Termination")
437
- term_plot_all
438
 
439
  # Define the list of output components in the exact order
440
  outputs_list = [
441
- status_output, df_state, # Status and State first
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 (Correct component name)
449
  term_plot_tf_summary, term_plot_tf_tc, term_df_tf_list, term_plot_all # Tab 8
450
  ]
451
- # Calculate length dynamically to avoid manual count errors
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 perform_full_analysis return tuple logic for errors
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
- # Ensure the correct number of outputs is always returned, even on error
459
- if len(results) != expected_outputs_count:
460
- print(f"WARN: Mismatch in expected ({expected_outputs_count}) vs actual ({len(results)}) outputs!")
461
- # Pad with Nones if too few, or truncate if too many (though padding is safer)
462
- if len(results) < expected_outputs_count:
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, # Use the 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) # Keep debug True for local testing
 
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)