Files
pub-quiz/AGENTS.md
John Gatward 80a66bc44c enhanced
2026-03-19 21:43:34 +00:00

3.8 KiB
Raw Blame History

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.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 01, 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

  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

  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

[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.