A Flask-based web application for visualizing and analyzing professional football matches using Opta event data and optical tracking data. The app provides real-time pitch control, player heatmaps, timeline events, Voronoi diagrams, and player enrichment via the Transfermarkt API.
- Multi-game management — load and switch between multiple matches via a folder browser; recent games are persisted in a history file
- Player heatmaps — per-player positional heatmaps generated from tracking data, with in-memory and on-disk caching and a background warmup system
- Pitch control timeline — raster-based (nearest-neighbour / Voronoi) territorial dominance computed across all available tracking frames
- Voronoi diagrams — spatial control visualisation rendered with
mplsoccer - Match timeline & events — Opta live event data exposed as a versioned JSON API
- Player enrichment — market values, nationalities, and biographical details fetched from a companion Transfermarkt microservice and cached locally
- Team colours — automatic colour selection based on team details for consistent visual identity across views
- Stats view — dedicated statistics page per match with team colour theming
project-root/
├── app.py # Main Flask application
├── games_history.json # Persisted list of recently loaded game paths
├── data/
│ └── cache/
│ └── heatmaps/ # On-disk heatmap PNG cache (per game / per player)
├── games/ # Default games root (optional, configurable)
│ └── <game-folder>/
│ ├── <*opta_match*.json> # Opta match file (events + lineup)
│ ├── tracking/ # One JSON file per tracking frame
│ ├── players_enrichments.json # Generated player enrichment cache
│ ├── teams_details.json # Generated team enrichment cache
│ └── transfermarkt_data.json # Raw Transfermarkt API response cache
└── templates/
└── stats.html # Stats page template
- Python 3.10+
- Flask
- NumPy / SciPy
- Matplotlib
- mplsoccer
- requests
Install dependencies:
pip install flask numpy scipy matplotlib mplsoccer requestsThe application is configured via environment variables:
| Variable | Default | Description |
|---|---|---|
DEFAULT_GAME |
(none) | Game ID to load on startup |
TRANSFERMARKT_API_URL |
http://localhost:8000/match-data |
URL of the Transfermarkt enrichment microservice |
PORT |
5000 |
HTTP port |
FLASK_DEBUG |
1 |
Enable Flask debug mode (1 = on) |
python app.pyThe server starts on http://0.0.0.0:<PORT> (default: 5000).
In a Docker container, the host binding ensures port-forwarding works out of the box.
A JSON file whose name contains opta_match, placed inside the game folder. Must include:
matchInfo— teams, contestants, date, competitionliveData.lineUp— squad lists with player IDs and shirt numbersliveData— event stream
One JSON file per frame inside the tracking/ sub-folder. Each file contains a list with at least one frame object structured as:
[
{
"players": [
{ "id": "...", "team": "team_id", "x": 12.3, "y": -5.1 }
]
}
]Coordinates follow the standard tracking convention: X ∈ [-52.5, 52.5], Y ∈ [-34, 34].
| Method | Path | Description |
|---|---|---|
GET |
/api/browse-folder |
Browse the server filesystem to find game folders |
POST |
/api/load-game |
Load a game folder and trigger Transfermarkt enrichment |
POST |
/api/settings/clear-history |
Clear the games history |
GET |
/heatmap/<opta_id> |
Retrieve (or trigger generation of) a player heatmap |
POST |
/api/heatmap/warmup |
Start background heatmap pre-generation for all players |
GET |
/api/heatmap/warmup/status |
Poll warmup progress |
GET |
/api/stats/pitch-control |
Get the pitch-control timeline for the selected game |
GET |
/stats |
Stats HTML view |
Most endpoints accept a ?game=<game_id> query parameter to target a specific loaded match.
The application uses a layered caching approach to minimise redundant computation:
- In-memory caches — Python dicts keyed by game ID or player ID; cleared on application restart
- On-disk heatmap cache — PNG files stored under
data/cache/heatmaps/<game_id>/; versioned withHEATMAP_CACHE_VERSION - Transfermarkt raw response cache —
transfermarkt_data.jsoninside the game folder; prevents repeated API calls on reload - Enrichment file cache —
players_enrichments.json/teams_details.json; generated once and reused on subsequent loads
Background heatmap generation is managed via a ThreadPoolExecutor (8 workers) with in-flight deduplication, ensuring that concurrent requests for the same player map do not trigger redundant computation.
This project is provided as-is for internal/research use. See your organisation's licensing terms for Opta and Transfermarkt data usage.