enhanced
This commit is contained in:
59
AGENTS.md
59
AGENTS.md
@@ -1,45 +1,62 @@
|
||||
# AGENTS.md — Pub Quiz Dashboard
|
||||
# AGENTS.md — The Hope Pub Quiz Dashboard
|
||||
|
||||
## Overview
|
||||
Single-page Flask dashboard that reads `data.csv` (one row per quiz night) and renders statistics, a player table, and Plotly charts via a Jinja2 template. No database — the CSV is the sole data store.
|
||||
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
|
||||
```bash
|
||||
uv sync # install dependencies (uses uv.lock / pyproject.toml)
|
||||
cd src && python app.py # start Flask dev server at http://127.0.0.1:5000
|
||||
uv sync # install deps from uv.lock / pyproject.toml
|
||||
PYTHONPATH=src python src/app.py # run from project root — resolves data.csv correctly
|
||||
```
|
||||
> **Critical:** `data.csv` is read with a bare relative path (`"data.csv"`), so the app **must** be launched from the `src/` directory, where Flask's CWD resolves to the project root via the relative path `../data.csv` — actually, `data.csv` sits at project root and `src/` is the CWD, so Flask resolves it as `../data.csv` would fail. The app reads `data.csv` directly, so run from `src/` only after confirming the path resolves (currently works because Flask's CWD is `src/` and `data.csv` is read as a sibling — verify if moving files).
|
||||
> **Path gotcha:** `app.py` reads `data.csv` with a bare relative path, so the CWD must be the **project root**. `PYTHONPATH=src` puts `src/` 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, where 0=1st, 1=last), `Number of Players`, `Number of Teams`, `Points on Scattergories`, then one binary column per player (1=attended, 0=absent).
|
||||
- Date parsing is always `dayfirst=True`; the DataFrame is sorted by date on load.
|
||||
- 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 routes, all Plotly figure generation, data loading |
|
||||
| `src/stats.py` | Summary statistics dict (streaks, averages) passed to template as `stats` |
|
||||
| `src/player_table.py` | List-of-lists table `[header, ...rows..., footer]`; cost fixed at **£3/quiz** per player |
|
||||
| `src/constants.py` | Single source of truth for player names, regression features, and colour scheme |
|
||||
| `src/templates/index.html` | Renders `stats` dict, `player_table` list, and `plots` dict of JSON-serialised Plotly figures |
|
||||
| `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
|
||||
1. Update `constants.PLAYER_NAME_COLUMNS` (ordered list — controls display order).
|
||||
2. Update `constants.FEATURE_COLUMNS` (set — controls regression inputs).
|
||||
3. Add the new column to `data.csv` with `0`/`1` values.
|
||||
1. Update `constants.PLAYER_NAME_COLUMNS` (ordered list — controls display order everywhere).
|
||||
2. Update `constants.FEATURE_COLUMNS` (set — controls which columns feed the regression model).
|
||||
3. 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` — plain `dict` of human-readable `label: value` pairs; rendered as a secondary list.
|
||||
|
||||
The `index()` route unpacks both: `stats, highlights = generate_stats(df)`.
|
||||
|
||||
### Plots Pipeline
|
||||
Figures are built with Plotly in `app.py`, serialised to JSON with `plotly.utils.PlotlyJSONEncoder`, stored in a `plots` dict keyed by a snake_case name, and rendered client-side via `Plotly.newPlot("{{ key }}", figure.data, figure.layout)` in the template. The dict key becomes both the HTML `id` and the JS variable target — keep keys unique and valid as HTML ids.
|
||||
1. Build a Plotly figure in `app.py` using `Relative Position` directly (or `Relative Position * 100` for percentile display), where **lower = better**.
|
||||
2. Serialise: `json.dumps(fig, cls=plotly.utils.PlotlyJSONEncoder)`.
|
||||
3. Store in the `plots` dict under a **snake_case key** (e.g. `"position_trend"`).
|
||||
4. 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
|
||||
A flat list-of-lists: index `[0]` = header row, `[1:-1]` = data rows, `[-1]` = footer row. The template iterates `player_table[1:]` for `<tbody>`, so the footer is rendered as a regular row — style it in CSS if distinction is needed.
|
||||
`[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` Semantics
|
||||
Lower is better (0 = first place). The trendline in `generate_relative_position_over_time` extrapolates towards `target_value = 0.08` (≈top 8%). Adjust this constant to change the goal projection.
|
||||
### `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 and Plotly are loaded from CDN in `index.html`. All styling is inline Tailwind utility classes.
|
||||
|
||||
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.
|
||||
|
||||
Reference in New Issue
Block a user