diff --git a/WebHostLib/stats.py b/WebHostLib/stats.py index 131f807d2..a647be5ee 100644 --- a/WebHostLib/stats.py +++ b/WebHostLib/stats.py @@ -1,20 +1,24 @@ from collections import Counter, defaultdict -from itertools import cycle +from colorsys import hsv_to_rgb from datetime import datetime, timedelta, date from math import tau +import typing 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.resources import INLINE +from bokeh.colors import RGB from flask import render_template from pony.orm import select from . import app, cache 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) total_games = Counter() cutoff = date.today()-timedelta(days=30) @@ -26,29 +30,72 @@ def get_db_data(): 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') -@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(): 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() 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): occurences = [] for day in days: occurences.append(games_played[day][game]) 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()) 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")], - 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.xgrid.visible = False + pie.ygrid.visible = False data = { "games": [], @@ -65,12 +112,15 @@ def stats(): current_angle += angle data["end_angles"].append(current_angle) - data["colors"] = [element[1] for element in sorted((game, color) for game, color in - zip(data["games"], cycle(palette)))] - pie.wedge(x=0.5, y=0.5, radius=0.5, + data["colors"] = [game_to_color[game] for game in data["games"]] + + pie.wedge(x=0, y=0, radius=0.5, start_angle="start_angles", end_angle="end_angles", fill_color="colors", 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(), chart_data=script, charts=charts)