add rank delta to leaderboard
This commit is contained in:
1 parent
b21a5dc002
commit
7648bf6e6a
4 files changed
+102
-34
No files matched your search
@@ -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,
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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
@@ -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() {
|
||||||
|
|||||||
Reference in new issue
Block a user