diff --git a/app.py b/app.py index 7d31d0d..72159bc 100644 --- a/app.py +++ b/app.py @@ -7,6 +7,7 @@ from pathlib import Path from flask import Flask, flash, redirect, render_template, request, session, url_for from stats import ( + apply_rank_changes, build_leaderboard, build_session_summaries, cents_to_dollars, @@ -71,22 +72,30 @@ def home() -> str: def leaderboard() -> str: events = load_events(DATA_PATH) all_sessions = build_session_summaries(events) + ordered_sessions = sorted(all_sessions, key=lambda s: s.session_date) + + session_dates = [session.session_date for session in ordered_sessions] selected_session_date = request.args.get("through_session", "").strip() - valid_session_dates = {session.session_date for session in all_sessions} - if selected_session_date not in valid_session_dates: - selected_session_date = "" + if session_dates: + if selected_session_date in session_dates: + cutoff_index = session_dates.index(selected_session_date) + else: + cutoff_index = len(session_dates) - 1 + selected_session = session_dates[cutoff_index] - filtered_sessions = all_sessions - if selected_session_date: - filtered_sessions = [ - session - for session in all_sessions - if session.session_date <= selected_session_date - ] + filtered_sessions = ordered_sessions[: cutoff_index + 1] + previous_sessions = ordered_sessions[:cutoff_index] + else: + filtered_sessions = [] + previous_sessions = [] board = build_leaderboard(filtered_sessions) + previous_board = build_leaderboard(previous_sessions) + board = apply_rank_changes(board, previous_board) + chart_data = cumulative_profit_series(filtered_sessions) + return render_template( "leaderboard.html", leaderboard=board, diff --git a/static/css/style.css b/static/css/style.css index 96ec9c5..b679fa4 100644 --- a/static/css/style.css +++ b/static/css/style.css @@ -489,6 +489,31 @@ h2 { letter-spacing: -0.06em; } +.rank-move { + display: inline-flex; + align-items: center; + justify-content: center; + min-width: 2.5rem; + font-weight: 650; + letter-spacing: 0.01em; +} + +.rank-up { + color: #22c55e; +} + +.rank-down { + color: #ef4444; +} + +.rank-flat { + color: var(--text-muted); +} + +.inline-filter-form select { + min-width: 12rem; +} + .table-wrap { position: relative; z-index: 1; diff --git a/stats.py b/stats.py index db88e80..107daf9 100644 --- a/stats.py +++ b/stats.py @@ -71,15 +71,30 @@ class PlayerStats: total_cash_out_cents: int total_net_cents: int roi_pct: float + rank_change: int = 0 +def apply_rank_changes( + current_board: list[PlayerStats], prev_board: list[PlayerStats] +) -> list[PlayerStats]: + prev_ranks = {p.player_name: i for i, p in enumerate(prev_board, start=1)} + + for i, p in enumerate(current_board, start=1): + prev_rank = prev_ranks.get(p.player_name) + + if prev_rank is None: + p.rank_change = 0 + else: + p.rank_change = prev_rank - i + + return current_board + def cents_to_dollars(cents: int) -> str: value = cents / 100 return f"${value:,.2f}" - def safe_date_label(session_date: str) -> str: try: return datetime.strptime(session_date, "%Y-%m-%d").strftime("%b %d, %Y") @@ -87,7 +102,6 @@ def safe_date_label(session_date: str) -> str: return session_date - def color_for_name(name: str, names: list[str]) -> str: try: index = sorted(names, key=str.casefold).index(name) @@ -96,7 +110,6 @@ def color_for_name(name: str, names: list[str]) -> str: return PLAYER_PALETTE[index % len(PLAYER_PALETTE)] - def net_tone(value_cents: int) -> str: if value_cents > 0: return "#22c55e" @@ -105,7 +118,6 @@ def net_tone(value_cents: int) -> str: return "#f59e0b" - def build_session_summaries(events: list[EventRow]) -> list[SessionSummary]: grouped: dict[tuple[str, str], SessionEntry] = {} @@ -141,7 +153,6 @@ def build_session_summaries(events: list[EventRow]) -> list[SessionSummary]: return sessions - def build_leaderboard(sessions: list[SessionSummary]) -> list[PlayerStats]: player_entries: dict[str, list[SessionEntry]] = defaultdict(list) for session in sessions: @@ -162,7 +173,9 @@ def build_leaderboard(sessions: list[SessionSummary]) -> list[PlayerStats]: break_even_sessions = sessions_played - winning_sessions - losing_sessions win_pct = (winning_sessions / sessions_played * 100) if sessions_played else 0.0 avg_win = round(sum(wins) / len(wins)) if wins else 0 - avg_loss = round(sum(abs(value) for value in losses) / len(losses)) if losses else 0 + avg_loss = ( + round(sum(abs(value) for value in losses) / len(losses)) if losses else 0 + ) biggest_win = max(wins) if wins else 0 biggest_loss = min(losses) if losses else 0 roi_pct = (total_net / total_buy_in * 100) if total_buy_in else 0.0 @@ -193,11 +206,14 @@ def build_leaderboard(sessions: list[SessionSummary]) -> list[PlayerStats]: return leaderboard - def cumulative_profit_series(sessions: list[SessionSummary]) -> dict[str, Any]: ordered_sessions = sorted(sessions, key=lambda session: session.session_date) player_names = sorted( - {entry.player_name for session in ordered_sessions for entry in session.entries}, + { + entry.player_name + for session in ordered_sessions + for entry in session.entries + }, key=str.casefold, ) @@ -211,7 +227,11 @@ def cumulative_profit_series(sessions: list[SessionSummary]) -> dict[str, Any]: for session in ordered_sessions: matching_entry = next( - (entry for entry in session.entries if entry.player_name == player_name), + ( + entry + for entry in session.entries + if entry.player_name == player_name + ), None, ) if matching_entry is not None: @@ -241,8 +261,9 @@ def cumulative_profit_series(sessions: list[SessionSummary]) -> dict[str, Any]: return {"labels": labels, "datasets": datasets} - -def player_session_series(sessions: list[SessionSummary], player_name: str) -> dict[str, Any]: +def player_session_series( + sessions: list[SessionSummary], player_name: str +) -> dict[str, Any]: ordered_sessions = sorted(sessions, key=lambda session: session.session_date) labels: list[str] = [] net_values: list[float] = [] @@ -250,7 +271,11 @@ def player_session_series(sessions: list[SessionSummary], player_name: str) -> d running_total = 0 all_player_names = sorted( - {entry.player_name for session in ordered_sessions for entry in session.entries}, + { + entry.player_name + for session in ordered_sessions + for entry in session.entries + }, key=str.casefold, ) @@ -276,11 +301,9 @@ def player_session_series(sessions: list[SessionSummary], player_name: str) -> d } - def session_events(events: list[EventRow], session_date: str) -> list[EventRow]: return [event for event in events if event["session_date"] == session_date] - def unique_player_names(events: list[EventRow]) -> list[str]: return sorted({event["player_name"] for event in events}, key=str.casefold) diff --git a/templates/leaderboard.html b/templates/leaderboard.html index 13a66ec..4c15c24 100644 --- a/templates/leaderboard.html +++ b/templates/leaderboard.html @@ -66,6 +66,7 @@ + @@ -81,6 +82,15 @@ {% for player in leaderboard %} #{{ loop.index }} + + {% if player.rank_change > 0 %} + ↑ {{ player.rank_change }} + {% elif player.rank_change < 0 %} + ↓ {{ player.rank_change | abs }} + {% else %} + - + {% endif %} + {{ player.player_name }} {{ player.total_buy_in_cents | money }} {{ player.total_cash_out_cents | money }} @@ -93,7 +103,7 @@ {% else %} - No data yet. Add the first event from the admin page. + No data yet. Add the first event from the admin page. {% endfor %} @@ -254,15 +264,16 @@ const sortButtons = Array.from(document.querySelectorAll('.sort-button')); const columnIndexMap = { rank: 0, - player: 1, - buyins: 2, - cashouts: 3, - net: 4, - winpct: 5, - roi: 6, - avgwin: 7, - avgloss: 8, - sessions: 9, + rankdelta: 1, + player: 2, + buyins: 3, + cashouts: 4, + net: 5, + winpct: 6, + roi: 7, + avgwin: 8, + avgloss: 9, + sessions: 10, }; function updateRankCells() {