add rank delta to leaderboard

This commit is contained in:
SowinskiBraeden committed 2026-03-20 23:28:22 -07:00
1 parent b21a5dc002
commit 7648bf6e6a
4 files changed
+102 -34

No files matched your search

+19 -10
View File
@@ -7,6 +7,7 @@ from pathlib import Path
from flask import Flask, flash, redirect, render_template, request, session, url_for from flask import Flask, flash, redirect, render_template, request, session, url_for
from stats import ( from stats import (
apply_rank_changes,
build_leaderboard, build_leaderboard,
build_session_summaries, build_session_summaries,
cents_to_dollars, cents_to_dollars,
@@ -71,22 +72,30 @@ def home() -> str:
def leaderboard() -> str: def leaderboard() -> str:
events = load_events(DATA_PATH) events = load_events(DATA_PATH)
all_sessions = build_session_summaries(events) 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() 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: if session_dates:
selected_session_date = "" 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 filtered_sessions = ordered_sessions[: cutoff_index + 1]
if selected_session_date: previous_sessions = ordered_sessions[:cutoff_index]
filtered_sessions = [ else:
session filtered_sessions = []
for session in all_sessions previous_sessions = []
if session.session_date <= selected_session_date
]
board = build_leaderboard(filtered_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) chart_data = cumulative_profit_series(filtered_sessions)
return render_template( return render_template(
"leaderboard.html", "leaderboard.html",
leaderboard=board, leaderboard=board,
+25
View File
@@ -489,6 +489,31 @@ h2 {
letter-spacing: -0.06em; 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 { .table-wrap {
position: relative; position: relative;
z-index: 1; z-index: 1;
+37 -14
View File
@@ -71,15 +71,30 @@ class PlayerStats:
total_cash_out_cents: int total_cash_out_cents: int
total_net_cents: int total_net_cents: int
roi_pct: float 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: def cents_to_dollars(cents: int) -> str:
value = cents / 100 value = cents / 100
return f"${value:,.2f}" return f"${value:,.2f}"
def safe_date_label(session_date: str) -> str: def safe_date_label(session_date: str) -> str:
try: try:
return datetime.strptime(session_date, "%Y-%m-%d").strftime("%b %d, %Y") 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 return session_date
def color_for_name(name: str, names: list[str]) -> str: def color_for_name(name: str, names: list[str]) -> str:
try: try:
index = sorted(names, key=str.casefold).index(name) 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)] return PLAYER_PALETTE[index % len(PLAYER_PALETTE)]
def net_tone(value_cents: int) -> str: def net_tone(value_cents: int) -> str:
if value_cents > 0: if value_cents > 0:
return "#22c55e" return "#22c55e"
@@ -105,7 +118,6 @@ def net_tone(value_cents: int) -> str:
return "#f59e0b" return "#f59e0b"
def build_session_summaries(events: list[EventRow]) -> list[SessionSummary]: def build_session_summaries(events: list[EventRow]) -> list[SessionSummary]:
grouped: dict[tuple[str, str], SessionEntry] = {} grouped: dict[tuple[str, str], SessionEntry] = {}
@@ -141,7 +153,6 @@ def build_session_summaries(events: list[EventRow]) -> list[SessionSummary]:
return sessions return sessions
def build_leaderboard(sessions: list[SessionSummary]) -> list[PlayerStats]: def build_leaderboard(sessions: list[SessionSummary]) -> list[PlayerStats]:
player_entries: dict[str, list[SessionEntry]] = defaultdict(list) player_entries: dict[str, list[SessionEntry]] = defaultdict(list)
for session in sessions: 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 break_even_sessions = sessions_played - winning_sessions - losing_sessions
win_pct = (winning_sessions / sessions_played * 100) if sessions_played else 0.0 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_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_win = max(wins) if wins else 0
biggest_loss = min(losses) if losses 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 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 return leaderboard
def cumulative_profit_series(sessions: list[SessionSummary]) -> dict[str, Any]: def cumulative_profit_series(sessions: list[SessionSummary]) -> dict[str, Any]:
ordered_sessions = sorted(sessions, key=lambda session: session.session_date) ordered_sessions = sorted(sessions, key=lambda session: session.session_date)
player_names = sorted( 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, key=str.casefold,
) )
@@ -211,7 +227,11 @@ def cumulative_profit_series(sessions: list[SessionSummary]) -> dict[str, Any]:
for session in ordered_sessions: for session in ordered_sessions:
matching_entry = next( 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, None,
) )
if matching_entry is not 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} return {"labels": labels, "datasets": datasets}
def player_session_series(
def player_session_series(sessions: list[SessionSummary], player_name: str) -> dict[str, Any]: sessions: list[SessionSummary], player_name: str
) -> dict[str, Any]:
ordered_sessions = sorted(sessions, key=lambda session: session.session_date) ordered_sessions = sorted(sessions, key=lambda session: session.session_date)
labels: list[str] = [] labels: list[str] = []
net_values: list[float] = [] net_values: list[float] = []
@@ -250,7 +271,11 @@ def player_session_series(sessions: list[SessionSummary], player_name: str) -> d
running_total = 0 running_total = 0
all_player_names = sorted( 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, 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]: def session_events(events: list[EventRow], session_date: str) -> list[EventRow]:
return [event for event in events if event["session_date"] == session_date] return [event for event in events if event["session_date"] == session_date]
def unique_player_names(events: list[EventRow]) -> list[str]: def unique_player_names(events: list[EventRow]) -> list[str]:
return sorted({event["player_name"] for event in events}, key=str.casefold) return sorted({event["player_name"] for event in events}, key=str.casefold)
+21 -10
View File
@@ -66,6 +66,7 @@
<thead> <thead>
<tr> <tr>
<th><button type="button" class="sort-button" data-sort-key="rank" data-sort-type="number" data-initial-direction="asc">Rank <span class="sort-indicator"></span></button></th> <th><button type="button" class="sort-button" data-sort-key="rank" data-sort-type="number" data-initial-direction="asc">Rank <span class="sort-indicator"></span></button></th>
<th><button type="button" class="sort-button" data-sort-key="rankdelta" data-sort-type="number" data-initial-direction="desc">Rank Δ <span class="sort-indicator"></span></button></th>
<th><button type="button" class="sort-button" data-sort-key="player" data-sort-type="string" data-initial-direction="asc">Player <span class="sort-indicator"></span></button></th> <th><button type="button" class="sort-button" data-sort-key="player" data-sort-type="string" data-initial-direction="asc">Player <span class="sort-indicator"></span></button></th>
<th><button type="button" class="sort-button" data-sort-key="buyins" data-sort-type="number" data-initial-direction="desc">Buy-ins <span class="sort-indicator"></span></button></th> <th><button type="button" class="sort-button" data-sort-key="buyins" data-sort-type="number" data-initial-direction="desc">Buy-ins <span class="sort-indicator"></span></button></th>
<th><button type="button" class="sort-button" data-sort-key="cashouts" data-sort-type="number" data-initial-direction="desc">Cash-outs <span class="sort-indicator"></span></button></th> <th><button type="button" class="sort-button" data-sort-key="cashouts" data-sort-type="number" data-initial-direction="desc">Cash-outs <span class="sort-indicator"></span></button></th>
@@ -81,6 +82,15 @@
{% for player in leaderboard %} {% for player in leaderboard %}
<tr data-player-row="true" data-original-rank="{{ loop.index }}"> <tr data-player-row="true" data-original-rank="{{ loop.index }}">
<td data-sort-value="{{ loop.index }}" data-rank-cell>#{{ loop.index }}</td> <td data-sort-value="{{ loop.index }}" data-rank-cell>#{{ loop.index }}</td>
<td data-sort-value="{{ player.rank_change }}">
{% if player.rank_change > 0 %}
<span class="rank-move rank-up">↑ {{ player.rank_change }}</span>
{% elif player.rank_change < 0 %}
<span class="rank-move rank-down">↓ {{ player.rank_change | abs }}</span>
{% else %}
<span class="rank-move rank-neutral">-</span>
{% endif %}
</td>
<td data-sort-value="{{ player.player_name|lower }}"><a href="{{ url_for('player_detail', player_name=player.player_name) }}">{{ player.player_name }}</a></td> <td data-sort-value="{{ player.player_name|lower }}"><a href="{{ url_for('player_detail', player_name=player.player_name) }}">{{ player.player_name }}</a></td>
<td data-sort-value="{{ player.total_buy_in_cents }}">{{ player.total_buy_in_cents | money }}</td> <td data-sort-value="{{ player.total_buy_in_cents }}">{{ player.total_buy_in_cents | money }}</td>
<td data-sort-value="{{ player.total_cash_out_cents }}">{{ player.total_cash_out_cents | money }}</td> <td data-sort-value="{{ player.total_cash_out_cents }}">{{ player.total_cash_out_cents | money }}</td>
@@ -93,7 +103,7 @@
</tr> </tr>
{% else %} {% else %}
<tr> <tr>
<td colspan="10">No data yet. Add the first event from the admin page.</td> <td colspan="11">No data yet. Add the first event from the admin page.</td>
</tr> </tr>
{% endfor %} {% endfor %}
</tbody> </tbody>
@@ -254,15 +264,16 @@
const sortButtons = Array.from(document.querySelectorAll('.sort-button')); const sortButtons = Array.from(document.querySelectorAll('.sort-button'));
const columnIndexMap = { const columnIndexMap = {
rank: 0, rank: 0,
player: 1, rankdelta: 1,
buyins: 2, player: 2,
cashouts: 3, buyins: 3,
net: 4, cashouts: 4,
winpct: 5, net: 5,
roi: 6, winpct: 6,
avgwin: 7, roi: 7,
avgloss: 8, avgwin: 8,
sessions: 9, avgloss: 9,
sessions: 10,
}; };
function updateRankCells() { function updateRankCells() {