135 lines
5.5 KiB
Markdown
135 lines
5.5 KiB
Markdown
# 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` | `date_end - date_start` if trip `date_end` is set; else `now - first entry date` | Fixed for past trips |
|
||
| 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
|
||
|
||
A single static racing/gravel bike icon is used whenever GPX files are present — both in the main stats distance block and the cycling panel header. No dynamic switching based on `<type>`.
|
||
|
||
Known Komoot `<type>` values for reference (future use if icon switching is ever added):
|
||
`racebike`, `touringbicycle`, `mtb`, `cycling`, `hiking`, `hike`
|
||
|
||
---
|
||
|
||
## 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)
|