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:
parent
4565b3af8d
commit
e849e4792d
|
@ -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)
|
||||||
|
|
Loading…
Reference in New Issue