Strava Dashboard

A personal project to explore my Strava data

Strava
Perso
Author

Corentin Maslard

Published

August 31, 2024

Motivation

There’s nothing better than having fun with data that’s close to your heart. For me, it’s my Strava data (yes, I admit that sounds a bit silly). For my sanity and to decompress after work I like to spend my free time for hicking, running or cycling. It’s also allowed me to learn more about APIs, graph creation…

The project is still a work in progress, but the first version already includes total mileage for hiking, biking, and running, as well as a heat map of all my activities.

There are many more visualizations and features I’d love to add, but finding the time has been a challenge. For now, I manually retrieve new activities using the Strava API via the {rstrava} package, but I plan to automate this in the next version. Stay tuned!

Code written by me, Corentin Maslard and inspired by various websites:
  • https://www.dancullen.me/articles/creating-a-heatmap-in-r-with-google-polylines
  • https://padpadpadpad.github.io/post/animate-your-strava-activities-using-rstrava-and-gganimate/
  • https://medium.com/@annthurium/getting-started-with-the-strava-api-a-tutorial-f3909496cd2d
  • https://www.r-bloggers.com/where-do-you-run-to-map-your-strava-activities-on-static-and-leaflet-maps/
  • http://www.databrew.cc/posts/strava.html
  • https://github.com/fawda123/rStrava

The rStrava package allows easy access to your Strava data using the Strava API. Combined with leaflet I can easily create a heatmap of all of my Strava activities. One interesting challenge does arise when doing this. Strava stores the coordinates of the activity as a Google Polyline, so I will use the googleway package to decode the data in to latitude and longitude coordinates.

Data importation

Then I will enter my personal Strava API information which can be found here after you apply for API access.

Code
## Necessary info from Strava api https://www.strava.com/settings/api 
app_name       <- 'website' # chosen by user
app_client_id  <- '133545' # an integer, assigned by Strava
app_secret     <- 'e606d239de2a102bf125357c62641af7b2c39e04' # an alphanumeric secret, assigned by Strava

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

Next, I load in my activity list and compile my activities into a data frame using the rStrava library.

Code
## Load my activities and compile activities with rStrava library
my_data  <- get_activity_list(stoken)
act_data <- compile_activities(my_data)
save(my_data, act_data,stoken, file = "perso_projects/media/strava_report/data_strava.RData")

The activity data contains 56 variables. We only need to keep two, the map data called ‘map.summary_polyline’ and the ‘upload_id’.

Code
load(here::here("perso_projects/media/strava_report/data_strava.RData"))
## Keep only activity id and map line
keeps   <- c('map.summary_polyline', 'upload_id')
my_acts <- dplyr::select(act_data, match(keeps, names(act_data)))

Now that the data is prepared I want to create a blank map of the area I want to include in my map. I create longitude and latitude bounds to create the map area I want. This will take some trial and error to get the exact bounds that you want.

Code
## Create blank map bounded by given lon and lat
# Dijon
lons.range <- c(5, 5.1) #(x)
lats.range <- c(47.2, 47.4) #(y)
## tile options CartoDB.Positron , CartoDB.DarkMatter , Stamen.Toner  
map <- leaflet(options = leafletOptions(zoomControl = TRUE)) %>%
  addProviderTiles('CartoDB.Positron',
                   options = providerTileOptions(noWrap = T, minZoom=0, maxZoom=50)) %>%
  fitBounds(lng1 = min(lons.range), lat1 = max(lats.range), lng2 <- max(lons.range), lat2 = min(lats.range))
#map   #save using mapshot(map, file="sbmap.png")

Next I loop through all my activities and convert them from Google Polylines to a dataframe of longitudes and latitudes each loop through will add one activity to the map.

Below is a heat map of all my recorded activities, created using leaflet.

Code
# my_acts<-my_acts %>% 
#   drop_na() %>% 
#   dplyr::filter(map.summary_polyline!="")
# loop <- unique(my_acts$upload_id)
# for (i in loop) {
#   activity <- dplyr::filter(my_acts, upload_id == i)
#   coords   <- googleway::decode_pl(activity$map.summary_polyline)
#   map      <- addPolylines(map, lng = coords$lon, lat = coords$lat,
#                       color = 'red', opacity = 1/4, weight = 2)
# }
# 
# map

Change color depending of the different activity

Code
my_acts_select <- get_activity_list(stoken, after = as.Date('2021-01-01'))
act_data <- compile_activities(my_acts_select) %>% 
  dplyr::filter(map.summary_polyline!="") %>% 
  mutate(type2 = ifelse(type %in% c("Ride", "Run", "Hike"), type, "Other"))

loop <- unique(act_data$upload_id)

for (i in loop) {
  activity <- dplyr::filter(act_data, upload_id == i)
  coords   <- googleway::decode_pl(activity$map.summary_polyline)
  
  color <- switch(activity$type2,
                  "Ride" = '#007FE0',
                  "Run"  = '#fc4c02',
                  "Hike" = '#08AF93',
                  "Other" = '#F15BB5')  # Default color
  
  map <- addPolylines(map, lng = coords$lon, lat = coords$lat,
                      color = color, opacity = 1/4, weight = 2,
                      group = activity$type2)  # Ajouter la polyligne à un groupe
}

# Ajouter les contrôles JavaScript pour le filtrage
map <- map %>% addLayersControl(
  overlayGroups = unique(act_data$type2),
  options = layersControlOptions(collapsed = FALSE)
)
map
Code
#athl_fun(133545, trace = FALSE)
my_acts_select <- get_activity_list(stoken, after = as.Date('2021-01-01'))
act_data <- compile_activities(my_acts_select)
act_data<- act_data %>%
  dplyr::filter(type %in% c("Hike","Ride","Run","Workout")) %>% 
  dplyr::mutate(
    cumulative_distance = cumsum(distance),
    cumulative_duration = cumsum(moving_time),
    Year = format(as.Date(start_date), "%Y"),
    month = format(as.Date(start_date), "%m-%d")
  ) %>% 
  dplyr::group_by(Year, type) %>%
  dplyr::mutate(cumulative_duration_years = cumsum(moving_time),
                cumulative_distance_years = cumsum(distance)) %>%
  dplyr::ungroup() %>% 
  mutate(Hours = cumulative_duration_years/3600)

#test=act_data %>% dplyr::select(cumulative_duration_years,Hours,year,month)

Years <- unique(act_data$Year)

n_years <- length(Years)
my_gradient <- colorRampPalette(colors = c("#230007","#7F9CC7","#FFBA08", "#fc4c02"))(n_years)

p1<-act_data %>% 
  ggplot(aes(x= as.Date(month,"%m-%d"),y=Hours, col=Year, group = Year))+
  geom_line()+
  facet_wrap(type~., scales = "free_y")+
  ylab("Cumulative duration (h)")+
  xlab("Month")+
  scale_x_date(date_labels = "%m", date_breaks = "1 month") +  # Format pour les labels des mois
  theme_minimal()+
  scale_color_manual(values = my_gradient)

p2<-act_data %>% 
  ggplot(aes(x= as.Date(month,"%m-%d"),y=cumulative_distance_years, col=Year, group = Year))+
  geom_line()+
  facet_wrap(type~., scales = "free_y")+
  ylab("Distance (km)")+
  xlab("Month")+
  scale_x_date(date_labels = "%m", date_breaks = "1 month") +  # Format pour les labels des mois
  theme_minimal()+
  scale_color_manual(values = my_gradient)
#p1
ggplotly(p1)
Code
#ggplotly(p2)
Back to top