Where do you run to? Map your Strava activities on static and Leaflet maps.

January 31, 2018

(This article was first published on Rcrastinate, and kindly contributed to R-bloggers)

So, Strava’s heatmap made quite a stir the last few weeks. I decided to give it a try myself. I wanted to create some kind of “personal heatmap” of my runs, using Strava’s API. Also, combining the data with Leaflet maps allows us to make use of the beautiful map tiles supported by Leaflet and to zoom and move the maps around – with the runs on it, of course.

So, let’s get started. First, you will need an access token for Strava’s API. I found all the necessary information for this in this helpful “Getting started” post. As soon as you have the token, you have access to your own data.

Now, let’s load some packages and define functions for getting and handling the data. For the get.activities() function, I adapted code from here.


token <- “

get.coord.df.from.stream <- function (stream.obj) {
  data.frame(lat = sapply(stream.obj[[1]]$data, USE.NAMES = F, FUN = function (x) x[[1]]),
             lon = sapply(stream.obj[[1]]$data, USE.NAMES = F, FUN = function (x) x[[2]]))

get.stream.from.activity <- function (act.id, token) {
  stream <- GET(“https://www.strava.com/”,
                path = paste0(“api/v3/activities/”, act.id, “/streams/latlng”),
                query = list(access_token = token))

get.activities <- function (token) {
  activities <- GET(“https://www.strava.com/”, path = “api/v3/activities”,
                    query = list(access_token = token, per_page = 200))
  activities <- content(activities, “text”)
  activities <- fromJSON(activities)
  activities <- lapply(activities, function(x) {
    x[sapply(x, is.null)] <- NA
  data.frame(do.call(“rbind”, activities))

get.multiple.streams <- function (act.ids, token) {
  res.list <- list()
  for (act.id.i in 1:length(act.ids)) {
    if (act.id.i %% 5 == 0) cat(“Actitivy no.”, act.id.i, “of”, length(act.ids), “\n”)
    stream <- get.stream.from.activity(act.ids[act.id.i], token)
    coord.df <- get.coord.df.from.stream(stream)
    res.list[[length(res.list) + 1]] <- list(act.id = act.ids[act.id.i],
                                             coords = coord.df)

We have all the functions we need to get and parse the APIs output available now. Let’s apply them. The logic is: First, we get all activities. This dataframe has a column called ‘id’ which we can use to get all the raw data for all activities (called ‘streams’ in the Strava API). The function get.coord.df.from.stream() creates a dataframe with lat/lon coordinates for one stream.

activities <- get.activities(token)
stream.list <- get.multiple.streams(activities$id, token)

We might want to get the boundaries of the cumulated set of all streams. We can use these boundaries as a bounding box for plotting the data. This means that all activities are going to be in the plotted map section.

all.lats <- unlist(sapply(stream.list, USE.NAMES = F, FUN = function (x) {
all.lons <- unlist(sapply(stream.list, USE.NAMES = F, FUN = function (x) {
lats.range <- range(all.lats)
lons.range <- range(all.lons)

Alternatively, you can set your own bounding box. These are the boundaries for Stuttgart, Germany. One suggestion: to find your own boundaries, you can plot your first map and use the locator() function in RStudio, this is a very convenient way of getting coordinates by clicking.
lons.range <- c(9.156572, 9.237580)
lats.range <- c(48.74085, 48.82079)

We start by plotting the tracks in red against a black background. You can play around with the alpha value and the lwd parameter to change the appearance. By plotting several tracks over each other, thicker lines represent routes I took more often.

# Setting up the plot
par(bg = “black”)
plot(x = lons.range, y = lats.range, type = “n”, bty = “n”, xlab = “”, ylab = “”, xaxt = “n”, yaxt = “n”)

# Plotting tracks one by one
for (el in stream.list) {
  lines(el$coords$lon, el$coords$lat,
        col = alpha(“darkred”, .4), lwd = 2)

A black-and-red plot of my runs through Stuttgart.
Now, this already looks quite nice and we see some kind of network through the city. But the city itself is missing. We need to get some map below the tracks. I am using the OpenStreetMap package for this. I already used it in an earlier post. Note that getting the map tiles from the servers might take a long time and might fail in some cases. Loading time (and the resolution of the final map) will depend heavily on the ‘zoom’ parameter.
map <- openmap(c(max(lats.range), min(lons.range)),
               c(min(lats.range), max(lons.range)), type = “maptoolkit-topo”, zoom = 14)
transmap <- openproj(map, projection = “+proj=longlat”)
plot(transmap, raster = T)

for (el in stream.list) {
  lines(el$coords$lon, el$coords$lat,
        col = alpha(“darkred”, .5), lwd = 3)
My runs through Stuttgart on an OpenStreetMap, click to enlarge

I also like a simple satellite map. I am using the ‘bing’ map type for this. There is a high-def version available here.
map <- openmap(c(max(lats.range), min(lons.range)),
               c(min(lats.range), max(lons.range)), type = “bing”, zoom = 15)
transmap <- openproj(map, projection = “+proj=longlat”)
plot(transmap, raster = T)

for (el in stream.list) {
  lines(el$coords$lon, el$coords$lat,
        col = alpha(“yellow”, 1/3), lwd = 3)
My runs through Stuttgart on a Bing satellite map, click to enlarge or click here for an HD version

Now, for the final step. These static maps already look quite nice and with sufficient resolution (as in the HD case with the satellite map), we can also zoom the map without loosing too much quality. But a more dynamical map would also be nice. Let’s use the wonderful leaflet package for this. I already done this in another post with only a single track wrapped in a Shiny app. I am using some pipe notation from the dplyr package to adapt the map and get the tracks onto it.
map <- leaflet() %>%
                   options = providerTileOptions(noWrap = T)) %>%
  fitBounds(lng1 = min(lons.range), lat1 = max(lats.range), lng2 <- max(lons.range), lat2 = min(lats.range))

for (el in stream.list) {
  map <- addPolylines(map, lng = el$coords$lon, lat = el$coords$lat,
                      color = “red”, opacity = 1/3, weight = 2)
With the function saveWidget(), we can save the resulting html map. You can move the map to Basel or Zürich to find some more tracks I ran there.

With the leaflet functions, we could even associate each track with a little mouseover text (like total distance or the date). I did not include this here because quite a few tracks have been plotted over each other and mouseover texts might just confuse us here.

Have fun running and plotting. 

To leave a comment for the author, please follow the link and comment on their blog: Rcrastinate.

R-bloggers.com offers daily e-mail updates about R news and tutorials on topics such as: Data science, Big Data, R jobs, visualization (ggplot2, Boxplots, maps, animation), programming (RStudio, Sweave, LaTeX, SQL, Eclipse, git, hadoop, Web Scraping) statistics (regression, PCA, time series, trading) and more...

If you got this far, why not subscribe for updates from the site? Choose your flavor: e-mail, twitter, RSS, or facebook...

Comments are closed.

Search R-bloggers


Never miss an update!
Subscribe to R-bloggers to receive
e-mails with the latest R posts.
(You will not see this message again.)

Click here to close (This popup will not appear again)