from dash import Dash, callback, html, dcc, Input, Output, State
import dash_ag_grid as dag
import pandas as pd
import os
# === CONSTANTS ===
DATA_PATH = "./tmp_data"
os.makedirs(DATA_PATH, exist_ok=True)
MATCHES_FILE = os.path.join(DATA_PATH, "matches.csv")
PLAYER_FILE = os.path.join(DATA_PATH, "player.csv")
RANKING_FILE = os.path.join(DATA_PATH, "ranking.csv")
EXCLUSION_FILE = os.path.join(DATA_PATH, "exclusion.csv")
PLAYER_ID = 142510
# === UI Styles ===
BASE_STYLE = {"fontFamily": "Arial, sans-serif", "fontSize": "10px"}
TEXT_BLOCK_STYLE = {"whiteSpace": "pre-wrap", "margin": "7px", **BASE_STYLE}
LINK_STYLE = {"textDecoration": "none", "color": "DarkSlateBlue"}
INPUT_STYLE = {"fontSize": "9px", "padding": "2px", "height": "17px", "margin": "7px", "width": "150px"}
MELODY_STYLE = {"backgroundColor": "GhostWhite", "padding": "7px", "borderRadius": "7px", "width": "130px", "textDecoration": "none", "text-align": "center", "fontSize": "11px"}
SUMMARY_STYLE = {"fontSize": "11px", "fontWeight": "bold", "cursor": "pointer", "backgroundColor": "Lavender", "padding": "5px", "border": "1px solid Lavender", "borderRadius": "7px", "margin": "0 0 2px 0"}
GRID_STYLE = {"height": "207px", "width": "100%"}
def debug_print(msg, obj=None):
print(f"DEBUG: {msg}")
if obj is not None:
print(obj if isinstance(obj, str) else str(obj)[:600])
print("--------")
def safe_load_csv(path):
if os.path.exists(path):
return pd.read_csv(path)
else:
debug_print(f"File {path} missing, loading as empty DataFrame.")
return pd.DataFrame()
def load_all_data():
df = safe_load_csv(MATCHES_FILE)
info = safe_load_csv(PLAYER_FILE)
ranking_df = safe_load_csv(RANKING_FILE)
exclusion_df = safe_load_csv(EXCLUSION_FILE)
# Process match dataframe
if not df.empty and df.shape[1] > 8:
try:
df.drop(columns=df.columns[[0, 5, 7, 8, 11]], inplace=True)
df.sort_values(by="EventDate", ascending=False, inplace=True)
wl_summary = (
df.groupby("OpponentID")["WonLost"]
.value_counts()
.unstack(fill_value=0)
.rename(columns={"W": "Wins", "L": "Losses"})
)
wl_summary["W/L"] = wl_summary.apply(
lambda r: f"{r.get('Wins', 0)}/{r.get('Losses', 0)}", axis=1
)
df = df.merge(wl_summary[["W/L"]], on="OpponentID", how="left")
df = df.reset_index(drop=True)
df["row_id"] = df.apply(lambda r: f"{r['OpponentID']}_{r['EventDate']}_{r.name}", axis=1)
debug_print("Loaded match DataFrame", df.head())
except Exception as e:
debug_print("Error processing match data", str(e))
info_row = info.iloc[0] if not info.empty else {}
# Process rankings
if not ranking_df.empty and not exclusion_df.empty and "ID" in ranking_df.columns:
filtered_df = ranking_df[~ranking_df["ID"].isin(exclusion_df["ID"])]
debug_print("Loaded filtered rankings", filtered_df.head())
else:
filtered_df = pd.DataFrame()
debug_print("No rankings data available")
return df, info_row, filtered_df
def build_aggrid(df, selectedRows=None):
if df.empty:
return html.Div("No match data available")
expected_columns = [
"EventDate", "OpponentName", "OpponentMean", "WonLost",
"OpponentID", "Score", "PlayerMean", "W/L", "row_id"
]
for col in expected_columns:
if col not in df.columns:
df[col] = [None] * len(df)
col_map = {
"EventDate": "Date",
"OpponentName": "Name",
"OpponentMean": "Rating",
"WonLost": "WonLost",
}
hidden = {"OpponentID", "Score", "PlayerMean", "W/L", "row_id"}
column_defs = [
{
"field": c,
"headerName": col_map.get(c, c),
"hide": c in hidden
}
for c in expected_columns
]
row_data = df.to_dict("records")
return dag.AgGrid(
id="grid-match-history",
rowData=row_data,
columnDefs=column_defs,
style=GRID_STYLE,
columnSize="sizeToFit",
dashGridOptions={
"rowSelection": {
"mode": "singleRow",
"checkboxes": False,
"headerCheckbox": False,
"enableClickSelection": True,
},
"getRowId": "params.data.row_id",
"domLayout": "normal",
"headerHeight": 25,
"rowHeight": 20,
},
selectedRows=selectedRows or []
)
def create_accordion(df):
if df.empty:
return html.Div("No rankings data")
thresholds = [1600, 1500, 1400, 1300, 1200, 1100, 1000, 900, 850]
melody_rows = df[df["Name"] == "Shih, Melody"]
melody_rating = int(melody_rows.iloc[0]["Rating"]) if not melody_rows.empty else 1300
sorted_df = df.sort_values(by="Rating", ascending=False).reset_index(drop=True)
melody_idx = sorted_df[sorted_df["Name"] == "Shih, Melody"].index
melody_order = melody_idx[0] + 1 if len(melody_idx) > 0 else None
left_threshold_cutoff = min([t for t in thresholds if t > melody_rating], default=thresholds[0])
df_left = df[df["Rating"] > left_threshold_cutoff].reset_index(drop=True)
df_right = df[df["Rating"] <= left_threshold_cutoff].reset_index(drop=True)
def make_player_row(row):
rating_display = f" {row['Rating']}"
if row["Name"] == "Shih, Melody" and melody_order is not None:
rating_display += f" (#{melody_order})"
return html.Div([
html.A(
row["Name"],
href=f"https://www.ratingscentral.com/PlayerHistory.php?PlayerID={row['ID']}",
target="_blank",
style=LINK_STYLE,
),
html.Span(rating_display),
], style=MELODY_STYLE if row["Name"] == "Shih, Melody" else {})
def insert_thresholds(players_df, thresholds_for_column):
children, t_idx = [], 0
current_threshold = thresholds_for_column[0] if thresholds_for_column else None
for _, row in players_df.iterrows():
while current_threshold and row["Rating"] < current_threshold:
children.append(html.Div(f"------- {current_threshold} -------", style={"fontSize":"9px", "fontStyle":"italic", "margin":"2px 0", "color":"MediumPurple"}))
t_idx += 1
current_threshold = thresholds_for_column[t_idx] if t_idx < len(thresholds_for_column) else None
children.append(make_player_row(row))
return children
left_thresholds = [t for t in thresholds if t > left_threshold_cutoff]
right_thresholds = [t for t in thresholds if t <= left_threshold_cutoff]
left_children = insert_thresholds(df_left, left_thresholds)
right_children = insert_thresholds(df_right, right_thresholds)
return html.Details([
html.Summary("NSW Junior Girls", style=SUMMARY_STYLE),
html.Div([
html.Div(left_children, style={"flex": 1, "paddingRight": "10px"}),
html.Div(right_children, style={"flex": 1, "paddingLeft": "10px"}),
], style={**BASE_STYLE, "display": "flex", "gap": "20px"}),
], open=True)
# Load data once at startup
df_all, info, filtered_df = load_all_data()
rating = info.get("Rating", "")
stdev = info.get("StDev", "")
last_played = info.get("LastPlayed", "")
last_event = info.get("LastEvent", "")
app = Dash(__name__)
app.layout = html.Div([
dcc.Store(id="df-store", data=df_all.to_dict("records")),
html.Div([
html.Div([
html.A(
f"{rating} ± {stdev}",
href=f"https://www.ratingscentral.com/PlayerHistory.php?PlayerID={PLAYER_ID}",
target="_blank",
style=MELODY_STYLE,
),
html.A(
last_played,
href=f"https://www.ratingscentral.com/EventDetail.php?EventID={last_event}#P{PLAYER_ID}",
target="_blank",
style={"margin": "7px", **LINK_STYLE},
),
]),
dcc.Input(
id="opp-filter",
type="text",
placeholder="Filter Opponent...",
style=INPUT_STYLE,
),
]),
html.Div(id="grid-container", children=build_aggrid(df_all)),
html.Div(id="opp-container", style=TEXT_BLOCK_STYLE),
create_accordion(filtered_df),
])
@callback(
Output("grid-container", "children"),
[Input("df-store", "data"), Input("opp-filter", "value"), Input("grid-match-history", "selectedRows")],
prevent_initial_call=False
)
def update_grid(data, filter_value, selected):
debug_print("CALLBACK: update_grid triggered", {"filter_value": filter_value})
df = pd.DataFrame(data)
if filter_value:
filtered = df[df["OpponentName"].str.contains(filter_value, case=False, na=False)]
else:
filtered = df
next_selected = []
if selected:
ids = set(row.get("row_id") for row in selected if "row_id" in row)
next_selected = [row for row in filtered.to_dict("records") if row.get("row_id") in ids]
return build_aggrid(filtered, next_selected)
@callback(
Output("opp-container", "children"),
Input("grid-match-history", "selectedRows"),
State("df-store", "data"),
)
def display_selected_row(selected, data):
debug_print("CALLBACK: display_selected_row triggered", {"selected": selected})
if not selected or len(selected) == 0:
return "No row selected."
try:
df = pd.DataFrame(data)
row = selected[0]
opponent_id = row.get("OpponentID")
wl = row.get("W/L", "")
try:
wins, losses = (int(x) for x in wl.split("/"))
win_rate = wins / (wins + losses) * 100 if (wins + losses) > 0 else 0
summary = f"{win_rate:.0f}% win rate ({wins} {'Win' if wins == 1 else 'Wins'} {losses} {'Loss' if losses == 1 else 'Losses'})"
except Exception:
summary = f"W/L: {wl}"
opp_df = df[df["OpponentID"] == opponent_id][
["EventDate", "OpponentMean", "WonLost", "Score", "PlayerMean"]
]
opp_df.columns = ["Date", "OppRating", "Result", "Score", "Rating"]
link = html.A(
row.get("OpponentName", ""),
href=f"https://www.ratingscentral.com/PlayerHistory.php?PlayerID={opponent_id}",
target="_blank",
style=LINK_STYLE,
)
return html.Div([
html.Span([link, f" {summary}"]),
html.Pre(opp_df.to_string(index=False, justify="center")),
])
except Exception as e:
debug_print("Exception in display_selected_row", str(e))
return f"Error: {str(e)}"
if __name__ == "__main__":
debug_print("Dash Starting")
app.run_server(debug=True)