WebHost: games played per day plot per game on stats page (#827)

* WebHost: generate stats page palette for maximum hue difference between neighbours.

* WebHost: add per game played stats
This commit is contained in:
Fabian Dill 2022-07-27 23:36:20 +02:00 committed by GitHub
parent 4565b3af8d
commit e849e4792d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
1 changed files with 62 additions and 12 deletions

View File

@ -1,20 +1,24 @@
from collections import Counter, defaultdict from collections import Counter, defaultdict
from itertools import cycle from colorsys import hsv_to_rgb
from datetime import datetime, timedelta, date from datetime import datetime, timedelta, date
from math import tau from math import tau
import typing
from bokeh.embed import components from bokeh.embed import components
from bokeh.palettes import Dark2_8 as palette from bokeh.models import HoverTool
from bokeh.plotting import figure, ColumnDataSource from bokeh.plotting import figure, ColumnDataSource
from bokeh.resources import INLINE from bokeh.resources import INLINE
from bokeh.colors import RGB
from flask import render_template from flask import render_template
from pony.orm import select from pony.orm import select
from . import app, cache from . import app, cache
from .models import Room from .models import Room
PLOT_WIDTH = 600
def get_db_data():
def get_db_data() -> typing.Tuple[typing.Dict[str, int], typing.Dict[datetime.date, typing.Dict[str, int]]]:
games_played = defaultdict(Counter) games_played = defaultdict(Counter)
total_games = Counter() total_games = Counter()
cutoff = date.today()-timedelta(days=30) cutoff = date.today()-timedelta(days=30)
@ -26,29 +30,72 @@ def get_db_data():
return total_games, games_played return total_games, games_played
def get_color_palette(colors_needed: int) -> typing.List[RGB]:
colors = []
# colors_needed +1 to prevent first and last color being too close to each other
colors_needed += 1
for x in range(0, 361, 360 // colors_needed):
# a bit of noise on value to add some luminosity difference
colors.append(RGB(*(val * 255 for val in hsv_to_rgb(x / 360, 0.8, 0.8 + (x / 1800)))))
# splice colors for maximum hue contrast.
colors = colors[::2] + colors[1::2]
return colors
def create_game_played_figure(all_games_data: typing.Dict[datetime.date, typing.Dict[str, int]],
game: str, color: RGB) -> figure:
occurences = []
days = [day for day, game_data in all_games_data.items() if game_data[game]]
for day in days:
occurences.append(all_games_data[day][game])
data = {
"days": [datetime.combine(day, datetime.min.time()) for day in days],
"played": occurences
}
plot = figure(
title=f"{game} Played Per Day", x_axis_type='datetime', x_axis_label="Date",
y_axis_label="Games Played", sizing_mode="scale_both", width=PLOT_WIDTH, height=500,
toolbar_location=None, tools="",
# setting legend to False seems broken in bokeh currently?
# legend=False
)
hover = HoverTool(tooltips=[("Date:", "@days{%F}"), ("Played:", "@played")], formatters={"@days": "datetime"})
plot.add_tools(hover)
plot.vbar(x="days", top="played", legend_label=game, color=color, source=ColumnDataSource(data=data), width=1)
return plot
@app.route('/stats') @app.route('/stats')
@cache.memoize(timeout=60*60) # regen once per hour should be plenty @cache.memoize(timeout=60 * 60) # regen once per hour should be plenty
def stats(): def stats():
plot = figure(title="Games Played Per Day", x_axis_type='datetime', x_axis_label="Date", plot = figure(title="Games Played Per Day", x_axis_type='datetime', x_axis_label="Date",
y_axis_label="Games Played", sizing_mode="scale_both", width=500, height=500) y_axis_label="Games Played", sizing_mode="scale_both", width=PLOT_WIDTH, height=500)
total_games, games_played = get_db_data() total_games, games_played = get_db_data()
days = sorted(games_played) days = sorted(games_played)
cyc_palette = cycle(palette) color_palette = get_color_palette(len(total_games))
game_to_color: typing.Dict[str, RGB] = {game: color for game, color in zip(total_games, color_palette)}
for game in sorted(total_games): for game in sorted(total_games):
occurences = [] occurences = []
for day in days: for day in days:
occurences.append(games_played[day][game]) occurences.append(games_played[day][game])
plot.line([datetime.combine(day, datetime.min.time()) for day in days], plot.line([datetime.combine(day, datetime.min.time()) for day in days],
occurences, legend_label=game, line_width=2, color=next(cyc_palette)) occurences, legend_label=game, line_width=2, color=game_to_color[game])
total = sum(total_games.values()) total = sum(total_games.values())
pie = figure(plot_height=350, title=f"Games Played in the Last 30 Days (Total: {total})", toolbar_location=None, pie = figure(plot_height=350, title=f"Games Played in the Last 30 Days (Total: {total})", toolbar_location=None,
tools="hover", tooltips=[("Game:", "@games"), ("Played:", "@count")], tools="hover", tooltips=[("Game:", "@games"), ("Played:", "@count")],
sizing_mode="scale_both", width=500, height=500) sizing_mode="scale_both", width=PLOT_WIDTH, height=500, x_range=(-0.5, 1.2))
pie.axis.visible = False pie.axis.visible = False
pie.xgrid.visible = False
pie.ygrid.visible = False
data = { data = {
"games": [], "games": [],
@ -65,12 +112,15 @@ def stats():
current_angle += angle current_angle += angle
data["end_angles"].append(current_angle) data["end_angles"].append(current_angle)
data["colors"] = [element[1] for element in sorted((game, color) for game, color in data["colors"] = [game_to_color[game] for game in data["games"]]
zip(data["games"], cycle(palette)))]
pie.wedge(x=0.5, y=0.5, radius=0.5, pie.wedge(x=0, y=0, radius=0.5,
start_angle="start_angles", end_angle="end_angles", fill_color="colors", start_angle="start_angles", end_angle="end_angles", fill_color="colors",
source=ColumnDataSource(data=data), legend_field="games") source=ColumnDataSource(data=data), legend_field="games")
script, charts = components((plot, pie)) per_game_charts = [create_game_played_figure(games_played, game, game_to_color[game]) for game in total_games
if total_games[game] > 1]
script, charts = components((plot, pie, *per_game_charts))
return render_template("stats.html", js_resources=INLINE.render_js(), css_resources=INLINE.render_css(), return render_template("stats.html", js_resources=INLINE.render_js(), css_resources=INLINE.render_css(),
chart_data=script, charts=charts) chart_data=script, charts=charts)