3.8 KiB
AGENTS.md — The Hope Pub Quiz Dashboard
Overview
Single-page Flask dashboard that reads data.csv (one row per quiz night) and renders summary statistics, a player cost table, and five Plotly charts via a Jinja2 template. No database — the CSV is the sole data store.
Running the App
uv sync # install deps from uv.lock / pyproject.toml
PYTHONPATH=src python src/app.py # run from project root — resolves data.csv correctly
Path gotcha:
app.pyreadsdata.csvwith a bare relative path, so the CWD must be the project root.PYTHONPATH=srcputssrc/on the import path so local modules resolve.
Data Shape (data.csv)
- One row = one quiz night
- Columns:
Date(DD/MM/YYYY),Absolute Position,Relative Position(float 0–1, 0=1st/best, 1=last),Number of Players,Number of Teams,Points on Scattergories, then one binary column per player (1=attended, 0=absent). - Date parsing uses
dayfirst=True; the DataFrame is sorted ascending by date on every load.
Module Responsibilities
| File | Role |
|---|---|
src/app.py |
Flask route, five Plotly chart builders, data loading |
src/stats.py |
generate_stats(df) → (stats_dict, highlights_list) tuple |
src/player_table.py |
generate_player_table(df) → flat list-of-lists; cost hard-coded at £3/quiz |
src/constants.py |
Player names, regression features, colour scheme, ordinal(n) helper |
src/templates/index.html |
Renders highlights list, stats dict, player_table, and plots dict |
Key Conventions
Adding/Removing a Player
- Update
constants.PLAYER_NAME_COLUMNS(ordered list — controls display order everywhere). - Update
constants.FEATURE_COLUMNS(set — controls which columns feed the regression model). - Add the new binary column to
data.csv.
generate_stats Return Value
Returns a tuple (stats, highlights):
highlights— list of{"label": str, "value": str, "detail": str}dicts; rendered as 6 KPI cards.stats— plaindictof human-readablelabel: valuepairs; rendered as a secondary list.
The index() route unpacks both: stats, highlights = generate_stats(df).
Plots Pipeline
- Build a Plotly figure in
app.pyusingRelative Positiondirectly (orRelative Position * 100for percentile display), where lower = better. - Serialise:
json.dumps(fig, cls=plotly.utils.PlotlyJSONEncoder). - Store in the
plotsdict under a snake_case key (e.g."position_trend"). - The template renders every entry automatically:
Plotly.newPlot("{{ key }}", ...)— key is both the<div id>and JS target.
Current charts (in render order): position_trend, player_impact, scattergories_vs_position, player_participation, calendar.
player_table Structure
[0] = header row, [1:-1] = data rows sorted by appearances descending, [-1] = totals footer. The template uses player_table[1:-1] for <tbody> and player_table[-1] for <tfoot>.
Relative Position Convention
Raw data stores Relative Position (0=best). The dashboard keeps this convention everywhere: lower values are better in stats, tables, and chart labels. If a chart uses percentile text, it is Relative Position * 100 (still lower = better).
ordinal(n) Helper
Lives in constants.py. Returns e.g. "1st", "22nd", "63rd". Import where needed: from constants import ordinal.
Player Impact Chart
Shows average relative percentile when each player attends. Only players with >= 3 appearances are shown (MIN_APPEARANCES = 3 in generate_player_impact). Green bar = below overall average (better); red = above (worse).
Frontend
No build step. Tailwind CSS (cdn.tailwindcss.com) and Plotly (cdn.plot.ly/plotly-3.0.1.min.js) loaded from CDN. Charts use { responsive: true, displayModeBar: false } config.