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:
@@ -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)
|
||||
Reference in New Issue
Block a user