Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RB86BaJBG3eGiMdfhmHRrQ
5.6 KiB
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/lngpoints (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)