Kaveh commited on
Commit
58423aa
·
unverified ·
1 Parent(s): 081e53e

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +77 -134
app.py CHANGED
@@ -1,7 +1,7 @@
1
  # -*- coding: utf-8 -*-
2
  # =============================================
3
  # Gradio App for Chess Game Analysis - Lichess API Version
4
- # v18: Fixed NameError for titled_df_h2h component in outputs_list.
5
  # =============================================
6
 
7
  import gradio as gr
@@ -28,37 +28,68 @@ 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
  def categorize_time_control(tc_str, speed_info):
34
- if isinstance(speed_info, str) and speed_info in ['bullet', 'blitz', 'rapid', 'classical', 'correspondence']: return speed_info.capitalize()
35
- if not isinstance(tc_str, str) or tc_str in ['-', '?', 'Unknown','Correspondence']: return 'Unknown' if tc_str!='Correspondence' else 'Correspondence'
36
- if '+' in tc_str:
37
- try: parts=tc_str.split('+');
38
- if len(parts)==2: base=int(parts[0]); increment=int(parts[1]); total=base+40*increment
39
- else: return 'Unknown'
40
- except(ValueError,IndexError): return 'Unknown'
41
- if total>=1500: return 'Classical';
42
- if total>=480: return 'Rapid';
43
- if total>=180: return 'Blitz';
44
- if total>0 : return 'Bullet';
45
  return 'Unknown'
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
46
  else:
47
- try: base=int(tc_str)
48
- if base>=1500: return 'Classical';
49
- if base>=480: return 'Rapid';
50
- if base>=180: return 'Blitz';
51
- if base>0 : return 'Bullet';
52
- return 'Unknown'
53
- except ValueError: tc_lower=tc_str.lower();
54
- if 'classical' in tc_lower: return 'Classical';
55
- if 'rapid' in tc_lower: return 'Rapid';
56
- if 'blitz' in tc_lower: return 'Blitz';
57
- if 'bullet' in tc_lower: return 'Bullet';
58
- return 'Unknown'
 
 
 
 
 
 
 
59
 
60
  # =============================================
61
- # Helper Function: Load ECO Mapping
62
  # =============================================
63
  ECO_MAPPING = {}
64
  try:
@@ -71,11 +102,11 @@ except FileNotFoundError: print(f"WARN: ECO file '{ECO_CSV_PATH}' not found.")
71
  except Exception as e: print(f"WARN: Error loading ECO file: {e}")
72
 
73
  # =============================================
74
- # API Data Loading and Processing Function (Correct)
75
  # =============================================
76
  @gr.Progress(track_tqdm=True)
77
  def load_from_lichess_api(username: str, time_period_key: str, perf_type: str, rated: bool, eco_map: dict, progress=None):
78
- # ... (Function identical to v15) ...
79
  if not username: return pd.DataFrame(), "⚠️ Enter username."
80
  if not perf_type: return pd.DataFrame(), "⚠️ Select game type."
81
  if progress: progress(0, desc="Initializing...");
@@ -147,7 +178,7 @@ def load_from_lichess_api(username: str, time_period_key: str, perf_type: str, r
147
  # =============================================
148
  # Plotting Functions (Unchanged)
149
  # =============================================
150
- # (Insert ALL plotting functions here - code identical to previous version v14)
151
  # ... (plot_win_loss_pie, ..., plot_time_forfeit_by_tc) ...
152
  def plot_win_loss_pie(df, display_name):
153
  if 'PlayerResultString' not in df.columns: return go.Figure()
@@ -270,6 +301,7 @@ def plot_time_forfeit_by_tc(tf_games_df):
270
  # =============================================
271
  # Helper Functions
272
  # =============================================
 
273
  def filter_and_analyze_titled(df, titles):
274
  if 'OpponentTitle' not in df.columns: return pd.DataFrame()
275
  titled_games = df[df['OpponentTitle'].isin(titles)].copy(); return titled_games
@@ -289,13 +321,11 @@ def perform_full_analysis(username, time_period_key, perf_type, selected_titles_
289
  df, status_msg = load_from_lichess_api(username, time_period_key, perf_type, DEFAULT_RATED_ONLY, ECO_MAPPING, progress)
290
  num_outputs = 30 # Define the total number of expected output components
291
  if not isinstance(df, pd.DataFrame) or df.empty:
292
- # Return status, empty df, and None for all other components
293
- return status_msg, pd.DataFrame(), *( [None] * (num_outputs - 2) )
294
  try:
295
- # Generate all base plots and data...
296
  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)
297
  total_g=len(df); w=len(df[df['PlayerResultNumeric']==1]); l=len(df[df['PlayerResultNumeric']==0]); d=len(df[df['PlayerResultNumeric']==0.5])
298
- 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}"
299
  fig_games_yr=plot_games_per_year(df); fig_wr_yr=plot_win_rate_per_year(df); fig_perf_tc=plot_performance_by_time_control(df)
300
  fig_games_dow=plot_games_by_dow(df); fig_wr_dow=plot_winrate_by_dow(df); fig_games_hod=plot_games_by_hour(df); fig_wr_hod=plot_winrate_by_hour(df)
301
  fig_games_dom=plot_games_by_dom(df); fig_wr_dom=plot_winrate_by_dom(df)
@@ -307,8 +337,6 @@ def perform_full_analysis(username, time_period_key, perf_type, selected_titles_
307
  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()
308
  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)
309
  fig_term_all.update_layout(dragmode=False); fig_term_all.update_traces(textposition='outside')
310
-
311
- # Generate Titled Player analysis...
312
  titled_status_msg = ""; fig_titled_pie, fig_titled_color, fig_titled_rating, df_titled_h2h = go.Figure(), go.Figure(), go.Figure(), pd.DataFrame()
313
  if selected_titles_list:
314
  titled_games = filter_and_analyze_titled(df, selected_titles_list)
@@ -323,126 +351,41 @@ def perform_full_analysis(username, time_period_key, perf_type, selected_titles_
323
  df_titled_h2h = h2h.sort_values('Total', ascending=False).reset_index()
324
  else: titled_status_msg = f"ℹ️ No games found vs selected titles ({', '.join(selected_titles_list)})."
325
  else: titled_status_msg = "ℹ️ Select titles from the sidebar to analyze."
326
-
327
- # Return all results... MUST match outputs_list order
328
  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 )
329
  except Exception as e: error_msg = f"🚨 Error generating results: {e}\n{traceback.format_exc()}"; return error_msg, pd.DataFrame(), *( [None] * num_outputs )
330
 
331
-
332
  # =============================================
333
- # Gradio Interface Definition (Corrected UI Syntax)
334
  # =============================================
 
335
  css = """.gradio-container { font-family: 'IBM Plex Sans', sans-serif; } footer { display: none !important; }"""
336
  with gr.Blocks(theme=gr.themes.Soft(), css=css) as demo:
337
  gr.Markdown("# ♟️ Lichess Insights\nAnalyze rated game statistics from Lichess API.")
338
  df_state = gr.State(pd.DataFrame())
339
-
340
  with gr.Row():
341
  with gr.Column(scale=1, min_width=250): # Sidebar
342
  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)*");
343
  with gr.Column(scale=4): # Main Content
344
- # Define Output Components - Order Matters!
345
  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")
346
  time_plot_games_yr=gr.Plot(label="Games per Year"); time_plot_wr_yr=gr.Plot(label="Win Rate per Year")
347
  color_plot_placeholder=gr.Markdown()
348
  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")
349
  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)")
350
  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")
351
- titled_status=gr.Markdown(); titled_plot_pie=gr.Plot(label="Results vs Selected Titles"); titled_plot_color=gr.Plot(label="Results by Color vs Selected Titles"); titled_plot_rating=gr.Plot(label="Rating Trend vs Selected Titles"); titled_df_h2h_comp=gr.DataFrame(label="Head-to-Head vs Selected Titles", wrap=True); # <<< RENAMED COMPONENT
352
  term_plot_tf_summary=gr.Plot(label="Time Forfeit Summary"); term_plot_tf_tc=gr.Plot(label="Time Forfeits by Time Control"); term_df_tf_list=gr.DataFrame(label="Recent TF Games", wrap=True); term_plot_all=gr.Plot(label="Overall Termination")
353
-
354
- # Arrange Components in Tabs - Using correct block structure
355
  with gr.Tabs() as tabs:
356
- with gr.TabItem("1. Overview", id=0):
357
- overview_stats_md_out # Display metrics
358
- with gr.Row():
359
- overview_plot_pie
360
- overview_plot_color
361
- overview_plot_rating
362
- overview_plot_elo_diff
363
-
364
- with gr.TabItem("2. Perf. Over Time", id=1):
365
- overview_plot_rating # Reuse rating trend plot
366
- time_plot_games_yr
367
- time_plot_wr_yr
368
-
369
- with gr.TabItem("3. Perf. by Color", id=2):
370
- overview_plot_color # Reuse color plot
371
- color_plot_placeholder # Display placeholder text
372
-
373
- with gr.TabItem("4. Time & Date", id=3):
374
- gr.Markdown("### Day of Week")
375
- with gr.Row():
376
- time_plot_games_dow
377
- time_plot_wr_dow
378
- gr.Markdown("### Hour of Day (UTC)")
379
- with gr.Row():
380
- time_plot_games_hod
381
- time_plot_wr_hod
382
- gr.Markdown("### Day of Month")
383
- with gr.Row():
384
- time_plot_games_dom
385
- time_plot_wr_dom
386
- gr.Markdown("### Time Control Category")
387
- time_plot_perf_tc
388
-
389
- with gr.TabItem("5. ECO & Openings", id=4):
390
- gr.Markdown("#### Based on Lichess API Opening Names")
391
- # Add sliders using gr.Slider if desired, link their change event
392
- eco_plot_freq_api
393
- eco_plot_wr_api
394
- gr.Markdown("---")
395
- gr.Markdown("#### Based on Custom ECO Map")
396
- if not ECO_MAPPING:
397
- gr.Markdown("⚠️ Custom ECO map file not loaded.")
398
- else:
399
- eco_plot_freq_cust
400
- eco_plot_wr_cust
401
-
402
- with gr.TabItem("6. Opponents", id=5):
403
- # Add slider using gr.Slider if desired
404
- opp_plot_freq
405
- opp_df_list
406
- opp_plot_elo
407
-
408
- with gr.TabItem("7. vs Titled", id=6):
409
- gr.Markdown("Analysis based on titles selected in the sidebar.")
410
- titled_status # Show status message
411
- with gr.Row():
412
- titled_plot_pie
413
- titled_plot_color
414
- titled_plot_rating
415
- titled_df_h2h_comp # Show H2H table using the component
416
-
417
- with gr.TabItem("8. Termination", id=7):
418
- gr.Markdown("### Time Forfeit")
419
- term_plot_tf_summary
420
- term_plot_tf_tc
421
- with gr.Accordion("View Recent TF Games", open=False):
422
- term_df_tf_list
423
- gr.Markdown("### Overall Termination")
424
- term_plot_all
425
-
426
- # Define the list of output components in the exact order
427
- # Use the CORRECT variable name for the H2H DataFrame component
428
- outputs_list = [
429
- status_output, df_state, # Status and State
430
- overview_plot_pie, overview_stats_md_out, overview_plot_color, overview_plot_rating, overview_plot_elo_diff, # Tab 1
431
- time_plot_games_yr, time_plot_wr_yr, # Tab 2
432
- color_plot_placeholder, # Tab 3
433
- 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
434
- eco_plot_freq_api, eco_plot_wr_api, eco_plot_freq_cust, eco_plot_wr_cust, # Tab 5
435
- opp_plot_freq, opp_df_list, opp_plot_elo, # Tab 6
436
- titled_status, titled_plot_pie, titled_plot_color, titled_plot_rating, titled_df_h2h_comp, # Tab 7 <<< CORRECTED
437
- term_plot_tf_summary, term_plot_tf_tc, term_df_tf_list, term_plot_all # Tab 8
438
- ]
439
-
440
- # Connect button click to the main analysis function
441
- analyze_btn.click(
442
- fn=perform_full_analysis,
443
- inputs=[username_input, time_period_input, perf_type_input, titled_player_select],
444
- outputs=outputs_list
445
- )
446
 
447
  # --- Launch the Gradio App ---
448
  if __name__ == "__main__":
 
1
  # -*- coding: utf-8 -*-
2
  # =============================================
3
  # Gradio App for Chess Game Analysis - Lichess API Version
4
+ # v17: OBSESSIVELY REWRITTEN categorize_time_control for syntax.
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 *** OBSESSIVELY REWRITTEN ***
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
+ parts = tc_str.split('+')
48
+ if len(parts) == 2:
49
+ base_str, increment_str = parts[0], parts[1]
50
+ base, increment = None, None # Initialize
51
+ # *** Isolate ONLY the conversion in try-except ***
52
+ try:
53
+ base = int(base_str)
54
+ increment = int(increment_str)
55
+ except ValueError:
56
+ return 'Unknown' # Conversion failed
57
+
58
+ # *** Classification happens AFTER successful conversion ***
59
+ total = base + 40 * increment
60
+ if total >= 1500: return 'Classical'
61
+ if total >= 480: return 'Rapid'
62
+ if total >= 180: return 'Blitz'
63
+ if total > 0 : return 'Bullet'
64
+ return 'Unknown'
65
+ else:
66
+ # '+' was present but not exactly two parts
67
+ return 'Unknown'
68
+
69
+ # 4. Handle format like "300" (only base time)
70
  else:
71
+ base = None # Initialize
72
+ # *** Isolate ONLY the conversion in try-except ***
73
+ try:
74
+ base = int(tc_str)
75
+ except ValueError:
76
+ # *** Fallback to keywords ONLY if conversion fails ***
77
+ tc_lower = tc_str.lower()
78
+ if 'classical' in tc_lower: return 'Classical'
79
+ if 'rapid' in tc_lower: return 'Rapid'
80
+ if 'blitz' in tc_lower: return 'Blitz'
81
+ if 'bullet' in tc_lower: return 'Bullet'
82
+ return 'Unknown' # Failed conversion and keyword match
83
+
84
+ # *** Classification happens AFTER successful conversion ***
85
+ if base >= 1500: return 'Classical'
86
+ if base >= 480: return 'Rapid'
87
+ if base >= 180: return 'Blitz'
88
+ if base > 0 : return 'Bullet'
89
+ return 'Unknown' # Base time is 0 or negative
90
 
91
  # =============================================
92
+ # Helper Function: Load ECO Mapping (Unchanged)
93
  # =============================================
94
  ECO_MAPPING = {}
95
  try:
 
102
  except Exception as e: print(f"WARN: Error loading ECO file: {e}")
103
 
104
  # =============================================
105
+ # API Data Loading and Processing Function (Unchanged)
106
  # =============================================
107
  @gr.Progress(track_tqdm=True)
108
  def load_from_lichess_api(username: str, time_period_key: str, perf_type: str, rated: bool, eco_map: dict, progress=None):
109
+ # ... (Code identical to version 15 - calls the fixed helper now) ...
110
  if not username: return pd.DataFrame(), "⚠️ Enter username."
111
  if not perf_type: return pd.DataFrame(), "⚠️ Select game type."
112
  if progress: progress(0, desc="Initializing...");
 
178
  # =============================================
179
  # Plotting Functions (Unchanged)
180
  # =============================================
181
+ # (Insert ALL plotting functions here - code identical to previous version v15)
182
  # ... (plot_win_loss_pie, ..., plot_time_forfeit_by_tc) ...
183
  def plot_win_loss_pie(df, display_name):
184
  if 'PlayerResultString' not in df.columns: return go.Figure()
 
301
  # =============================================
302
  # Helper Functions
303
  # =============================================
304
+ # ... (Functions identical to v15) ...
305
  def filter_and_analyze_titled(df, titles):
306
  if 'OpponentTitle' not in df.columns: return pd.DataFrame()
307
  titled_games = df[df['OpponentTitle'].isin(titles)].copy(); return titled_games
 
321
  df, status_msg = load_from_lichess_api(username, time_period_key, perf_type, DEFAULT_RATED_ONLY, ECO_MAPPING, progress)
322
  num_outputs = 30 # Define the total number of expected output components
323
  if not isinstance(df, pd.DataFrame) or df.empty:
324
+ return status_msg, pd.DataFrame(), *( [None] * (num_outputs - 2) ) # Return None for plot/df components
 
325
  try:
 
326
  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)
327
  total_g=len(df); w=len(df[df['PlayerResultNumeric']==1]); l=len(df[df['PlayerResultNumeric']==0]); d=len(df[df['PlayerResultNumeric']==0.5])
328
+ 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'}"
329
  fig_games_yr=plot_games_per_year(df); fig_wr_yr=plot_win_rate_per_year(df); fig_perf_tc=plot_performance_by_time_control(df)
330
  fig_games_dow=plot_games_by_dow(df); fig_wr_dow=plot_winrate_by_dow(df); fig_games_hod=plot_games_by_hour(df); fig_wr_hod=plot_winrate_by_hour(df)
331
  fig_games_dom=plot_games_by_dom(df); fig_wr_dom=plot_winrate_by_dom(df)
 
337
  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()
338
  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)
339
  fig_term_all.update_layout(dragmode=False); fig_term_all.update_traces(textposition='outside')
 
 
340
  titled_status_msg = ""; fig_titled_pie, fig_titled_color, fig_titled_rating, df_titled_h2h = go.Figure(), go.Figure(), go.Figure(), pd.DataFrame()
341
  if selected_titles_list:
342
  titled_games = filter_and_analyze_titled(df, selected_titles_list)
 
351
  df_titled_h2h = h2h.sort_values('Total', ascending=False).reset_index()
352
  else: titled_status_msg = f"ℹ️ No games found vs selected titles ({', '.join(selected_titles_list)})."
353
  else: titled_status_msg = "ℹ️ Select titles from the sidebar to analyze."
 
 
354
  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 )
355
  except Exception as e: error_msg = f"🚨 Error generating results: {e}\n{traceback.format_exc()}"; return error_msg, pd.DataFrame(), *( [None] * num_outputs )
356
 
 
357
  # =============================================
358
+ # Gradio Interface Definition (Unchanged UI Structure)
359
  # =============================================
360
+ # ... (Code identical to version 15) ...
361
  css = """.gradio-container { font-family: 'IBM Plex Sans', sans-serif; } footer { display: none !important; }"""
362
  with gr.Blocks(theme=gr.themes.Soft(), css=css) as demo:
363
  gr.Markdown("# ♟️ Lichess Insights\nAnalyze rated game statistics from Lichess API.")
364
  df_state = gr.State(pd.DataFrame())
 
365
  with gr.Row():
366
  with gr.Column(scale=1, min_width=250): # Sidebar
367
  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)*");
368
  with gr.Column(scale=4): # Main Content
 
369
  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")
370
  time_plot_games_yr=gr.Plot(label="Games per Year"); time_plot_wr_yr=gr.Plot(label="Win Rate per Year")
371
  color_plot_placeholder=gr.Markdown()
372
  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")
373
  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)")
374
  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")
375
+ titled_status=gr.Markdown(); titled_plot_pie=gr.Plot(label="Results vs Selected Titles"); titled_plot_color=gr.Plot(label="Results by Color vs Selected Titles"); titled_plot_rating=gr.Plot(label="Rating Trend vs Selected Titles"); titled_df_h2h_comp=gr.DataFrame(label="Head-to-Head vs Selected Titles", wrap=True); # Component name
376
  term_plot_tf_summary=gr.Plot(label="Time Forfeit Summary"); term_plot_tf_tc=gr.Plot(label="Time Forfeits by Time Control"); term_df_tf_list=gr.DataFrame(label="Recent TF Games", wrap=True); term_plot_all=gr.Plot(label="Overall Termination")
 
 
377
  with gr.Tabs() as tabs:
378
+ with gr.TabItem("1. Overview", id=0): overview_stats_md_out; with gr.Row(): overview_plot_pie; overview_plot_color; overview_plot_rating; overview_plot_elo_diff
379
+ with gr.TabItem("2. Perf. Over Time", id=1): overview_plot_rating; time_plot_games_yr; time_plot_wr_yr
380
+ with gr.TabItem("3. Perf. by Color", id=2): overview_plot_color; color_plot_placeholder
381
+ with gr.TabItem("4. Time & Date", id=3): gr.Markdown("### Day of Week"); with gr.Row(): time_plot_games_dow; time_plot_wr_dow; gr.Markdown("### Hour of Day (UTC)"); with gr.Row(): time_plot_games_hod; time_plot_wr_hod; gr.Markdown("### Day of Month"); with gr.Row(): time_plot_games_dom; time_plot_wr_dom; gr.Markdown("### Time Control Category"); time_plot_perf_tc
382
+ with gr.TabItem("5. ECO & Openings", id=4): gr.Markdown("#### API Names"); eco_plot_freq_api; eco_plot_wr_api; gr.Markdown("---"); gr.Markdown("#### Custom Map"); if not ECO_MAPPING: gr.Markdown("⚠️ Custom map not loaded."); else: eco_plot_freq_cust; eco_plot_wr_cust
383
+ with gr.TabItem("6. Opponents", id=5): opp_plot_freq; opp_df_list; opp_plot_elo
384
+ with gr.TabItem("7. vs Titled", id=6): gr.Markdown("Analysis based on sidebar selection."); titled_status; with gr.Row(): titled_plot_pie; titled_plot_color; titled_plot_rating; titled_df_h2h_comp
385
+ with gr.TabItem("8. Termination", id=7): gr.Markdown("### Time Forfeit"); term_plot_tf_summary; term_plot_tf_tc; with gr.Accordion("View Recent TF Games",open=False): term_df_tf_list; gr.Markdown("### Overall Termination"); term_plot_all
386
+ outputs_list = [ status_output, df_state, overview_plot_pie, overview_stats_md_out, overview_plot_color, overview_plot_rating, overview_plot_elo_diff, time_plot_games_yr, time_plot_wr_yr, color_plot_placeholder, 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, eco_plot_freq_api, eco_plot_wr_api, eco_plot_freq_cust, eco_plot_wr_cust, opp_plot_freq, opp_df_list, opp_plot_elo, titled_status, titled_plot_pie, titled_plot_color, titled_plot_rating, titled_df_h2h_comp, # Correct component name
387
+ term_plot_tf_summary, term_plot_tf_tc, term_df_tf_list, term_plot_all ]
388
+ analyze_btn.click(fn=perform_full_analysis, inputs=[username_input, time_period_input, perf_type_input, titled_player_select], outputs=outputs_list)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
389
 
390
  # --- Launch the Gradio App ---
391
  if __name__ == "__main__":