Site icon R-bloggers

Mapping a marathon with {rStrava}

[This article was first published on R on Nicola Rennie, and kindly contributed to R-bloggers]. (You can report issue about the content on this page here)
Want to share your content on R-bloggers? click here if you have a blog, or here if you don't.

After a long run in the Forest of Bowland when visiting Lancaster for a few days, I decided to try out the {rStrava} package to make some maps of where I’d been. This tutorial blog will walk through the process of getting the data from Strava, making the map, and animating it with {gganimate}.

Last week's route mapped out in #rstats using {rStrava} and {gganimate}🏃‍♀️🏃‍♀️🏃‍♀️ https://t.co/O510G8E6hr pic.twitter.com/WgUAFV6eFA

— Nicola Rennie (@nrennie35) July 15, 2022

What is {rStrava}? < svg class="anchor-symbol" aria-hidden="true" height="26" width="26" viewBox="0 0 22 22" xmlns="http://www.w3.org/2000/svg"> < path d="M0 0h24v24H0z" fill="currentColor"> < path d="M3.9 12c0-1.71 1.39-3.1 3.1-3.1h4V7H7c-2.76.0-5 2.24-5 5s2.24 5 5 5h4v-1.9H7c-1.71.0-3.1-1.39-3.1-3.1zM8 13h8v-2H8v2zm9-6h-4v1.9h4c1.71.0 3.1 1.39 3.1 3.1s-1.39 3.1-3.1 3.1h-4V17h4c2.76.0 5-2.24 5-5s-2.24-5-5-5z">

Strava is a an app for tracking physical activities, mostly used for running and cycling. The {rStrava} package lets you access data through the Strava API.

Since {rStrava} is available on CRAN, you can install it in the usual way using:

install.packages("rStrava")
library(rStrava)

There are two levels of data you can get using {rStrava} depending on whether or not you have an authentication token. If you don’t have a token, you can access some basic summary information on athletes with public profiles and their recent activities. You don’t even need a Strava account to access this data. You can see which functions don’t require a token by running:

help.search('notoken', package = 'rStrava')

However, if you want detailed data on your activities or if your profile is private (like mine) you need an authentication token to get the data into R. To get an authentication token you do need a Strava account of your own. The instructions on the {rStrava} README are pretty easy to follow to set up your authentication token.

Setting up an authentication token < svg class="anchor-symbol" aria-hidden="true" height="26" width="26" viewBox="0 0 22 22" xmlns="http://www.w3.org/2000/svg"> < path d="M0 0h24v24H0z" fill="currentColor"> < path d="M3.9 12c0-1.71 1.39-3.1 3.1-3.1h4V7H7c-2.76.0-5 2.24-5 5s2.24 5 5 5h4v-1.9H7c-1.71.0-3.1-1.39-3.1-3.1zM8 13h8v-2H8v2zm9-6h-4v1.9h4c1.71.0 3.1 1.39 3.1 3.1s-1.39 3.1-3.1 3.1h-4V17h4c2.76.0 5-2.24 5-5s-2.24-5-5-5z">

On the Strava website, in the settings, you can make an API application. There are three pieces of information you need to fill out:

After you click “Create”, you’ll be prompted to upload an icon (can be any image), and this will generate a token for you.

Now, you need to add this token into R. You can do this using the config() function from {httr}, and the strava_oauth() function from {rStrava}. The strava_oauth() function needs four pieces of information, all provided as character strings.

strava_token <- httr::config(token = strava_oauth(app_name,
                                                  app_client_id,
                                                  app_secret,
                                                  app_scope = "activity:read_all"))

The app_name is the name you gave to the app when making your token on the Strava website. The app_client_id and app_secret were generated after you clicked “Create” on the Strava website, and you can simply pass these in. You will also perhaps want to change the app_scope argument. By default, this is set to "public", but you may want to get information on your activities which are not public. You can save the token as a variable, to pass into the {rStrava} functions. I’ve called it strava_token.

Reading in the data < svg class="anchor-symbol" aria-hidden="true" height="26" width="26" viewBox="0 0 22 22" xmlns="http://www.w3.org/2000/svg"> < path d="M0 0h24v24H0z" fill="currentColor"> < path d="M3.9 12c0-1.71 1.39-3.1 3.1-3.1h4V7H7c-2.76.0-5 2.24-5 5s2.24 5 5 5h4v-1.9H7c-1.71.0-3.1-1.39-3.1-3.1zM8 13h8v-2H8v2zm9-6h-4v1.9h4c1.71.0 3.1 1.39 3.1 3.1s-1.39 3.1-3.1 3.1h-4V17h4c2.76.0 5-2.24 5-5s-2.24-5-5-5z">

With the authentication token set you can now begin to get data into R, directly from the Strava API. First of all, I grabbed the data on my activities using the get_activity_list() function, for which I need to pass in my Strava token. I then use the get_activity_streams() function to get detailed information on a specific activity. Here the id is the activity id i.e., the number that comes at the end of the URL string for the activity: https://www.strava.com/activities/{id}.

my_acts <- get_activity_list(strava_token) 
id = {id}
strava_data <- get_activity_streams(my_acts,
                                    strava_token,
                                    id = id)

This is what the output of strava_data looks like:

  altitude cadence distance grade_smooth heartrate      lat       lng moving time velocity_smooth         id
1     24.8      84   0.0027          2.0       105 54.04575 -2.798552  FALSE    0          0.0000 7419225187
2     24.9      85   0.0066          1.3       112 54.04572 -2.798607   TRUE    3          4.6548 7419225187
3     24.9      85   0.0078          1.0       117 54.04572 -2.798626   TRUE    4          4.5180 7419225187
4     24.9      86   0.0078          0.8       117 54.04570 -2.798638  FALSE    5          3.6144 7419225187
5     24.9      86   0.0102          0.9       118 54.04567 -2.798653   TRUE    6          4.4892 7419225187
6     24.9      85   0.0130          1.1       119 54.04565 -2.798678   TRUE    7          5.2812 7419225187

There are some nice built-in mapping functions in {rStrava} that I recommend checking out, but since I’m going to build my own here, I don’t need to use {rStrava} again. I saved the data as a CSV file so that I could go back and work on it again without having to re-download it using {rStrava}.

write.csv(strava_data, "strava_data.csv", row.names = F)

Data wrangling < svg class="anchor-symbol" aria-hidden="true" height="26" width="26" viewBox="0 0 22 22" xmlns="http://www.w3.org/2000/svg"> < path d="M0 0h24v24H0z" fill="currentColor"> < path d="M3.9 12c0-1.71 1.39-3.1 3.1-3.1h4V7H7c-2.76.0-5 2.24-5 5s2.24 5 5 5h4v-1.9H7c-1.71.0-3.1-1.39-3.1-3.1zM8 13h8v-2H8v2zm9-6h-4v1.9h4c1.71.0 3.1 1.39 3.1 3.1s-1.39 3.1-3.1 3.1h-4V17h4c2.76.0 5-2.24 5-5s-2.24-5-5-5z">

The data the comes out of the get_activity_streams() function is already very clean, so the data wrangling for this example is very minimal. In fact, I only used two functions, neither of which was really necessary. I converted the data frame to a tibble using as_tibble() because I prefer working with tibbles. Since all the data is for a single activity in this case, the id column is a bit redundant so I also used select() from {dplyr} to remove the id column.

library(tidyverse)
strava_data %>% 
  as_tibble() %>% 
  select(-id)

Background maps < svg class="anchor-symbol" aria-hidden="true" height="26" width="26" viewBox="0 0 22 22" xmlns="http://www.w3.org/2000/svg"> < path d="M0 0h24v24H0z" fill="currentColor"> < path d="M3.9 12c0-1.71 1.39-3.1 3.1-3.1h4V7H7c-2.76.0-5 2.24-5 5s2.24 5 5 5h4v-1.9H7c-1.71.0-3.1-1.39-3.1-3.1zM8 13h8v-2H8v2zm9-6h-4v1.9h4c1.71.0 3.1 1.39 3.1 3.1s-1.39 3.1-3.1 3.1h-4V17h4c2.76.0 5-2.24 5-5s-2.24-5-5-5z">

Now it’s finally time to start building a map! Here, I loaded the rest of the R packages I’ll be using for mapping and animating.

library(sf)
library(ggmap)
library(osmdata)
library(rcartocolor)
library(gganimate)

Here, {sf} isn’t technically necessary but useful if you want to make a geometry object in R (more on that later). {ggmap} and {osmdata} are used for creating a background map. {ggplot2} has already been loaded eariler with the rest of the tidyverse, and along with {rcartocolor} for a nice colour scheme, this will plot the main map. Then, {gganimate} is used for animating the map.

Before I actually mapped my run, I wanted to get a background map. I used the getbb() (bounding box) function from {osmdata} to get the approximate coordinates around where I started my run using the place name as input.

getbb("Lancaster, UK")
        min       max
x -2.983647 -2.458735
y 53.918066 54.239557

I then played around to get the exact rectangle I wanted, and specified it manually. Now, bb specifies the minimum and maximum latitude and longitude of where my background map should cover.

bb <- matrix(c(-2.9, -2.53, 53.95, 54.10), 
             ncol = 2, 
             nrow = 2,
             byrow = TRUE,
             dimnames = list(c("x", "y"), c("min", "max")))

This bounding box can be passed into get_map() from {ggmap} to get the background map. By default, {ggmap} uses Google Maps, for which an API key is required. Setting the source = "stamen" means that you don’t have to register a Google API key. You can also choose a maptype, and here I chose "toner-hybrid". I’d recommend playing around with the different types to see which one you like – use ?get_map() for a list of options. You can also choose whether or not you want a colour or black and white background. I opted for a black and white ("bw") background map, as I later found it difficult to get enough contrast between my data points and the background map otherwise.

bg_map <- get_map(bb,
                  source = "stamen",
                  maptype = "toner-hybrid", 
                  color = "bw")

The background map can be visualised using ggmap().

ggmap(bg_map)

Overlaying the activity data < svg class="anchor-symbol" aria-hidden="true" height="26" width="26" viewBox="0 0 22 22" xmlns="http://www.w3.org/2000/svg"> < path d="M0 0h24v24H0z" fill="currentColor"> < path d="M3.9 12c0-1.71 1.39-3.1 3.1-3.1h4V7H7c-2.76.0-5 2.24-5 5s2.24 5 5 5h4v-1.9H7c-1.71.0-3.1-1.39-3.1-3.1zM8 13h8v-2H8v2zm9-6h-4v1.9h4c1.71.0 3.1 1.39 3.1 3.1s-1.39 3.1-3.1 3.1h-4V17h4c2.76.0 5-2.24 5-5s-2.24-5-5-5z">

I’m simply going to use {ggplot2} to overlay the data in strava_data on top of my background map. Using {ggplot2}, there are (at least) two different ways we could add the data: either using geom_point() or geom_sf(). We’ll start with geom_point().

g <- ggmap(bg_map) +
     geom_point(data = strava_data,
                inherit.aes = FALSE,
                aes(x = lng, 
                    y = lat, 
                    colour = altitude), 
                 size = 1)

Here, we specify strava_data as the data argument in geom_point(). Note that there is no ggplot() call here, as it’s hidden inside the ggmap() function. Therefore, we also want to specify inherit.aes = FALSE to make sure that the hidden aesthetics carried through by ggmap() don’t interfere with our point data. I specify the x and y coordinates as the longitude and latitude, respectively, and colour the points based on the altitude. I also played around with the size of the points until it looked the way I wanted it to. Note that, alternatively you could use geom_line() in exactly the same way.

Since, longitude and latitude are geographic data, it may make sense to instead convert them to a geometry object using the {sf} package. This may be necessary if your background map and coordinate data use different coordinate systems. In this case, it doesn’t actually matter. But I’ll show you anyway, just in case you need it. First, we convert our strava_data tibble into an sf object using st_as_sf(). We also specify which columns from strava_data are the longitude and latitude, with the longitude column coming first. We set the coordinate reference system (crs) as 4326 to match the coordinate system used. Setting remove = FALSE also keeps the original latitude and longitude columns in the tibble, even after converting to an sf object.

strava_sf <- st_as_sf(strava_data,
                      coords = c("lng", "lat"),
                      crs = 4326,
                      remove = FALSE)

The strava_sf object is now an sf object so it can be used with geom_sf() instead of geom_point(). Here, we don’t need to specify the x and y aesthetics as they are automatically detected from the sf object. You may get a Coordinate system already present. Adding new coordinate system, which will replace the existing one. warning. This is because geom_sf() and ggmap() are both trying to set the (same) coordinate system.

g <- ggmap(bg_map) +
     geom_sf(data = strava_sf,
             inherit.aes = FALSE,
             aes(colour = altitude), 
             size = 1) 
g

The maps returned using geom_point() and geom_sf() are essentially the same in this case.

Styling the map < svg class="anchor-symbol" aria-hidden="true" height="26" width="26" viewBox="0 0 22 22" xmlns="http://www.w3.org/2000/svg"> < path d="M0 0h24v24H0z" fill="currentColor"> < path d="M3.9 12c0-1.71 1.39-3.1 3.1-3.1h4V7H7c-2.76.0-5 2.24-5 5s2.24 5 5 5h4v-1.9H7c-1.71.0-3.1-1.39-3.1-3.1zM8 13h8v-2H8v2zm9-6h-4v1.9h4c1.71.0 3.1 1.39 3.1 3.1s-1.39 3.1-3.1 3.1h-4V17h4c2.76.0 5-2.24 5-5s-2.24-5-5-5z">

The inital map looks okay, but we can add some styling to make it look better. I’m a big fan of {rcartocolor} for colour palettes. I can get the hex codes of the "SunsetDark" palette, and use the same hex codes for the title later.

my_colors <- carto_pal(7, "SunsetDark")
my_colors

I change the colour of my points using scale_colour_carto_c() from {rcartocolor}, and change the title that appears in the legend at the same time. I also add a caption using the labs() function. Finally, I edit the theme. The theme_void() function is really useful for maps because it removes most of the theme elements which aren’t very useful on maps like this e.g. axis labels, axis ticks, grid lines. I use the theme() function to bring the legend and the plot caption (used as a title here) inside the plot area. This create a little bit of white space at the bottom of the plot, so I remove it using plot.margin. I also edit the colour and size of the caption text.

g <- g + 
  scale_colour_carto_c(name = "Altitude (m)", palette = "SunsetDark") +
  labs(caption = "Lancaster - Forest of Bowland ") +
  theme_void() +
  theme(legend.position = c(0.85, 0.7), 
        legend.title = element_text(face = "bold", hjust = 0.5), 
        plot.caption = element_text(colour = "#dc3977", face = "bold", size = 16, 
                                    vjust = 10), 
        plot.margin = unit(c(0, 0, -0.75, 0), unit = "cm"))
g

Animating with {gganimate} < svg class="anchor-symbol" aria-hidden="true" height="26" width="26" viewBox="0 0 22 22" xmlns="http://www.w3.org/2000/svg"> < path d="M0 0h24v24H0z" fill="currentColor"> < path d="M3.9 12c0-1.71 1.39-3.1 3.1-3.1h4V7H7c-2.76.0-5 2.24-5 5s2.24 5 5 5h4v-1.9H7c-1.71.0-3.1-1.39-3.1-3.1zM8 13h8v-2H8v2zm9-6h-4v1.9h4c1.71.0 3.1 1.39 3.1 3.1s-1.39 3.1-3.1 3.1h-4V17h4c2.76.0 5-2.24 5-5s-2.24-5-5-5z">

I was pretty happy with the final static image, but why not animate it? {gganimate} makes it really easy to animate ggplot objects. For this example, I’d strongly recommend using the geom_point() version of the map.

g <- ggmap(bg_map) +
  geom_point(data = strava_data,
          inherit.aes = FALSE,
          aes(colour = altitude, 
              x = lng, 
              y = lat), 
          size = 1) + 
  scale_colour_carto_c(name = "Altitude (m)", palette = "SunsetDark") +
  labs(caption = "Lancaster - Forest of Bowland ") +
  theme_void() +
  theme(legend.position = c(0.85, 0.7), 
        axis.title = element_blank(), 
        legend.title = element_text(face = "bold", hjust = 0.5), 
        plot.caption = element_text(colour = "#dc3977", face = "bold", size = 16, 
                                    vjust = 10), 
        plot.margin = unit(c(0, 0, -0.75, 0), unit = "cm"))

Although you can animate plots with sf data using {gganimate}, it’s a little bit trickier and it takes longer to render. So why not make our lives a little easier? There are two functions we need to animate our map:

g = g + 
  transition_time(time = time) +
  shadow_mark()

The animate() function then actually builds the animation. Usually renderer = gifski_renderer() should be the default, but I kept getting individual images instead of a gif unless I specified it manually – to investigate later. Here, I also specified the width and height (using a little bit of trial and error to avoid white space caused by the fixed ratio from ggmap()). anim_save() then saves the gif to a file (analogously to ggsave() from {ggplot2}).

animate(g, renderer = gifski_renderer(), height = 372, width = 538, units = "px")
anim_save("mapping_marathon.gif")

And that’s it! You now have an animated map of your Strava recorded run (or cycle, or walk, or …)! If you want to create a map of your own, you can find the R code used in this blog on my website. Thanks very much to the creators of {rStrava} for such an easy to use package!

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

R-bloggers.com offers daily e-mail updates about R news and tutorials about learning R and many other topics. Click here if you're looking to post or find an R/data-science job.
Want to share your content on R-bloggers? click here if you have a blog, or here if you don't.