diff --git a/app/routers/__init__.py b/app/routers/__init__.py index c063896..0c0b83b 100644 --- a/app/routers/__init__.py +++ b/app/routers/__init__.py @@ -1,6 +1,6 @@ from fastapi import APIRouter -from . import base, decks, games, players, score, seed +from . import base, decks, games, players, score, seed, stats api_router = APIRouter(prefix="/api") html_router = APIRouter() @@ -10,10 +10,12 @@ api_router.include_router(players.api_router) api_router.include_router(games.api_router) api_router.include_router(score.api_router) api_router.include_router(seed.api_router) +api_router.include_router(stats.api_router) html_router.include_router(decks.html_router) html_router.include_router(players.html_router) html_router.include_router(games.html_router) html_router.include_router(seed.html_router) +html_router.include_router(stats.html_router) html_router.include_router(base.html_router) diff --git a/app/routers/stats.py b/app/routers/stats.py new file mode 100644 index 0000000..af804f3 --- /dev/null +++ b/app/routers/stats.py @@ -0,0 +1,59 @@ +from collections import defaultdict + +from fastapi import APIRouter, Depends, Request +from fastapi.responses import HTMLResponse +from sqlalchemy.sql.expression import func + +from app.sql import models + +from ..templates import jinja_templates +from ..sql.database import get_db + +api_router = APIRouter(prefix="/stats", tags=["stats"]) +html_router = APIRouter( + prefix="/stats", include_in_schema=False, default_response_class=HTMLResponse +) + + +@api_router.get("/graph") +def stats_graph_api(db=Depends(get_db)): + # TODO - parallelize? (Probably not worth it :P ) + + # SO Answer on row_number: https://stackoverflow.com/a/38160409/1040915 + # Docs: https://docs.sqlalchemy.org/en/20/core/sqlelement.html#sqlalchemy.sql.expression.over + row_number_column = ( + func.row_number() + .over( + partition_by=[models.Deck.name, models.Game.date], + order_by=models.EloScore.id.desc(), + ) + .label("row_number") + ) + sub_query = ( + db.query(models.Deck.name, models.EloScore.score, models.Game.date) + .outerjoin(models.EloScore, models.Deck.id == models.EloScore.deck_id) + .join(models.Game, models.EloScore.after_game_id == models.Game.id) + .add_column(row_number_column) + .subquery() + ) + query = db.query(sub_query).filter(sub_query.c.row_number == 1) + results = query.all() + + data_grouped_by_deck = defaultdict(list) + for result in results: + # TODO - how to index results by name instead of tuple-number + data_grouped_by_deck[result[0]].append( + {"score": result[1], "date": result[2].strftime("%Y-%m-%d")} + ) + + return { + "datasets": [ + {"label": key, "data": data_grouped_by_deck[key]} + for key in data_grouped_by_deck + ] + } + + +@html_router.get("/graph") +def stats_graph(request: Request, db=Depends(get_db)): + return jinja_templates.TemplateResponse(request, "stats/graph.html") diff --git a/app/static/css/base.css b/app/static/css/base.css index 550c147..f20984c 100644 --- a/app/static/css/base.css +++ b/app/static/css/base.css @@ -45,6 +45,38 @@ body { .topbar_item { margin: 0px 10px; + float: none !important; + display: inline; + position: relative; /* Necessary in order for dropdowns to be contained within them */ +} + +/* https://www.freecodecamp.org/news/how-to-build-a-dropdown-menu-with-javascript/*/ +.topbar_dropdown_button { + cursor: pointer; +} + +.topbar_dropdown { + visibility: hidden; + opacity: 0; + position: absolute; + top: 20px; + left: 0px; +} + +.topbar_dropdown.show { + transform: translateY(0rem); + visibility: visible; + opacity: 1; +} + +.topbar_dropdown a { + background-color: #0080ff; + display: block; + padding: 4px; +} + +.topbar_dropdown a:last-child { + border-radius: 0px 0px 10px 10px; } #header div#create_game_button { diff --git a/app/static/js/base.js b/app/static/js/base.js index 491bfa0..ad6c367 100644 --- a/app/static/js/base.js +++ b/app/static/js/base.js @@ -1,3 +1,11 @@ $.ajaxSetup({ contentType: "application/json; charset=utf-8" -}); \ No newline at end of file +}); + +$(document).ready(function() { + $('.topbar_dropdown_button').click(function() { + console.log('clicked') + console.log(this); + $(this).children('.topbar_dropdown').toggleClass('show'); + }); +}) \ No newline at end of file diff --git a/app/static/js/stats/graph.js b/app/static/js/stats/graph.js new file mode 100644 index 0000000..230547e --- /dev/null +++ b/app/static/js/stats/graph.js @@ -0,0 +1,39 @@ +$(document).ready(function() { + const data = [ + { year: 2010, count: 10 }, + { year: 2011, count: 20 }, + { year: 2012, count: 15 }, + { year: 2013, count: 25 }, + { year: 2014, count: 22 }, + { year: 2015, count: 30 }, + { year: 2016, count: 28 }, + ]; + + fetch('/api/stats/graph') + .then(response => response.json()) + .then(response => { + console.log(response.datasets); + new Chart( + document.getElementById('graph_canvas'), + { + type: 'line', + data: { + datasets: response.datasets + }, + options: { + scales: { + x: { + type: 'time' + } + }, + parsing: { + xAxisKey: 'date', + yAxisKey: 'score' + } + } + + } + ); + }); + +}); diff --git a/app/templates/base.html b/app/templates/base.html index 9579ec7..50b4aed 100644 --- a/app/templates/base.html +++ b/app/templates/base.html @@ -7,6 +7,7 @@ src="https://code.jquery.com/jquery-3.7.1.min.js" integrity="sha256-/JqT3SQfawRcv/BIHPThkBvs0OEvtFFmqPF/lYI/Cxo=" crossorigin="anonymous"> + {% endblock %} @@ -19,6 +20,12 @@ Games Decks Players +
+