Valentin Jacquemin

From my Strava activites into one sparkline

With sal it’s now possible to fetch on demand all my personal Strava activities from the past year. The next step is to transform this raw dataset into a sparkline. But before diving into this, let’s see concretly how to install and start using sal.

Setup walkthrough

sal’s README explains it all, let’s go through this step by step.

  1. On Strava, create an API Application.

  2. Fetch sal from the GitHub repository’s release section or build it on your machine.

  3. Launch sal. You’ll need a browser to complete the first launch as it has to fetch an authentication token and ask for your approval in doing so.

    You’ll need the client_id and the client_secret that got generated on step 1 as well. They are available anytime on the settings page of your Strava profile.

    SAL_CLIENT_ID=<your_client_id> SAL_CLIENT_SECRET_ID=<your_client_secret_id> sal

  4. Under the default root directory for user-specific configuration, you’ll find the authentication details that sal will use on further launches. Those are stored under sal/state.json. That will come handy later.

If everything is alright, that should be it, the past year of Strava activities are dumped in your terminal!

Raw data into a sparkline

That dataset is an array of activities. To transform that into a sparkline, that needs to go through a few steps of transformations.

The idea is to get the total distance per week and then to plot those 52 points on a line. Besides that, one or two other stats are computed so that we can generate the accompanying text.

The whole pipeline uses jq and m4. The bulk of the process is the following:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
SL_DATAPOINTS=$(jq -c '.| map(select(.sport_type == "Run" or .sport_type == "TrailRun"))
    | map({distance: .distance, start_date: (.start_date | fromdate | strftime("%g%V"))})
    | group_by(.start_date)
    | map({start_date: (first.start_date), distance: (map(.distance) | add | floor | . / 1000)})

    +
    [(range(52) | {start_date: (now - (60 * 60 * 24 * 7 * .) | strftime("%g%V")), distance: 0})]
    | group_by(.start_date)
    | map({(first.start_date): (map(.distance) | add)})
    | reduce .[] as $week ({max: 0}; . as $acc | . + $week + ($week | keys | $week[.[0]] as $d | if $d > $acc.max then {max: $d} else {} end))
    | .max as $max | map_values(. / $max * 24 | round) | del(.max)

    | to_entries
    | . as $payload | reduce .[] as $week (""; . + ($payload | index($week) | . * 3 | tostring) + "," + ($week.value | tostring) + " ")
    ' activities.json)

A whole bunch of things are taking place 🤠:

  1. map and select filters the dataset to retain only running and trailrunning activities
  2. another map transforms each entries to pick only the distance and the date of the activity; the %g%V date format creates a key we’ll use to group activities of the same week. For example for today 2608 is %g%V
  3. group_by prepares the dataset so that we can make a total per week, it produces an array of activites per week, or more precisely per %g%V
  4. the next map sums all the activites of each week
  5. to ensure there’s no hole in the series, another array is created. It contains the 52 past weeks and an initial distance to zero
  6. next, it groups the 2 arrays in one, with the week as key
  7. for each week, it sums the .distance key, from each array; if I don’t run on a given week, this would ensure that I still have a datapoint for that week. At that stage, the dataset is made up of week keys and _distance values, as in [{"2607": 42}, ...].
  8. lines 10-11 normalize the .distance values
  9. lines 13-14 are the final touches to generate the 52 datapoints that we are going to draw with svg. The . * 3 is for the width of a given week. So the whole sparkline would be approximately 160px wide. There was definitely a bit of playing around before I’d end up with a pleasing rendering
  10. activities.json contains the output of sal, the past 52 weeks of Strava activities

So SL_DATAPOINTS contains the datapoints, something like 0,6 3,3 6,5..., and are included in a m4 template that I prepared.

More on this next step, in yet another upcoming and final post about this fun little side project.