Files
intotheeast-com/docs/working/specs/2026-06-19-stats-redesign.md
T

135 lines
5.5 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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)