Files
intotheeast-com/docs/superpowers/specs/2026-06-19-stats-redesign.md
T
2026-06-19 22:31:33 +02:00

5.6 KiB
Raw Blame History

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)