import typing from collections import Counter, defaultdict from colorsys import hsv_to_rgb from datetime import datetime, timedelta, date from math import tau from bokeh.colors import RGB from bokeh.embed import components from bokeh.models import HoverTool from bokeh.plotting import figure, ColumnDataSource from bokeh.resources import INLINE 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(known_games: typing.Set[str]) -> typing.Tuple[typing.Counter[str], typing.DefaultDict[datetime.date, typing.Dict[str, int]]]: games_played = defaultdict(Counter) total_games = Counter() cutoff = date.today() - timedelta(days=30) room: Room for room in select(room for room in Room if room.creation_time >= cutoff): for slot in room.seed.slots: if slot.game in known_games: total_games[slot.game] += 1 games_played[room.creation_time.date()][slot.game] += 1 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 def stats(): from worlds import network_data_package known_games = set(network_data_package["games"]) 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=PLOT_WIDTH, height=500) total_games, games_played = get_db_data(known_games) days = sorted(games_played) 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=game_to_color[game]) total = sum(total_games.values()) pie = figure(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=PLOT_WIDTH, height=500, x_range=(-0.5, 1.2)) pie.axis.visible = False pie.xgrid.visible = False pie.ygrid.visible = False data = { "games": [], "count": [], "start_angles": [], "end_angles": [], } current_angle = 0 for i, (game, count) in enumerate(total_games.most_common()): data["games"].append(game) data["count"].append(count) data["start_angles"].append(current_angle) angle = count / total * tau current_angle += angle data["end_angles"].append(current_angle) 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") per_game_charts = [create_game_played_figure(games_played, game, game_to_color[game]) for game in sorted(total_games, key=lambda game: total_games[game]) 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)