Visualising my Transalp bike ride
In summer 2020, I crossed the Alps with my road bike. I’ve recorded the whole ride and as a nice memory, I would like to visualise this ride.
I collected the data using my GPS head unit and uploaded it to Strava. In this analysis it is available in its entirety as two .rds files.
To load, wrangle and visualise the data, the following libraries are used:
library(tarchetypes)
library(geotargets)
library(conflicted)
library(patchwork)
library(tidyterra)
library(tidyverse)
library(grateful)
library(elevatr)
library(ggrepel)
library(targets)
library(ggtext)
library(magick)
library(raster)
library(scales)
library(terra)
library(pins)
library(fs)
library(sf)
conflicts_prefer(dplyr::filter)
conflicts_prefer(dplyr::select)
conflicts_prefer(dplyr::lag)
theme_set(theme_minimal())
Define where exactly the data is located.
act_file <- "data/act.rds"
meas_file <- "data/meas.rds"
Read activity data from rds file.
act <- function(act_file) {
read_rds(act_file) |>
mutate(
start_name = case_when(
id == "3650448726" ~ "Albstadt",
id == "3654245140" ~ "Winterthur",
id == "3659045033" ~ "Fluelen",
id == "3664650034" ~ "Andermatt",
id == "3669729902" ~ "Andermatt",
TRUE ~ NA_character_),
end_name = case_when(
id == "3650448726" ~ "Winterthur",
id == "3654245140" ~ "Fluelen",
id == "3659045033" ~ "Andermatt",
id == "3664650034" ~ "Andermatt",
id == "3669729902" ~ "Lugano",
TRUE ~ NA_character_),
poi_name = if_else(end_name == "Lugano", "Lugano", start_name),
name = str_glue("{start_name} - {end_name}"),
lat_poi = if_else(
poi_name == "Lugano",
map_dbl(end_latlng, 1),
map_dbl(start_latlng, 1)),
lng_poi = if_else(
poi_name == "Lugano",
map_dbl(end_latlng, 2),
map_dbl(start_latlng, 2))) |>
select(-distance)
}
df_act <- act(act_file)
Read measurement data. This data represents each point in time during my trip across the Alps. This includes among other things:
meas <- function(meas_file) {
df_meas_raw <- read_rds(meas_file)
df_distance_max <- df_meas_raw |>
group_by(id) |>
summarise(distance_max = max(distance)) |>
mutate(
distance_max = lag(distance_max, default = 0),
distance_max = cumsum(distance_max))
df_meas_raw |>
left_join(df_distance_max, by = join_by(id)) |>
group_by(id) |>
mutate(distance_cum = distance + distance_max) |>
ungroup()
}
df_meas <- meas(meas_file)
Turn measurements into an sf (Pebesma and Bivand 2023) object:
sf_act_meas_points <- act_meas_points(df_meas, df_act)
The sf object represents the ride as point data. We combine it into one LINESTRING:
sf_act_meas_lines <- act_meas_lines(sf_act_meas_points)
With the help of the ‘elevatr’ package (Hollister et al. 2025), get the elevation data of the trip. Turn it into an ‘raster’ (Hijmans 2025) object for later plotting.
alpen_raster <- function(sf_lines) {
get_elev_raster(sf_lines, z = 6, clip = "bbox", expand = 0.1) |>
rast()
}
raster_alpen <- alpen_raster(sf_act_meas_lines)
Start with creating a map of the ride.
vis_ride <- function(sf_act_meas_lines, raster_alpen) {
ggplot(sf_act_meas_lines) +
geom_spatraster_contour(data = raster_alpen, binwidth = 200) +
geom_sf(aes(color = id)) +
geom_label_repel(
aes(x = lng_poi, y = lat_poi, label = poi_name),
size = 2.5, fill = alpha(c("white"), 0.5)
) +
labs(
x = "Longitude", y = "Latitude"
) +
theme(
legend.position = "none", panel.grid.major = element_blank(),
axis.text = element_blank(), axis.title = element_blank()
)
}
If we were to create a plot with this function, the result would look like this:
The main challenge of the ride was the enormous elevation that was covered. Display this in an altitude plot:
vis_altitude <- function(sf_act_meas_points, sf_act_meas_lines) {
sf_act_meas_points |>
filter(moving) |>
ggplot(aes(x = distance_cum, y = altitude, color = id, group = id, fill = id)) +
geom_area(alpha = 0.6) +
geom_line() +
geom_label(
data = sf_act_meas_lines,
mapping = aes(x = mean_distance, y = mean_altitude, label = name),
fill = alpha(c("white"), 0.5)
) +
labs(x = "Distance [km]", y = "Height [m]") +
theme(legend.position = "none") +
expand_limits(y = 0) +
scale_x_continuous(
labels = label_number(scale = 0.001),
breaks = breaks_width(50000)
) +
scale_y_continuous(position = "right")
}
The plot would look like this:
Combine the two plots into one with the help of the patchwork (Pedersen 2025) package:
vis_transalp <- function(sf_act_meas_points, sf_act_meas_lines, raster_alpen) {
gg_final <- vis_ride(sf_act_meas_lines, raster_alpen) +
vis_altitude(sf_act_meas_points, sf_act_meas_lines) +
plot_layout(widths = c(1, 3)) +
plot_annotation(
title = "Transalp 2020",
subtitle = "Albstadt - Lugano"
) &
theme(
text = element_text(family = "Fira Code", size = 12)
)
ggsave(
"transalp.png", gg_final,
width = 40, height = 25, units = "cm", scale = 0.8
)
}
gg_transalp <- vis_transalp(sf_act_meas_points, sf_act_meas_lines,
raster_alpen)
What a ride it has been! I’m always thinking back to this experience. It’s time to recreate such a ride in the near future.
This work was completed using R v. 4.4.1 (R Core Team 2024) and the following R packages: distill v. 1.6 (Dervieux et al. 2023), elevatr v. 0.99.1 (Hollister et al. 2025), fs v. 1.6.6 (Hester, Wickham, and Csárdi 2025), geotargets v. 0.3.1 (Tierney, Scott, and Brown 2024), ggrepel v. 0.9.6 (Slowikowski 2024), ggtext v. 0.1.2 (Wilke and Wiernik 2022), knitr v. 1.51 (Xie 2014, 2015, 2025), magick v. 2.9.0 (Ooms 2025), patchwork v. 1.3.2 (Pedersen 2025), pins v. 1.4.1 (Silge, Wickham, and Luraschi 2025), raster v. 3.6.32 (Hijmans 2025), rmarkdown v. 2.30 (Xie, Allaire, and Grolemund 2018; Xie, Dervieux, and Riederer 2020; Allaire et al. 2025), scales v. 1.4.0 (Wickham, Pedersen, and Seidel 2025), sf v. 1.0.23 (Pebesma 2018; Pebesma and Bivand 2023), shiny v. 1.12.1 (Chang et al. 2025), tarchetypes v. 0.13.2 (Landau 2021a), targets v. 1.11.4 (Landau 2021b), terra v. 1.8.93 (Hijmans 2026), tidyterra v. 0.7.2 (Hernangómez 2023), tidyverse v. 2.0.0 (Wickham et al. 2019).
If you see mistakes or want to suggest changes, please create an issue on the source repository.
Text and figures are licensed under Creative Commons Attribution CC BY 4.0. Source code is available at https://codeberg.org/duju211/transalp_codeberg, unless otherwise noted. The figures that have been reused from other sources don't fall under this license and can be recognized by a note in their caption: "Figure from ...".
For attribution, please cite this work as
During (2026, Jan. 15). Datannery: Getting Over It. Retrieved from https://www.datannery.com/posts/getting-over-it/
BibTeX citation
@misc{during2026getting,
author = {During, Julian},
title = {Datannery: Getting Over It},
url = {https://www.datannery.com/posts/getting-over-it/},
year = {2026}
}