docs: add stats redesign spec

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RB86BaJBG3eGiMdfhmHRrQ
This commit is contained in:
2026-06-19 22:31:33 +02:00
parent 46c33837ba
commit 7602b135f8
@@ -0,0 +1,143 @@
# Stats Redesign — Design Spec
*2026-06-19*
---
## Goal
Expand trip statistics with new data points already available in entry frontmatter, add smart distance labelling based on whether GPX files are present, and add a dedicated cycling stats panel derived from GPX track data.
---
## Data sources
| Source | Fields available |
|---|---|
| Entry frontmatter | `date`, `lat`, `lng`, `location_city`, `location_country`, `weather_temp_c` |
| Trip page media | `.gpx` files (Komoot exports) |
| GPX trackpoints | `lat`, `lon`, `<ele>` (meters), `<time>` (ISO 8601, 1s resolution) |
| GPX track metadata | `<type>` (e.g. `racebike`, `hiking`) |
---
## Main stats block — changes
The existing 4-stat grid expands to 6 stats. Both `stats.html.twig` and the inline toggle in `trip.html.twig` get the same treatment.
### Stats
| Stat | Label | Source | Notes |
|---|---|---|---|
| Days on the road | `days on the road` | `(now - first entry date) / 86400` | Unchanged |
| Entries posted | `entries posted` | `all_entries\|length` | Unchanged |
| Countries visited | `countries visited` | Deduplicated `location_country` | Unchanged; country list shown below grid |
| **Cities visited** | `cities visited` | Deduplicated `location_city` | New; same dedup logic as countries |
| Distance | see below | see below | Label + icon vary by mode |
| **Temperature range** | `°C range` | `min(weather_temp_c)` `max(weather_temp_c)` | New; shown as e.g. `2 → 28 °C` |
### Distance stat — two modes
**Mode A — GPX present** (any `.gpx` files exist on the trip page):
- Value: sum of haversine distances between all consecutive trackpoints across all GPX files
- Label: `km cycled`
- Icon: cycling icon (or activity-specific icon — see Icon system below)
**Mode B — No GPX files:**
- Value: sum of haversine distances between consecutive entry `lat`/`lng` points (current behaviour)
- Label: `km roamed`
- Icon: generic travel icon (compass / globe)
Edge case — trip has both GPX files and many geo-spread entries (e.g. a mixed cycling + backpacking trip): use Mode A (GPX total only). This may understate total travel distance. Accepted limitation; revisit when transport mode is implemented.
---
## Cycling panel
A separate expandable panel, independent of the main stats toggle. Only rendered when GPX files are present on the trip page.
### Button placement
Sits next to the existing Stats button in the filter bar area:
```
[ All ] [ Journal ] [ Stories ] [ Stats ] [ Cycling ]
```
The Cycling button is hidden entirely when no GPX files exist. Detection: server-side Twig filters `trip_page.media.all` for `.gpx` extension (same mechanism the map template already uses) and sets a boolean passed to the template.
### Stats shown
| Stat | Unit | How computed |
|---|---|---|
| Distance | km | Sum haversine between all trackpoints (same value as main stats Mode A) |
| Elevation gain | m ↑ | Sum of positive `<ele>` differences (threshold: > 1 m per step to filter GPS noise) |
| Elevation loss | m ↓ | Sum of negative `<ele>` differences (same threshold) |
| Highest point | m | `max(<ele>)` across all files |
| Lowest point | m | `min(<ele>)` across all files |
| Moving time | h:mm | Total time excluding segments where computed speed < 1 km/h |
| Average speed | km/h | Distance ÷ moving time |
Max speed is explicitly excluded — GPS noise at 1-second resolution produces unreliable spikes.
### Icon system
The GPX `<type>` tag on the track element drives the icon shown in both the main stats distance block and the cycling panel header:
| `<type>` value | Icon |
|---|---|
| `racebike` | Road bike |
| `touringbicycle` | Touring bike |
| `mtb` | Mountain bike |
| `cycling` (generic) | Generic bike |
| `hiking` | Hiking boot |
| `hike` | Hiking boot |
| Any unrecognised value | Generic bike (fallback) |
When multiple GPX files exist with different types, use the type from the first file. This is an acceptable heuristic for now.
---
## GPX parsing — algorithm
All parsing is client-side JavaScript. The template passes the list of GPX file URLs to a JS variable; JS fetches and processes them sequentially.
```
for each GPX file URL:
fetch(url) → text → DOMParser → XML document
extract all <trkpt> elements → array of { lat, lon, ele, time }
append to master trackpoint array
compute over master array:
distance = sum haversine(p[i-1], p[i]) for i in 1..n
ele_gain = sum max(0, ele[i] - ele[i-1] - 1) for i in 1..n (1m threshold)
ele_loss = sum max(0, ele[i-1] - ele[i] - 1)
highest = max(ele)
lowest = min(ele)
speed[i] = haversine(p[i-1], p[i]) / (time[i] - time[i-1]) in km/h
moving_time = sum (time[i] - time[i-1]) where speed[i] >= 1 km/h
avg_speed = distance / moving_time
```
The 1 m elevation threshold filters out the flat-line noise visible in the Komoot files (many consecutive identical `<ele>` values).
---
## Template changes
| File | Change |
|---|---|
| `stats.html.twig` | Add cities stat, temp range stat; update distance stat with mode detection + label + icon |
| `trip.html.twig` | Same stats changes; add Cycling button (hidden if no GPX); add cycling panel block with JS parsing |
The cycling panel JS and the distance mode detection JS share the same GPX fetch logic — extract into a single `parseGpxFiles(urls)` function called once, results used by both.
---
## Out of scope
- Transport mode per entry (deferred — tracked separately)
- Weather breakdown (dropped — depends on free-text consistency)
- Max speed stat (dropped — GPS noise)
- Lowest point shown in main stats (cycling panel only)
- Per-file breakdown (one aggregate across all GPX files)