Kaveh commited on
Commit
e36519b
·
unverified ·
1 Parent(s): e72c4be

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +110 -66
app.py CHANGED
@@ -1,7 +1,7 @@
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
@@ -32,17 +32,12 @@ TITLES_TO_ANALYZE = ['GM', 'IM', 'FM', 'CM', 'WGM', 'WIM', 'WFM', 'WCM', 'NM']
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:
@@ -60,8 +55,6 @@ def categorize_time_control(tc_str, speed_info):
60
  return 'Unknown'
61
  else:
62
  return 'Unknown'
63
-
64
- # 4. Handle format like "300" (only base time)
65
  else:
66
  try:
67
  base = int(tc_str)
@@ -100,12 +93,11 @@ except Exception as e:
100
  def load_from_lichess_api(username: str, time_period_key: str, perf_type: str, rated: bool, eco_map: dict, progress=None):
101
  if not username: return pd.DataFrame(), "⚠️ Enter username."
102
  if not perf_type: return pd.DataFrame(), "⚠️ Select game type."
103
- # Safe handling of progress
104
  if progress is not None:
105
  try:
106
  progress(0, desc="Initializing...")
107
  except Exception:
108
- pass # Ignore progress errors if not properly initialized
109
  username_lower = username.lower()
110
  status_message = f"Fetching {perf_type} games..."
111
  if progress is not None:
@@ -239,7 +231,7 @@ def plot_win_loss_pie(df, display_name):
239
  fig = px.pie(values=result_counts.values, names=result_counts.index, title=f'Overall Results for {display_name}',
240
  color=result_counts.index, color_discrete_map={'Win': '#4CAF50', 'Draw': '#B0BEC5', 'Loss': '#F44336'}, hole=0.3)
241
  fig.update_traces(textposition='inside', textinfo='percent+label', pull=[0.05 if x == 'Win' else 0 for x in result_counts.index])
242
- fig.update_layout(dragmode=False)
243
  return fig
244
 
245
  def plot_win_loss_by_color(df):
@@ -255,7 +247,7 @@ def plot_win_loss_by_color(df):
255
  fig = px.bar(color_results_pct, barmode='stack', title='Results by Color', labels={'value': '%', 'PlayerColor': 'Played As'},
256
  color='PlayerResultString', color_discrete_map={'Win': '#4CAF50', 'Draw': '#B0BEC5', 'Loss': '#F44336'},
257
  text_auto='.1f', category_orders={"PlayerColor": ["White", "Black"]})
258
- fig.update_layout(yaxis_title="Percentage (%)", xaxis_title="Color Played", dragmode=False)
259
  fig.update_traces(textangle=0)
260
  return fig
261
 
@@ -269,7 +261,7 @@ def plot_rating_trend(df, display_name):
269
  fig.add_trace(go.Scatter(x=df_sorted['Date'], y=df_sorted['PlayerElo'], mode='lines+markers', name='Elo',
270
  line=dict(color='#1E88E5', width=2), marker=dict(size=5, opacity=0.7)))
271
  fig.update_layout(title=f'{display_name}\'s Rating Trend', xaxis_title='Date', yaxis_title='Elo Rating',
272
- hovermode="x unified", xaxis_rangeslider_visible=True, dragmode=False)
273
  return fig
274
 
275
  def plot_performance_vs_opponent_elo(df):
@@ -280,7 +272,7 @@ def plot_performance_vs_opponent_elo(df):
280
  color_discrete_map={'Win': '#4CAF50', 'Draw': '#B0BEC5', 'Loss': '#F44336'}, points='outliers')
281
  fig.add_hline(y=0, line_dash="dash", line_color="grey")
282
  fig.update_traces(marker=dict(opacity=0.8))
283
- fig.update_layout(dragmode=False)
284
  return fig
285
 
286
  def plot_games_by_dow(df):
@@ -290,7 +282,7 @@ def plot_games_by_dow(df):
290
  fig = px.bar(games_by_dow, x=games_by_dow.index, y=games_by_dow.values, title="Games by Day of Week",
291
  labels={'x': 'Day', 'y': 'Games'}, text=games_by_dow.values)
292
  fig.update_traces(marker_color='#9C27B0', textposition='outside')
293
- fig.update_layout(dragmode=False)
294
  return fig
295
 
296
  def plot_winrate_by_dow(df):
@@ -303,7 +295,7 @@ def plot_winrate_by_dow(df):
303
  fig = px.bar(win_rate, x=win_rate.index, y=win_rate.values, title="Win Rate (%) by Day",
304
  labels={'x': 'Day', 'y': 'Win Rate (%)'}, text=win_rate.values)
305
  fig.update_traces(marker_color='#FF9800', texttemplate='%{text:.1f}%', textposition='outside')
306
- fig.update_layout(yaxis_range=[0, 100], dragmode=False)
307
  return fig
308
 
309
  def plot_games_by_hour(df):
@@ -312,7 +304,7 @@ def plot_games_by_hour(df):
312
  fig = px.bar(games_by_hour, x=games_by_hour.index, y=games_by_hour.values, title="Games by Hour (UTC)",
313
  labels={'x': 'Hour', 'y': 'Games'}, text=games_by_hour.values)
314
  fig.update_traces(marker_color='#03A9F4', textposition='outside')
315
- fig.update_layout(xaxis=dict(tickmode='linear'), dragmode=False)
316
  return fig
317
 
318
  def plot_winrate_by_hour(df):
@@ -324,7 +316,7 @@ def plot_winrate_by_hour(df):
324
  fig = px.line(win_rate, x=win_rate.index, y=win_rate.values, markers=True, title="Win Rate (%) by Hour (UTC)",
325
  labels={'x': 'Hour', 'y': 'Win Rate (%)'})
326
  fig.update_traces(line_color='#8BC34A')
327
- fig.update_layout(yaxis_range=[0, 100], xaxis=dict(tickmode='linear'), dragmode=False)
328
  return fig
329
 
330
  def plot_games_per_year(df):
@@ -333,7 +325,7 @@ def plot_games_per_year(df):
333
  fig = px.bar(games_per_year, x=games_per_year.index, y=games_per_year.values, title='Games Per Year',
334
  labels={'x': 'Year', 'y': 'Games'}, text=games_per_year.values)
335
  fig.update_traces(marker_color='#2196F3', textposition='outside')
336
- fig.update_layout(xaxis_title="Year", yaxis_title="Number of Games", xaxis={'type': 'category'}, dragmode=False)
337
  return fig
338
 
339
  def plot_win_rate_per_year(df):
@@ -345,7 +337,7 @@ def plot_win_rate_per_year(df):
345
  fig = px.line(win_rate, x=win_rate.index, y=win_rate.values, title='Win Rate (%) Per Year', markers=True,
346
  labels={'x': 'Year', 'y': 'Win Rate (%)'})
347
  fig.update_traces(line_color='#FFC107', line_width=2.5)
348
- fig.update_layout(yaxis_range=[0, 100], dragmode=False)
349
  return fig
350
 
351
  def plot_performance_by_time_control(df):
@@ -363,7 +355,7 @@ def plot_performance_by_time_control(df):
363
  fig = px.bar(tc_results_pct, title='Performance by Time Control', labels={'value': '%', 'TimeControl_Category': 'Category'},
364
  color='PlayerResultString', color_discrete_map={'Win': '#4CAF50', 'Draw': '#B0BEC5', 'Loss': '#F44336'},
365
  barmode='group', text_auto='.1f')
366
- fig.update_layout(xaxis_title="Time Control Category", yaxis_title="Percentage (%)", dragmode=False)
367
  fig.update_traces(textangle=0)
368
  return fig
369
  except Exception:
@@ -375,7 +367,7 @@ def plot_opening_frequency(df, top_n=20, opening_col='OpeningName_API'):
375
  opening_counts = df[df[opening_col] != 'Unknown Opening'][opening_col].value_counts().nlargest(top_n)
376
  fig = px.bar(opening_counts, y=opening_counts.index, x=opening_counts.values, orientation='h',
377
  title=f'Top {top_n} Openings ({source_label})', labels={'y': 'Opening', 'x': 'Games'}, text=opening_counts.values)
378
- fig.update_layout(yaxis={'categoryorder': 'total ascending'}, dragmode=False)
379
  fig.update_traces(marker_color='#673AB7', textposition='outside')
380
  return fig
381
 
@@ -392,7 +384,7 @@ def plot_win_rate_by_opening(df, min_games=5, top_n=20, opening_col='OpeningName
392
  title=f'Top {top_n} Openings by Win Rate (Min {min_games} games, {source_label})',
393
  labels={'win_rate': 'Win Rate (%)', opening_col: 'Opening'}, text='win_rate')
394
  fig.update_traces(texttemplate='%{text:.1f}%', textposition='inside', marker_color='#009688')
395
- fig.update_layout(yaxis={'categoryorder': 'total ascending'}, xaxis_title="Win Rate (%)", dragmode=False)
396
  return fig
397
 
398
  def plot_most_frequent_opponents(df, top_n=20):
@@ -400,7 +392,7 @@ def plot_most_frequent_opponents(df, top_n=20):
400
  opp_counts = df[df['OpponentName'] != 'Unknown']['OpponentName'].value_counts().nlargest(top_n)
401
  fig = px.bar(opp_counts, y=opp_counts.index, x=opp_counts.values, orientation='h',
402
  title=f'Top {top_n} Opponents', labels={'y': 'Opponent', 'x': 'Games'}, text=opp_counts.values)
403
- fig.update_layout(yaxis={'categoryorder': 'total ascending'}, dragmode=False)
404
  fig.update_traces(marker_color='#FF5722', textposition='outside')
405
  return fig
406
 
@@ -410,7 +402,7 @@ def plot_games_by_dom(df):
410
  fig = px.bar(games_by_dom, x=games_by_dom.index, y=games_by_dom.values, title="Games Played per Day of Month",
411
  labels={'x': 'Day of Month', 'y': 'Number of Games'}, text=games_by_dom.values)
412
  fig.update_traces(marker_color='#E91E63', textposition='outside')
413
- fig.update_layout(xaxis=dict(tickmode='linear'), dragmode=False)
414
  return fig
415
 
416
  def plot_winrate_by_dom(df):
@@ -422,7 +414,7 @@ def plot_winrate_by_dom(df):
422
  fig = px.line(win_rate, x=win_rate.index, y=win_rate.values, markers=True, title="Win Rate (%) per Day of Month",
423
  labels={'x': 'Day of Month', 'y': 'Win Rate (%)'})
424
  fig.update_traces(line_color='#FF5722')
425
- fig.update_layout(yaxis_range=[0, 100], xaxis=dict(tickmode='linear'), dragmode=False)
426
  return fig
427
 
428
  def plot_time_forfeit_summary(wins_tf, losses_tf):
@@ -430,7 +422,7 @@ def plot_time_forfeit_summary(wins_tf, losses_tf):
430
  df_tf = pd.DataFrame(data)
431
  fig = px.bar(df_tf, x='Outcome', y='Count', title="Time Forfeit Summary", color='Outcome',
432
  color_discrete_map={'Won on Time': '#4CAF50', 'Lost on Time': '#F44336'}, text='Count')
433
- fig.update_layout(showlegend=False, dragmode=False)
434
  fig.update_traces(textposition='outside')
435
  return fig
436
 
@@ -439,7 +431,7 @@ def plot_time_forfeit_by_tc(tf_games_df):
439
  tf_by_tc = tf_games_df['TimeControl_Category'].value_counts()
440
  fig = px.bar(tf_by_tc, x=tf_by_tc.index, y=tf_by_tc.values, title="Time Forfeits by Time Control",
441
  labels={'x': 'Category', 'y': 'Forfeits'}, text=tf_by_tc.values)
442
- fig.update_layout(dragmode=False)
443
  fig.update_traces(marker_color='#795548', textposition='outside')
444
  return fig
445
 
@@ -464,9 +456,9 @@ def filter_and_analyze_time_forfeits(df):
464
  # =============================================
465
  def perform_full_analysis(username, time_period_key, perf_type, selected_titles_list, progress=gr.Progress(track_tqdm=True)):
466
  df, status_msg = load_from_lichess_api(username, time_period_key, perf_type, DEFAULT_RATED_ONLY, ECO_MAPPING, progress)
467
- num_outputs = 33 # Adjusted to match the actual number of outputs
468
  if not isinstance(df, pd.DataFrame) or df.empty:
469
- return status_msg, pd.DataFrame(), *([None] * (num_outputs - 2))
470
  try:
471
  fig_pie = plot_win_loss_pie(df, username)
472
  fig_color = plot_win_loss_by_color(df)
@@ -478,7 +470,6 @@ def perform_full_analysis(username, time_period_key, perf_type, selected_titles_
478
  d = len(df[df['PlayerResultNumeric'] == 0.5])
479
  wr = (w / total_g * 100) if total_g > 0 else 0
480
  avg_opp = df['OpponentElo'].mean()
481
- # Fixed formatting issue
482
  avg_opp_display = f"{avg_opp:.0f}" if not pd.isna(avg_opp) else 'N/A'
483
  overview_stats_md = f"**Total:** {total_g:,} | **WR:** {wr:.1f}% | **W/L/D:** {w}/{l}/{d} | **Avg Opp:** {avg_opp_display}"
484
  fig_games_yr = plot_games_per_year(df)
@@ -504,7 +495,7 @@ def perform_full_analysis(username, time_period_key, perf_type, selected_titles_
504
  term_counts = df['Termination'].value_counts()
505
  fig_term_all = px.bar(term_counts, x=term_counts.index, y=term_counts.values, title="Overall Termination Reasons",
506
  labels={'x': 'Reason', 'y': 'Count'}, text=term_counts.values)
507
- fig_term_all.update_layout(dragmode=False)
508
  fig_term_all.update_traces(textposition='outside')
509
  titled_status_msg = ""
510
  fig_titled_pie, fig_titled_color, fig_titled_rating, df_titled_h2h = go.Figure(), go.Figure(), go.Figure(), pd.DataFrame()
@@ -525,22 +516,35 @@ def perform_full_analysis(username, time_period_key, perf_type, selected_titles_
525
  titled_status_msg = f"ℹ️ No games found vs selected titles ({', '.join(selected_titles_list)})."
526
  else:
527
  titled_status_msg = "ℹ️ Select titles from the sidebar to analyze."
528
- return (status_msg, df, fig_pie, overview_stats_md, fig_color, fig_rating, fig_elo_diff, fig_games_yr, fig_wr_yr,
529
  "(Results by color shown in Overview)", fig_games_dow, fig_wr_dow, fig_games_hod, fig_wr_hod, fig_games_dom,
530
  fig_wr_dom, fig_perf_tc, fig_open_freq_api, fig_open_wr_api, fig_open_freq_cust, fig_open_wr_cust,
531
  fig_opp_freq, df_opp_list, fig_opp_elo, titled_status_msg, fig_titled_pie, fig_titled_color, fig_titled_rating,
532
  df_titled_h2h, fig_tf_summary, fig_tf_tc, df_tf_list, fig_term_all)
533
  except Exception as e:
534
  error_msg = f"🚨 Error generating results: {e}\n{traceback.format_exc()}"
535
- return error_msg, pd.DataFrame(), *([None] * (num_outputs - 2))
536
 
537
  # =============================================
538
  # Gradio Interface Definition
539
  # =============================================
540
- css = """.gradio-container { font-family: 'IBM Plex Sans', sans-serif; } footer { display: none !important; }"""
 
 
 
 
 
 
 
 
 
 
 
 
541
  with gr.Blocks(theme=gr.themes.Soft(), css=css) as demo:
542
  gr.Markdown("# ♟️ Lichess Insights\nAnalyze rated game statistics from Lichess API.")
543
  df_state = gr.State(pd.DataFrame())
 
544
  with gr.Row():
545
  with gr.Column(scale=1, min_width=250): # Sidebar
546
  gr.Markdown("## ⚙️ Settings")
@@ -556,70 +560,110 @@ with gr.Blocks(theme=gr.themes.Soft(), css=css) as demo:
556
  with gr.Column(scale=4): # Main Content
557
  with gr.Tabs() as tabs:
558
  with gr.TabItem("1. Overview", id=0):
559
- overview_stats_md_out = gr.Markdown()
560
- with gr.Row():
561
  overview_plot_pie = gr.Plot(label="Overall Results")
562
  overview_plot_color = gr.Plot(label="Results by Color")
563
  overview_plot_rating = gr.Plot(label="Rating Trend")
564
  overview_plot_elo_diff = gr.Plot(label="Elo Advantage vs. Result")
565
  with gr.TabItem("2. Perf. Over Time", id=1):
566
- time_plot_games_yr = gr.Plot(label="Games per Year")
567
- time_plot_wr_yr = gr.Plot(label="Win Rate per Year")
 
568
  with gr.TabItem("3. Perf. by Color", id=2):
569
- color_plot_placeholder = gr.Markdown("(Results by color shown in Overview)")
570
  with gr.TabItem("4. Time & Date", id=3):
571
  gr.Markdown("### Day of Week")
572
- with gr.Row():
573
  time_plot_games_dow = gr.Plot(label="Games by Day of Week")
574
  time_plot_wr_dow = gr.Plot(label="Win Rate by Day of Week")
575
  gr.Markdown("### Hour of Day (UTC)")
576
- with gr.Row():
577
  time_plot_games_hod = gr.Plot(label="Games by Hour (UTC)")
578
  time_plot_wr_hod = gr.Plot(label="Win Rate by Hour (UTC)")
579
  gr.Markdown("### Day of Month")
580
- with gr.Row():
581
  time_plot_games_dom = gr.Plot(label="Games by Day of Month")
582
  time_plot_wr_dom = gr.Plot(label="Win Rate by Day of Month")
583
  gr.Markdown("### Time Control Category")
584
- time_plot_perf_tc = gr.Plot(label="Performance by Time Control")
585
  with gr.TabItem("5. ECO & Openings", id=4):
586
  gr.Markdown("#### API Names")
587
- eco_plot_freq_api = gr.Plot(label="Opening Frequency (API)")
588
- eco_plot_wr_api = gr.Plot(label="Opening Win Rate (API)")
589
  gr.Markdown("---")
590
  gr.Markdown("#### Custom Map")
591
  if not ECO_MAPPING:
592
  gr.Markdown("⚠️ Custom map not loaded.")
593
- eco_plot_freq_cust = gr.Plot(label="Opening Frequency (Custom)")
594
- eco_plot_wr_cust = gr.Plot(label="Opening Win Rate (Custom)")
595
  with gr.TabItem("6. Opponents", id=5):
596
- opp_plot_freq = gr.Plot(label="Frequent Opponents")
597
- opp_df_list = gr.DataFrame(label="Top Opponents List", wrap=True)
598
- opp_plot_elo = gr.Plot(label="Elo Advantage vs Result")
599
  with gr.TabItem("7. vs Titled", id=6):
600
  gr.Markdown("Analysis based on sidebar selection.")
601
- titled_status = gr.Markdown()
602
- with gr.Row():
603
  titled_plot_pie = gr.Plot(label="Results vs Selected Titles")
604
  titled_plot_color = gr.Plot(label="Results by Color vs Selected Titles")
605
  titled_plot_rating = gr.Plot(label="Rating Trend vs Selected Titles")
606
- titled_df_h2h_comp = gr.DataFrame(label="Head-to-Head vs Selected Titles", wrap=True)
607
  with gr.TabItem("8. Termination", id=7):
608
  gr.Markdown("### Time Forfeit")
609
- term_plot_tf_summary = gr.Plot(label="Time Forfeit Summary")
610
- term_plot_tf_tc = gr.Plot(label="Time Forfeits by Time Control")
611
  with gr.Accordion("View Recent TF Games", open=False):
612
- term_df_tf_list = gr.DataFrame(label="Recent TF Games", wrap=True)
613
  gr.Markdown("### Overall Termination")
614
- term_plot_all = gr.Plot(label="Overall Termination")
615
- outputs_list = [status_output, df_state, overview_plot_pie, overview_stats_md_out, overview_plot_color, overview_plot_rating,
616
- overview_plot_elo_diff, time_plot_games_yr, time_plot_wr_yr, color_plot_placeholder, time_plot_games_dow,
617
- time_plot_wr_dow, time_plot_games_hod, time_plot_wr_hod, time_plot_games_dom, time_plot_wr_dom,
618
- time_plot_perf_tc, eco_plot_freq_api, eco_plot_wr_api, eco_plot_freq_cust, eco_plot_wr_cust, opp_plot_freq,
619
- opp_df_list, opp_plot_elo, titled_status, titled_plot_pie, titled_plot_color, titled_plot_rating,
620
- titled_df_h2h_comp, term_plot_tf_summary, term_plot_tf_tc, term_df_tf_list, term_plot_all]
621
- analyze_btn.click(fn=perform_full_analysis, inputs=[username_input, time_period_input, perf_type_input, titled_player_select],
622
- outputs=outputs_list)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
623
 
624
  # --- Launch the Gradio App ---
625
  if __name__ == "__main__":
 
1
  # -*- coding: utf-8 -*-
2
  # =============================================
3
  # Gradio App for Chess Game Analysis - Lichess API Version
4
+ # v18: Fixed empty chart visibility and chart sizing for mobile
5
  # =============================================
6
 
7
  import gradio as gr
 
32
  # =============================================
33
  def categorize_time_control(tc_str, speed_info):
34
  """Categorizes time control based on speed info or parsed string."""
 
35
  if isinstance(speed_info, str) and speed_info in ['bullet', 'blitz', 'rapid', 'classical', 'correspondence']:
36
  return speed_info.capitalize()
 
 
37
  if not isinstance(tc_str, str) or tc_str in ['-', '?', 'Unknown']:
38
  return 'Unknown'
39
  if tc_str == 'Correspondence':
40
  return 'Correspondence'
 
 
41
  if '+' in tc_str:
42
  parts = tc_str.split('+')
43
  if len(parts) == 2:
 
55
  return 'Unknown'
56
  else:
57
  return 'Unknown'
 
 
58
  else:
59
  try:
60
  base = int(tc_str)
 
93
  def load_from_lichess_api(username: str, time_period_key: str, perf_type: str, rated: bool, eco_map: dict, progress=None):
94
  if not username: return pd.DataFrame(), "⚠️ Enter username."
95
  if not perf_type: return pd.DataFrame(), "⚠️ Select game type."
 
96
  if progress is not None:
97
  try:
98
  progress(0, desc="Initializing...")
99
  except Exception:
100
+ pass
101
  username_lower = username.lower()
102
  status_message = f"Fetching {perf_type} games..."
103
  if progress is not None:
 
231
  fig = px.pie(values=result_counts.values, names=result_counts.index, title=f'Overall Results for {display_name}',
232
  color=result_counts.index, color_discrete_map={'Win': '#4CAF50', 'Draw': '#B0BEC5', 'Loss': '#F44336'}, hole=0.3)
233
  fig.update_traces(textposition='inside', textinfo='percent+label', pull=[0.05 if x == 'Win' else 0 for x in result_counts.index])
234
+ fig.update_layout(dragmode=False, autosize=True, height=400, width=400, margin=dict(l=20, r=20, t=50, b=20))
235
  return fig
236
 
237
  def plot_win_loss_by_color(df):
 
247
  fig = px.bar(color_results_pct, barmode='stack', title='Results by Color', labels={'value': '%', 'PlayerColor': 'Played As'},
248
  color='PlayerResultString', color_discrete_map={'Win': '#4CAF50', 'Draw': '#B0BEC5', 'Loss': '#F44336'},
249
  text_auto='.1f', category_orders={"PlayerColor": ["White", "Black"]})
250
+ fig.update_layout(yaxis_title="Percentage (%)", xaxis_title="Color Played", dragmode=False, autosize=True, height=400, margin=dict(l=20, r=20, t=50, b=20))
251
  fig.update_traces(textangle=0)
252
  return fig
253
 
 
261
  fig.add_trace(go.Scatter(x=df_sorted['Date'], y=df_sorted['PlayerElo'], mode='lines+markers', name='Elo',
262
  line=dict(color='#1E88E5', width=2), marker=dict(size=5, opacity=0.7)))
263
  fig.update_layout(title=f'{display_name}\'s Rating Trend', xaxis_title='Date', yaxis_title='Elo Rating',
264
+ hovermode="x unified", xaxis_rangeslider_visible=True, dragmode=False, autosize=True, height=400, margin=dict(l=20, r=20, t=50, b=20))
265
  return fig
266
 
267
  def plot_performance_vs_opponent_elo(df):
 
272
  color_discrete_map={'Win': '#4CAF50', 'Draw': '#B0BEC5', 'Loss': '#F44336'}, points='outliers')
273
  fig.add_hline(y=0, line_dash="dash", line_color="grey")
274
  fig.update_traces(marker=dict(opacity=0.8))
275
+ fig.update_layout(dragmode=False, autosize=True, height=400, margin=dict(l=20, r=20, t=50, b=20))
276
  return fig
277
 
278
  def plot_games_by_dow(df):
 
282
  fig = px.bar(games_by_dow, x=games_by_dow.index, y=games_by_dow.values, title="Games by Day of Week",
283
  labels={'x': 'Day', 'y': 'Games'}, text=games_by_dow.values)
284
  fig.update_traces(marker_color='#9C27B0', textposition='outside')
285
+ fig.update_layout(dragmode=False, autosize=True, height=400, margin=dict(l=20, r=20, t=50, b=20))
286
  return fig
287
 
288
  def plot_winrate_by_dow(df):
 
295
  fig = px.bar(win_rate, x=win_rate.index, y=win_rate.values, title="Win Rate (%) by Day",
296
  labels={'x': 'Day', 'y': 'Win Rate (%)'}, text=win_rate.values)
297
  fig.update_traces(marker_color='#FF9800', texttemplate='%{text:.1f}%', textposition='outside')
298
+ fig.update_layout(yaxis_range=[0, 100], dragmode=False, autosize=True, height=400, margin=dict(l=20, r=20, t=50, b=20))
299
  return fig
300
 
301
  def plot_games_by_hour(df):
 
304
  fig = px.bar(games_by_hour, x=games_by_hour.index, y=games_by_hour.values, title="Games by Hour (UTC)",
305
  labels={'x': 'Hour', 'y': 'Games'}, text=games_by_hour.values)
306
  fig.update_traces(marker_color='#03A9F4', textposition='outside')
307
+ fig.update_layout(xaxis=dict(tickmode='linear'), dragmode=False, autosize=True, height=400, margin=dict(l=20, r=20, t=50, b=20))
308
  return fig
309
 
310
  def plot_winrate_by_hour(df):
 
316
  fig = px.line(win_rate, x=win_rate.index, y=win_rate.values, markers=True, title="Win Rate (%) by Hour (UTC)",
317
  labels={'x': 'Hour', 'y': 'Win Rate (%)'})
318
  fig.update_traces(line_color='#8BC34A')
319
+ fig.update_layout(yaxis_range=[0, 100], xaxis=dict(tickmode='linear'), dragmode=False, autosize=True, height=400, margin=dict(l=20, r=20, t=50, b=20))
320
  return fig
321
 
322
  def plot_games_per_year(df):
 
325
  fig = px.bar(games_per_year, x=games_per_year.index, y=games_per_year.values, title='Games Per Year',
326
  labels={'x': 'Year', 'y': 'Games'}, text=games_per_year.values)
327
  fig.update_traces(marker_color='#2196F3', textposition='outside')
328
+ fig.update_layout(xaxis_title="Year", yaxis_title="Number of Games", xaxis={'type': 'category'}, dragmode=False, autosize=True, height=400, margin=dict(l=20, r=20, t=50, b=20))
329
  return fig
330
 
331
  def plot_win_rate_per_year(df):
 
337
  fig = px.line(win_rate, x=win_rate.index, y=win_rate.values, title='Win Rate (%) Per Year', markers=True,
338
  labels={'x': 'Year', 'y': 'Win Rate (%)'})
339
  fig.update_traces(line_color='#FFC107', line_width=2.5)
340
+ fig.update_layout(yaxis_range=[0, 100], dragmode=False, autosize=True, height=400, margin=dict(l=20, r=20, t=50, b=20))
341
  return fig
342
 
343
  def plot_performance_by_time_control(df):
 
355
  fig = px.bar(tc_results_pct, title='Performance by Time Control', labels={'value': '%', 'TimeControl_Category': 'Category'},
356
  color='PlayerResultString', color_discrete_map={'Win': '#4CAF50', 'Draw': '#B0BEC5', 'Loss': '#F44336'},
357
  barmode='group', text_auto='.1f')
358
+ fig.update_layout(xaxis_title="Time Control Category", yaxis_title="Percentage (%)", dragmode=False, autosize=True, height=400, margin=dict(l=20, r=20, t=50, b=20))
359
  fig.update_traces(textangle=0)
360
  return fig
361
  except Exception:
 
367
  opening_counts = df[df[opening_col] != 'Unknown Opening'][opening_col].value_counts().nlargest(top_n)
368
  fig = px.bar(opening_counts, y=opening_counts.index, x=opening_counts.values, orientation='h',
369
  title=f'Top {top_n} Openings ({source_label})', labels={'y': 'Opening', 'x': 'Games'}, text=opening_counts.values)
370
+ fig.update_layout(yaxis={'categoryorder': 'total ascending'}, dragmode=False, autosize=True, height=500, margin=dict(l=20, r=20, t=50, b=20))
371
  fig.update_traces(marker_color='#673AB7', textposition='outside')
372
  return fig
373
 
 
384
  title=f'Top {top_n} Openings by Win Rate (Min {min_games} games, {source_label})',
385
  labels={'win_rate': 'Win Rate (%)', opening_col: 'Opening'}, text='win_rate')
386
  fig.update_traces(texttemplate='%{text:.1f}%', textposition='inside', marker_color='#009688')
387
+ fig.update_layout(yaxis={'categoryorder': 'total ascending'}, xaxis_title="Win Rate (%)", dragmode=False, autosize=True, height=500, margin=dict(l=20, r=20, t=50, b=20))
388
  return fig
389
 
390
  def plot_most_frequent_opponents(df, top_n=20):
 
392
  opp_counts = df[df['OpponentName'] != 'Unknown']['OpponentName'].value_counts().nlargest(top_n)
393
  fig = px.bar(opp_counts, y=opp_counts.index, x=opp_counts.values, orientation='h',
394
  title=f'Top {top_n} Opponents', labels={'y': 'Opponent', 'x': 'Games'}, text=opp_counts.values)
395
+ fig.update_layout(yaxis={'categoryorder': 'total ascending'}, dragmode=False, autosize=True, height=500, margin=dict(l=20, r=20, t=50, b=20))
396
  fig.update_traces(marker_color='#FF5722', textposition='outside')
397
  return fig
398
 
 
402
  fig = px.bar(games_by_dom, x=games_by_dom.index, y=games_by_dom.values, title="Games Played per Day of Month",
403
  labels={'x': 'Day of Month', 'y': 'Number of Games'}, text=games_by_dom.values)
404
  fig.update_traces(marker_color='#E91E63', textposition='outside')
405
+ fig.update_layout(xaxis=dict(tickmode='linear'), dragmode=False, autosize=True, height=400, margin=dict(l=20, r=20, t=50, b=20))
406
  return fig
407
 
408
  def plot_winrate_by_dom(df):
 
414
  fig = px.line(win_rate, x=win_rate.index, y=win_rate.values, markers=True, title="Win Rate (%) per Day of Month",
415
  labels={'x': 'Day of Month', 'y': 'Win Rate (%)'})
416
  fig.update_traces(line_color='#FF5722')
417
+ fig.update_layout(yaxis_range=[0, 100], xaxis=dict(tickmode='linear'), dragmode=False, autosize=True, height=400, margin=dict(l=20, r=20, t=50, b=20))
418
  return fig
419
 
420
  def plot_time_forfeit_summary(wins_tf, losses_tf):
 
422
  df_tf = pd.DataFrame(data)
423
  fig = px.bar(df_tf, x='Outcome', y='Count', title="Time Forfeit Summary", color='Outcome',
424
  color_discrete_map={'Won on Time': '#4CAF50', 'Lost on Time': '#F44336'}, text='Count')
425
+ fig.update_layout(showlegend=False, dragmode=False, autosize=True, height=400, margin=dict(l=20, r=20, t=50, b=20))
426
  fig.update_traces(textposition='outside')
427
  return fig
428
 
 
431
  tf_by_tc = tf_games_df['TimeControl_Category'].value_counts()
432
  fig = px.bar(tf_by_tc, x=tf_by_tc.index, y=tf_by_tc.values, title="Time Forfeits by Time Control",
433
  labels={'x': 'Category', 'y': 'Forfeits'}, text=tf_by_tc.values)
434
+ fig.update_layout(dragmode=False, autosize=True, height=400, margin=dict(l=20, r=20, t=50, b=20))
435
  fig.update_traces(marker_color='#795548', textposition='outside')
436
  return fig
437
 
 
456
  # =============================================
457
  def perform_full_analysis(username, time_period_key, perf_type, selected_titles_list, progress=gr.Progress(track_tqdm=True)):
458
  df, status_msg = load_from_lichess_api(username, time_period_key, perf_type, DEFAULT_RATED_ONLY, ECO_MAPPING, progress)
459
+ num_outputs = 34 # Adjusted for visibility state
460
  if not isinstance(df, pd.DataFrame) or df.empty:
461
+ return status_msg, pd.DataFrame(), False, *([None] * (num_outputs - 3))
462
  try:
463
  fig_pie = plot_win_loss_pie(df, username)
464
  fig_color = plot_win_loss_by_color(df)
 
470
  d = len(df[df['PlayerResultNumeric'] == 0.5])
471
  wr = (w / total_g * 100) if total_g > 0 else 0
472
  avg_opp = df['OpponentElo'].mean()
 
473
  avg_opp_display = f"{avg_opp:.0f}" if not pd.isna(avg_opp) else 'N/A'
474
  overview_stats_md = f"**Total:** {total_g:,} | **WR:** {wr:.1f}% | **W/L/D:** {w}/{l}/{d} | **Avg Opp:** {avg_opp_display}"
475
  fig_games_yr = plot_games_per_year(df)
 
495
  term_counts = df['Termination'].value_counts()
496
  fig_term_all = px.bar(term_counts, x=term_counts.index, y=term_counts.values, title="Overall Termination Reasons",
497
  labels={'x': 'Reason', 'y': 'Count'}, text=term_counts.values)
498
+ fig_term_all.update_layout(dragmode=False, autosize=True, height=400, margin=dict(l=20, r=20, t=50, b=20))
499
  fig_term_all.update_traces(textposition='outside')
500
  titled_status_msg = ""
501
  fig_titled_pie, fig_titled_color, fig_titled_rating, df_titled_h2h = go.Figure(), go.Figure(), go.Figure(), pd.DataFrame()
 
516
  titled_status_msg = f"ℹ️ No games found vs selected titles ({', '.join(selected_titles_list)})."
517
  else:
518
  titled_status_msg = "ℹ️ Select titles from the sidebar to analyze."
519
+ return (status_msg, df, True, fig_pie, overview_stats_md, fig_color, fig_rating, fig_elo_diff, fig_games_yr, fig_wr_yr,
520
  "(Results by color shown in Overview)", fig_games_dow, fig_wr_dow, fig_games_hod, fig_wr_hod, fig_games_dom,
521
  fig_wr_dom, fig_perf_tc, fig_open_freq_api, fig_open_wr_api, fig_open_freq_cust, fig_open_wr_cust,
522
  fig_opp_freq, df_opp_list, fig_opp_elo, titled_status_msg, fig_titled_pie, fig_titled_color, fig_titled_rating,
523
  df_titled_h2h, fig_tf_summary, fig_tf_tc, df_tf_list, fig_term_all)
524
  except Exception as e:
525
  error_msg = f"🚨 Error generating results: {e}\n{traceback.format_exc()}"
526
+ return error_msg, pd.DataFrame(), False, *([None] * (num_outputs - 3))
527
 
528
  # =============================================
529
  # Gradio Interface Definition
530
  # =============================================
531
+ css = """
532
+ .gradio-container { font-family: 'IBM Plex Sans', sans-serif; }
533
+ footer { display: none !important; }
534
+ /* Responsive adjustments for plots */
535
+ .gr-plot { min-width: 100% !important; }
536
+ @media (max-width: 768px) {
537
+ .gr-row { flex-direction: column !important; }
538
+ .gr-plot { height: 350px !important; margin-bottom: 20px !important; }
539
+ }
540
+ @media (min-width: 769px) {
541
+ .gr-plot { height: 400px !important; }
542
+ }
543
+ """
544
  with gr.Blocks(theme=gr.themes.Soft(), css=css) as demo:
545
  gr.Markdown("# ♟️ Lichess Insights\nAnalyze rated game statistics from Lichess API.")
546
  df_state = gr.State(pd.DataFrame())
547
+ has_data = gr.State(False) # State to track if data is available
548
  with gr.Row():
549
  with gr.Column(scale=1, min_width=250): # Sidebar
550
  gr.Markdown("## ⚙️ Settings")
 
560
  with gr.Column(scale=4): # Main Content
561
  with gr.Tabs() as tabs:
562
  with gr.TabItem("1. Overview", id=0):
563
+ overview_stats_md_out = gr.Markdown(visible=False)
564
+ with gr.Row(visible=False) as overview_row:
565
  overview_plot_pie = gr.Plot(label="Overall Results")
566
  overview_plot_color = gr.Plot(label="Results by Color")
567
  overview_plot_rating = gr.Plot(label="Rating Trend")
568
  overview_plot_elo_diff = gr.Plot(label="Elo Advantage vs. Result")
569
  with gr.TabItem("2. Perf. Over Time", id=1):
570
+ with gr.Row(visible=False) as perf_time_row:
571
+ time_plot_games_yr = gr.Plot(label="Games per Year")
572
+ time_plot_wr_yr = gr.Plot(label="Win Rate per Year")
573
  with gr.TabItem("3. Perf. by Color", id=2):
574
+ color_plot_placeholder = gr.Markdown("(Results by color shown in Overview)", visible=False)
575
  with gr.TabItem("4. Time & Date", id=3):
576
  gr.Markdown("### Day of Week")
577
+ with gr.Row(visible=False) as dow_row:
578
  time_plot_games_dow = gr.Plot(label="Games by Day of Week")
579
  time_plot_wr_dow = gr.Plot(label="Win Rate by Day of Week")
580
  gr.Markdown("### Hour of Day (UTC)")
581
+ with gr.Row(visible=False) as hod_row:
582
  time_plot_games_hod = gr.Plot(label="Games by Hour (UTC)")
583
  time_plot_wr_hod = gr.Plot(label="Win Rate by Hour (UTC)")
584
  gr.Markdown("### Day of Month")
585
+ with gr.Row(visible=False) as dom_row:
586
  time_plot_games_dom = gr.Plot(label="Games by Day of Month")
587
  time_plot_wr_dom = gr.Plot(label="Win Rate by Day of Month")
588
  gr.Markdown("### Time Control Category")
589
+ time_plot_perf_tc = gr.Plot(label="Performance by Time Control", visible=False)
590
  with gr.TabItem("5. ECO & Openings", id=4):
591
  gr.Markdown("#### API Names")
592
+ eco_plot_freq_api = gr.Plot(label="Opening Frequency (API)", visible=False)
593
+ eco_plot_wr_api = gr.Plot(label="Opening Win Rate (API)", visible=False)
594
  gr.Markdown("---")
595
  gr.Markdown("#### Custom Map")
596
  if not ECO_MAPPING:
597
  gr.Markdown("⚠️ Custom map not loaded.")
598
+ eco_plot_freq_cust = gr.Plot(label="Opening Frequency (Custom)", visible=False)
599
+ eco_plot_wr_cust = gr.Plot(label="Opening Win Rate (Custom)", visible=False)
600
  with gr.TabItem("6. Opponents", id=5):
601
+ opp_plot_freq = gr.Plot(label="Frequent Opponents", visible=False)
602
+ opp_df_list = gr.DataFrame(label="Top Opponents List", wrap=True, visible=False)
603
+ opp_plot_elo = gr.Plot(label="Elo Advantage vs Result", visible=False)
604
  with gr.TabItem("7. vs Titled", id=6):
605
  gr.Markdown("Analysis based on sidebar selection.")
606
+ titled_status = gr.Markdown(visible=False)
607
+ with gr.Row(visible=False) as titled_row:
608
  titled_plot_pie = gr.Plot(label="Results vs Selected Titles")
609
  titled_plot_color = gr.Plot(label="Results by Color vs Selected Titles")
610
  titled_plot_rating = gr.Plot(label="Rating Trend vs Selected Titles")
611
+ titled_df_h2h_comp = gr.DataFrame(label="Head-to-Head vs Selected Titles", wrap=True, visible=False)
612
  with gr.TabItem("8. Termination", id=7):
613
  gr.Markdown("### Time Forfeit")
614
+ term_plot_tf_summary = gr.Plot(label="Time Forfeit Summary", visible=False)
615
+ term_plot_tf_tc = gr.Plot(label="Time Forfeits by Time Control", visible=False)
616
  with gr.Accordion("View Recent TF Games", open=False):
617
+ term_df_tf_list = gr.DataFrame(label="Recent TF Games", wrap=True, visible=False)
618
  gr.Markdown("### Overall Termination")
619
+ term_plot_all = gr.Plot(label="Overall Termination", visible=False)
620
+
621
+ # Define visibility updates based on has_data
622
+ def update_visibility(has_data_value, *args):
623
+ visibility = has_data_value
624
+ return (
625
+ gr.update(visible=visibility), # overview_stats_md_out
626
+ gr.update(visible=visibility), # overview_row
627
+ gr.update(visible=visibility), # perf_time_row
628
+ gr.update(visible=visibility), # color_plot_placeholder
629
+ gr.update(visible=visibility), # dow_row
630
+ gr.update(visible=visibility), # hod_row
631
+ gr.update(visible=visibility), # dom_row
632
+ gr.update(visible=visibility), # time_plot_perf_tc
633
+ gr.update(visible=visibility), # eco_plot_freq_api
634
+ gr.update(visible=visibility), # eco_plot_wr_api
635
+ gr.update(visible=visibility), # eco_plot_freq_cust
636
+ gr.update(visible=visibility), # eco_plot_wr_cust
637
+ gr.update(visible=visibility), # opp_plot_freq
638
+ gr.update(visible=visibility), # opp_df_list
639
+ gr.update(visible=visibility), # opp_plot_elo
640
+ gr.update(visible=visibility), # titled_status
641
+ gr.update(visible=visibility), # titled_row
642
+ gr.update(visible=visibility), # titled_df_h2h_comp
643
+ gr.update(visible=visibility), # term_plot_tf_summary
644
+ gr.update(visible=visibility), # term_plot_tf_tc
645
+ gr.update(visible=visibility), # term_df_tf_list
646
+ gr.update(visible=visibility), # term_plot_all
647
+ )
648
+
649
+ outputs_list = [
650
+ status_output, df_state, has_data,
651
+ overview_plot_pie, overview_stats_md_out, overview_plot_color, overview_plot_rating, overview_plot_elo_diff,
652
+ time_plot_games_yr, time_plot_wr_yr, color_plot_placeholder, time_plot_games_dow, time_plot_wr_dow,
653
+ time_plot_games_hod, time_plot_wr_hod, time_plot_games_dom, time_plot_wr_dom, time_plot_perf_tc,
654
+ eco_plot_freq_api, eco_plot_wr_api, eco_plot_freq_cust, eco_plot_wr_cust, opp_plot_freq, opp_df_list,
655
+ opp_plot_elo, titled_status, titled_plot_pie, titled_plot_color, titled_plot_rating, titled_df_h2h_comp,
656
+ term_plot_tf_summary, term_plot_tf_tc, term_df_tf_list, term_plot_all
657
+ ]
658
+ visibility_outputs = [
659
+ overview_stats_md_out, overview_row, perf_time_row, color_plot_placeholder, dow_row, hod_row, dom_row,
660
+ time_plot_perf_tc, eco_plot_freq_api, eco_plot_wr_api, eco_plot_freq_cust, eco_plot_wr_cust, opp_plot_freq,
661
+ opp_df_list, opp_plot_elo, titled_status, titled_row, titled_df_h2h_comp, term_plot_tf_summary, term_plot_tf_tc,
662
+ term_df_tf_list, term_plot_all
663
+ ]
664
+ analyze_btn.click(fn=perform_full_analysis, inputs=[username_input, time_period_input, perf_type_input, titled_player_select], outputs=outputs_list).then(
665
+ fn=update_visibility, inputs=[has_data] + outputs_list[3:], outputs=visibility_outputs
666
+ )
667
 
668
  # --- Launch the Gradio App ---
669
  if __name__ == "__main__":