Add filtering on graph

This commit is contained in:
Jack Jackson 2024-07-29 20:16:01 -07:00
parent dee0c26260
commit 3b1c3d7eb3
5 changed files with 86 additions and 4 deletions

View File

@ -1,3 +1,5 @@
from collections import defaultdict
from fastapi import APIRouter, Depends, HTTPException, Request from fastapi import APIRouter, Depends, HTTPException, Request
from fastapi.responses import HTMLResponse from fastapi.responses import HTMLResponse
from sqlalchemy import and_, or_, func from sqlalchemy import and_, or_, func
@ -9,7 +11,7 @@ from ..templates import jinja_templates
from ..sql import crud, schemas from ..sql import crud, schemas
from ..sql.database import get_db from ..sql.database import get_db
from .players import list_players from .players import read_player, list_players
from .games import _render_game_participants from .games import _render_game_participants
api_router = APIRouter(prefix="/deck", tags=["deck"]) api_router = APIRouter(prefix="/deck", tags=["deck"])
@ -23,6 +25,19 @@ html_router = APIRouter(
######## ########
@api_router.get("/by_player")
def decks_by_player(db=Depends(get_db)):
decks = crud.get_decks(db, skip=0, limit=-1)
# TODO - could probably do this directly in-db with some fancy use of `group_concat`
return_value = defaultdict(list)
for deck in decks:
# TODO - if we cared about latency, could cache this
return_value[read_player(deck.owner_id, db).name].append(
{"id": deck.id, "name": deck.name}
)
return return_value
@api_router.post("/", response_model=schemas.Deck, status_code=201) @api_router.post("/", response_model=schemas.Deck, status_code=201)
def create_deck(deck: schemas.DeckCreate, db: Session = Depends(get_db)): def create_deck(deck: schemas.DeckCreate, db: Session = Depends(get_db)):
db_player = crud.get_player_by_id(db, deck.owner_id) db_player = crud.get_player_by_id(db, deck.owner_id)

View File

@ -1,4 +1,5 @@
from collections import defaultdict from collections import defaultdict
from datetime import datetime, MINYEAR
from typing import Optional from typing import Optional
from fastapi import APIRouter, Depends, Request from fastapi import APIRouter, Depends, Request
@ -17,7 +18,11 @@ html_router = APIRouter(
@api_router.get("/graph") @api_router.get("/graph")
def stats_graph_api(deck_ids: Optional[str] = None, db=Depends(get_db)): def stats_graph_api(
deck_ids: Optional[str] = None,
normalize_final_datapoint: bool = False,
db=Depends(get_db),
):
# TODO - parallelize? (Probably not worth it :P ) # TODO - parallelize? (Probably not worth it :P )
# SO Answer on row_number: https://stackoverflow.com/a/38160409/1040915 # SO Answer on row_number: https://stackoverflow.com/a/38160409/1040915
@ -44,12 +49,26 @@ def stats_graph_api(deck_ids: Optional[str] = None, db=Depends(get_db)):
results = query.all() results = query.all()
data_grouped_by_deck = defaultdict(list) data_grouped_by_deck = defaultdict(list)
latest_date_so_far = datetime(MINYEAR, 1, 1, 0, 0, 0, 0)
for result in results: for result in results:
# TODO - how to index results by name instead of tuple-number # TODO - how to index results by name instead of tuple-number
date = result[2]
latest_date_so_far = max(latest_date_so_far, date)
data_grouped_by_deck[result[0]].append( data_grouped_by_deck[result[0]].append(
{"score": result[1], "date": result[2].strftime("%Y-%m-%d")} {"score": result[1], "date": date.strftime("%Y-%m-%d")}
) )
if normalize_final_datapoint:
# Add a fake final datapoint to the series for any decks that weren't played in the latest game, so that lines
# continue all the way to the end of the graph
latest_date_formatted = latest_date_so_far.strftime("%Y-%m-%d")
print(f"DEBUG = {latest_date_formatted=}")
for games in data_grouped_by_deck.values():
if games[-1]["date"] != latest_date_formatted:
games.append(
{"score": games[-1]["score"], "date": latest_date_formatted}
)
return { return {
"datasets": [ "datasets": [
{"label": key, "data": data_grouped_by_deck[key]} {"label": key, "data": data_grouped_by_deck[key]}

5
app/static/css/graph.css Normal file
View File

@ -0,0 +1,5 @@
#options div {
float: left;
margin: 2px;
background-color: lightblue;
}

View File

@ -1,9 +1,35 @@
function updateGraphWithFilter() {
filterString = $('#options input[type=checkbox]:checked').map((idx, elem) => {
console.log('Adding ' + elem.id + ' to return string')
return elem.id
}).get().join(',')
console.log(filterString);
fetch('/api/stats/graph?deck_ids=' + filterString)
.then(response => response.json())
.then(response => {
window.chart.data.datasets = response.datasets
window.chart.update()
})
}
$(document).ready(function() { $(document).ready(function() {
$('#filter_button').click(updateGraphWithFilter)
fetch('/api/deck/by_player')
.then(response => response.json())
.then(response => {
console.log(response);
for (playerName in response) {
buildPlayerDecksDiv($('#options'), playerName, response[playerName])
}
})
fetch('/api/stats/graph') fetch('/api/stats/graph')
.then(response => response.json()) .then(response => response.json())
.then(response => { .then(response => {
console.log(response.datasets); console.log(response.datasets);
new Chart( window.chart = new Chart(
document.getElementById('graph_canvas'), document.getElementById('graph_canvas'),
{ {
type: 'line', type: 'line',
@ -27,3 +53,14 @@ $(document).ready(function() {
}); });
}); });
function buildPlayerDecksDiv(parentDiv, playerName, playerDecks) {
div = $('<div>',
id='player_div_for_' + playerName
)
div.append('<p>' + playerName + '</p>')
for (deck of playerDecks) {
div.append('<input type="checkbox" id="' + deck["id"] + '" name="' + deck["name"] + '" value="' + deck["name"] + '"><label for="' + deck["name"] + '">' + deck["name"] + '</label><br/>')
}
div.appendTo(parentDiv)
}

View File

@ -8,8 +8,14 @@
<script src="https://cdn.jsdelivr.net/npm/moment@2.27.0"></script> <script src="https://cdn.jsdelivr.net/npm/moment@2.27.0"></script>
<script src="https://cdn.jsdelivr.net/npm/chartjs-adapter-moment@0.1.1"></script> <script src="https://cdn.jsdelivr.net/npm/chartjs-adapter-moment@0.1.1"></script>
<script src="/static/js/stats/graph.js"></script> <script src="/static/js/stats/graph.js"></script>
<link rel="stylesheet" href="/static/css/graph.css"/>
{% endblock %} {% endblock %}
{% block content %} {% block content %}
<div id="wrapper" style="width:95%;margin-left:10px;"><canvas id="graph_canvas"></canvas></div> <div id="wrapper" style="width:95%;margin-left:10px;"><canvas id="graph_canvas"></canvas></div>
<div id="options">
<h2>Filter</h2>
<button id="filter_button">Go!</button><br/>
</div>
{% endblock %} {% endblock %}