From 7dea88a1b38d6300592ae04766b566c27b4ca9c8 Mon Sep 17 00:00:00 2001 From: Brad Brown Date: Mon, 27 Jun 2022 14:18:11 -0500 Subject: [PATCH] add leaderboard race lines --- wordlinator/utils/scores.py | 26 ++++++++++++++++ wordlinator/web/__init__.py | 59 ++++++++++++++++++++++++++++++++++++- 2 files changed, 84 insertions(+), 1 deletion(-) diff --git a/wordlinator/utils/scores.py b/wordlinator/utils/scores.py index 0148fae..7ee2e16 100644 --- a/wordlinator/utils/scores.py +++ b/wordlinator/utils/scores.py @@ -1,4 +1,5 @@ import collections +import itertools import typing import wordlinator.db.pg @@ -76,6 +77,15 @@ class UserRow(ScoreRow): def golf_score(self) -> int: return self.total - (self.count * 4) + @property + def progressive_score_list(self) -> typing.List[int]: + score_progress = list( + itertools.accumulate( + self.sorted_scores(), func=lambda t, e: t + (e.score - 4), initial=0 + ) + )[1:] + return score_progress + def sorted_scores(self): yield from sorted(self._scores, key=lambda s: s.hole_id.hole) @@ -189,3 +199,19 @@ class ScoreMatrix(ScoreContainer): def user_rows(self, wordle_day): hole_no = wordle_day.golf_hole.hole_no return [u.user_row(hole_no=hole_no) for u in self.by_user().values()] + + def top_by_day(self): + user_dict = {u: r.progressive_score_list for u, r in self.by_user().items()} + days = max(map(len, user_dict.values())) + + rankings = collections.defaultdict(list) + for day_idx in range(days): + day_scores = { + u: v[day_idx] for u, v in user_dict.items() if len(v) >= day_idx + 1 + } + tops = [(u, v) for u, v in sorted(day_scores.items(), key=lambda t: t[1])][ + :20 + ] + for (user, score) in tops: + rankings[user].append((day_idx + 1, score)) + return rankings diff --git a/wordlinator/web/__init__.py b/wordlinator/web/__init__.py index 0a6f297..b5696e8 100644 --- a/wordlinator/web/__init__.py +++ b/wordlinator/web/__init__.py @@ -1,3 +1,4 @@ +import collections import functools import os import pathlib @@ -16,7 +17,7 @@ import wordlinator.utils import wordlinator.utils.scores import wordlinator.utils.web -TTL_TIME = 30 if os.getenv("DEBUG") else 600 +TTL_TIME = 30 if os.getenv("DEBUG") else 90 LEADERBOARD_COUNT = 20 ################### @@ -251,6 +252,38 @@ def get_line_graph(round_id): return dash.dcc.Graph(figure=figure) +##################### +# Line Race Helpers # +##################### + + +def line_race_graph(round_id): + score_matrix = scores_from_db(round_id) + tops_by_day = score_matrix.top_by_day() + + figure = plotly.graph_objs.Figure() + figure.update_yaxes(autorange="reversed") + figure.update_xaxes(tickmode="linear", tick0=1, dtick=1) + annotation_names = collections.defaultdict(list) + for name, entries in tops_by_day.items(): + figure.add_trace( + plotly.graph_objs.Scatter( + name=name, + mode="lines+markers", + x=[e[0] for e in entries], + y=[e[1] for e in entries], + ) + ) + annotation_names[entries[-1]].append(name) + + annotations = [ + {"x": k[0], "y": k[1], "text": ", ".join(v)} + for k, v in annotation_names.items() + ] + figure.update_layout(annotations=annotations) + return dash.dcc.Graph(figure=figure) + + ############# # App Setup # ############# @@ -277,6 +310,18 @@ app.layout = dash.html.Div( ), ] ), + dash.html.Div( + [ + dash.html.H2( + f"Leaderboard - Top {LEADERBOARD_COUNT}", + style={"textAlign": "center"}, + ), + dash.dcc.Loading( + id="leaderboard-race-loading", + children=dash.html.Div("Loading...", id="leaderboard-race"), + ), + ] + ), dash.html.Div( [ dash.html.H2("User Scores", style={"textAlign": "center"}), @@ -320,6 +365,18 @@ def get_leaderboard_table(_, round_id): return get_leaderboard(round_id) +@app.long_callback( + output=dash.dependencies.Output("leaderboard-race", "children"), + inputs=[ + dash.dependencies.Input("title", "children"), + dash.dependencies.Input("round-selector-dropdown", "value"), + ], + manager=long_callback_manager, +) +def get_leaderboard_race(_, round_id): + return line_race_graph(round_id) + + @app.long_callback( output=dash.dependencies.Output("user-scores", "children"), inputs=[