Starting on the 29th April 2026, and continuing for the next nine days, myself and four others completed a ‘bike-packing’ expedition from Saint-Malo (France), to Santander (Spain). Even before the trip began, I had envisioned documenting the journey in a ‘computational’ way, and, after building some requisite tools, I am ready to do just that.

Preparing for the Trip

Bike-packing trips, defined as transporting yourself and your belongings around by bike, are notorious for warranting diligent ‘packing’ of your belongings. Reducing excess mass and volume makes for much easier and more comfortable cycling.
Everything I packed. Notice the saddle bag, frame bag, and handlebar bag in the top-left of this picture, into which everything else must fit.
Obviously, the cycling route is also crucial. This was largely planned in advance, allowing for some flexibility if we ran into trouble.
I can visualize our intended ‘waypoints’ on a map, representing ferry ports and where we planned on staying each night, in an Airbnb or similar:
overnightStayLocations=
Saint-Malo
CITY
,
La Gacilly
CITY
,
Nantes
CITY
,
La Rochelle
CITY
,
Pauillac
CITY
,
Biarritz
CITY
,
Lekeitio
CITY
,
Laredo
CITY
,
Santander
CITY
;​​GeoListPlotovernightStayLocations,

Out[]=

The Journey

My particular journey began at Wolfram’s office in Long Hanborough. However, I did not cycle from there, I took the train in the direction of our outbound ferry port, Portsmouth, via Reading and Basingstoke:
Photo credits to Xiaofan Zhang!

Retrieving Strava Data

StravaLink Paclet

I have tracked my cycling, running, hiking, skiing, etc., on Strava since ~2016. While I am not someone who obsesses over their fitness data, the ability to analyse my exercise data from the comfort of a Wolfram notebook was always desirable. This trip gave me sufficient incentive to build an interface between Wolfram Language and Strava. I called this StravaLink, and it is currently live on the Wolfram Language Paclet Repository.
There are some setup instructions in the paclet documentation regarding authorization, but since I have set this up already I can simply install the paclet:
In[]:=
PacletInstall["JosephBrennan/StravaLink"]
Out[]=
PacletObject
Name: JosephBrennan/StravaLink
Version: 2.0.0

And loading the necessary functions:
In[]:=
Needs["JosephBrennan`StravaLink`"]
I can start by returning the data associated with my Strava account, and, yes, that is a badger emoji:
In[]:=
Dataset[StravaAthleteData[],ItemSize->20]
Out[]=
I can also choose custom dates between which to return my activities. I will reverse the output so it is returned in chronological order rather than ‘most recent’-first:
In[]:=
activities=ReverseStravaActivityData"Before"->
Fri 8 May 2026
,"After"->
Thu 30 Apr 2026
;
In[]:=
Dataset[activities〚All,{"name","distance","sport_type","start_date"}〛]
Out[]=
name
distance
sport_type
start_date
Day 1
121064.
Ride
2026-04-30T06:41:03Z
Day 2
89037.4
Ride
2026-05-01T07:48:17Z
Afternoon Walk
2298.7
Walk
2026-05-01T14:48:22Z
Day 3
150171.
Ride
2026-05-02T05:52:55Z
Day 4 (i)
32508.3
Ride
2026-05-03T11:27:23Z
Day 4 (ii)
14020.9
Ride
2026-05-03T13:39:06Z
Day 4 (iii)
11727.6
Ride
2026-05-03T15:51:39Z
Day 5 (i)
59135.7
Ride
2026-05-04T08:16:31Z
Day 5 (ii)
10618.4
Ride
2026-05-04T15:38:57Z
Day 6
125828.
Ride
2026-05-05T08:40:35Z
Day 7
121025.
Ride
2026-05-06T08:42:31Z
Day 8
51428.6
Ride
2026-05-07T09:33:11Z
Firstly, I want to select only activities that had the “sport_type” of “Ride”:
Here are the properties contained within each activity:
Peloton in action.

Distance Per Day

Often I partitioned a day’s cycling into multiple uploads, hence “Day 4 (i)”/“Day 4 (ii)” above. I can split the activities according to the day on which they were uploaded:
Then find the daily distance over the course of the trip:
And plot the accumulated total:
My setup pictured near a beach in Biarritz.

Elevation Profile

We can also obtain activity ‘streams’ from each activity, and use these to visualize the elevation profile of the entire trip:
We must account for the fact that the ‘time’ axis must accumulate with each activity:
Vertical grey lines correspond to each day of cycling:
Evidently, the penultimate day contained the toughest climbs!
POV: Climbing uphill in Basque Country.

Speed Profile

I would like to make a similar visualization but for the ‘velocity’ stream:
Difficult to deduce much from this speed profile. Instead, I can plot a histogram of all speeds:

Grade vs Speed

I can also obtain the ‘grade’ streams, which returns the road gradient as a percentage:
We can plot the ‘grade’ data against the ‘speed’ data and look for a negative correlation:
Or visualize as a SmoothDensityHistogram:

Just For Fun

Just for fun, let’s plot each ride’s distance against the no. of kudos received:
We can even find the coefficients of the linear model that ListFitPlot uses:
Fascinating. Almost a 90% correlation between distance cycled and kudos received!
Cycling through Bordeaux vineyards.

Recovering The Route

We could simply use the “Coordinates” stream to find the pairs of {lat, lon} for each cycle route, however I noticed that Strava returns something called a polyline for each activity.
Here I am using a version of StravaActivityData that takes the ‘activity ID’ as an argument, which enables a more detailed analysis:
This polyline is a result of a lossy compression algorithm that represents a series of coordinates as a single string of ASCII characters. Thankfully I have previously written a resource function (GooglePolylineEncode) which does this encoding, and crucially, written another which does the inverse (GooglePolylineDecode). I can now use the latter to convert each polyline into a list of GeoPosition objects:
Now we have everything we need to visualize the entire cycling route on a map, complete with waypoints:
It is starting to become clear where we took a train...

Overpass Turbo

Knowing that I would ultimately want to plot my cycling routes on a map, I became interested in how I could intersperse the cycling routes with ferry routes, or even trains we took along the journey. A solid option for this is Overpass Turbo, a web-based filtering tool for OpenStreetMap.
We can make direct API calls from the notebook, so I will define a function which takes Overpass Query Language (Overpass QL) queries as a string:
I plan on using this function to obtain route information for the following routes:
◼
  • Outbound ferry from Portsmouth (UK) to Saint-Malo (France)
  • ◼
  • Train from La Rochelle to Montendre
  • ◼
  • Short ferry from Blaye to Lamarque
  • ◼
  • Train from Bordeaux to Bayonne
  • ◼
  • Return ferry from Santander (Spain) to Plymouth (UK)
  • The ‘elements’ key contains latitude and longitude co-ordinates:
    Obtaining a train route is slightly more involved. Since I cannot query for specific sections of the railway, I have to use latitude/longitude constraints, and sometimes these must be different if the route is spread across two distinct named railway lines:
    This seems to ‘cut off’ slightly north of the Montendre station, but it will have to do.
    Next, the short ferry:
    Next, the Bordeaux to Bayonne train:
    And finally, the return ferry from Santander to Plymouth:
    Then I define a function to convert the {lat, lon} pairs into GeoPosition objects:

    Join Everything Together

    Now I can visualize our movements, both cycling and otherwise, on a single map, complete with waypoints:
    We can take a closer look at the miniscule drop of cyan representing the Blaye to Lamarque ferry:

    More Analysis

    Heat Maps

    I wanted to see if I could replicate Strava’s heat maps, which overlays performance data such as pace, heart rate, or elevation directly onto the route line.
    The ‘velocity’ and ‘coordinates’ activity streams should provide what I need to create a velocity heatmap:
    I would like to pick a random ride and create a heatmap for it. I will choose “Day 7”, which is the 10th ride in the series:
    I can create a custom color function which rescales the speeds such that the 5th and 95th percentile represent 0 and 1:
    Visualizing this is as simple as using GeoPath with VertexColors:

    Mountainous Terrain

    The eIevation profile from earlier showed this “Day 7” section was the ‘hilliest’, so I thought it would be interesting to explore certain parts with GeoElevationData.
    Firstly, I needed to choose a location around which to plot the terrain. A neat way to do this is sample some {lat, lon} points along the route and determine which Wolfram Language “City” entity is closest:
    I decided that Muxika and Larrabetzu were suitable locations around which to sample the terrain:
    We can also see how much of the “Day 7” route intersects with these locations:
    I can combine the ‘coordinates’ and ‘elevation’ activity streams into a single list of tuples:
    Earlier, I saw that it was around the 2000th coordinate that was deemed nearest to Muxika. After some tests, points 1500-2500 seem appropriate to keep.
    It occurred to me that I can compare the elevation values returned by GeoElevationData to the elevation stream from Strava. I will sample every 5th point in the interests of speed:
    The discrepancies do vary, but mostly the Strava elevation values are 15m to 25m higher than Wolfram’s GeoElevationData. This is probably small enough to be explained by differences in the digital elevation model (DEM) used by each.
    Finally, let’s visualize the route (red) through the mountainous terrain. I have included a small vertical offset to ensure the red points appear ‘above’ the terrain surface:
    I am keen to repeat this with the climbs at the end of the day, near the city of Liendo:
    Excellent! You may be able to deduce what is coming...

    Where Did The Day Go?

    I was curious to explore the time allocated to cycling vs activities like stopping for coffee or constructing baguettes on the side of the road:
    Note, on the days which I separated into different uploads, the ‘static’ time is under-represented. These were essentially ‘rest days’ anyway!

    Segments

    Using the detailed activity breakdown, we can see all ‘segment efforts’ within a single activity. This is not simply the number of segment efforts, it contains detailed information on every single one!
    We can see that one of the rides, Day 4 (iii), did not feature any segments. Below we see the relationship between the distance of each ride and the number of segments:
    Interestingly, Day 6, 7, and 8 have an abundance of segments. Given that these rides all took place in the ‘Spanish’ portion of the trip, this could provide some insight into how different countries use Strava! I can also explore the distribution of segment distances on each ride longer than 50km:
    One segment that caught my eye was over 43km on Day 6. We can find more details about this segment:
    This segment effort has multiple IDs. There is the ID of ‘my effort’, which accounts for the possibility that I would have multiple efforts on the same segment. There is also the ID of the segment itself. This can be used to obtain even more detailed information about this segment, such as it’s polyline:
    Here we can see the geographic proportion of “Day 6” taken up by this segment:

    Closing

    I would seriously recommend a trip like this to any keen cyclist, and I hope that the StravaLink paclet makes diving into your data as straightforward as possible!
    One of the things I most appreciate about Wolfram Language is its ability to support curiosity-driven exploration, wherein each answer becomes the starting point for another question.
    There is a remarkable richness in this data, and the analyses presented here only scratch the surface; this post could easily have been several times longer!
    ​
    If anyone has any questions/suggestions, please reach out at josephb@wolfram.com. Thank you!

    CITE THIS NOTEBOOK

    Computational bike-packing: insights from Strava data​
    by Joseph Brennan​
    Wolfram Community, STAFF PICKS, June 7, 2026
    ​https://community.wolfram.com/groups/-/m/t/3729321