For a number of years the author has been trying to cycle as
many hills as possible throughout the country of The Netherlands. You
can find out where all these hills are located and their features on the
Climbfinder site. Strava offers cycling statistics, which can be
accessed in file format via VeloViewer. In this R markdown we are going
to plot the climb segments on a map, with characteristics and stats in a
popup list. Below three example print-screens (reference date July 14,
2024).
Cauberg climb, Valkenburg, the Netherlands (OpenStreetMap)
Cauberg climb, Valkenburg, the Netherlands (Esri.WorldImagery)
Cauberg climb, Valkenburg, the Netherlands (OpenTopoMap)
Strava segment data can be accessed via VeloViewer https://www.veloviewer.com. VeloViewer offers two CSV
export files, both of which we use the latter. The first csv provides
all attempts per segment, the second only the best attempt for each
segment based on speed.
To display in the popup list, we take the
number of Strava cyclists on a segment, the number of times the author
has cycled the segment and his position on the rankings. In addition, we
use the relative position score defined by VeloViewer. This score
represents your relative position to the other riders on each segment.
Close to 100 is very good, close to 0 very bad. The formula:
\(\displaystyle RelativePositionScore =
\left(\frac{n_{riders} + 1 - position}{n_{riders}+1}\right)
100\)
where:
\(\displaystyle n_{riders}\)
the number of cyclists who completed the segment (each different
cyclist counts once)
\(\displaystyle position\)
your segment ranking based on speed
Because VeloViewer adds 1 to both of the riders counts in the
equation it is impossible to actually get a value of 0 or 100.
Hills in this predominantly very flat country? Maybe you prefer to
call them molehills or microrelief. Anyway, there are a number of
irregularities in the landscape, apart from dikes and viaducts. The
southernmost part of the Netherlands, South Limburg, has been lifted by
tectonics, creating a plateau landscape in which a large number of
valleys have been carved out by water erosion. Furthermore, the
penultimate ice age - the Saale glacial stage - has left its mark on the
landscape, especially in the middle of the country. On top of the
Vaalserberg you will find the highest point in the Netherlands, at 323
meters above sea level. A very interesting site for anyone who loves
climbing is Climbfinder (https://www.climbfinder.com). For each climb, they
provide key facts such as its length in km, average gradient (%), and
the total ascent in meters. In addition, every ascent has a difficulty
indicator allowing climbs to be easily compared with each other.
I
link every cycled Climbfinder climb to the corresponding Strava
segments, which are available in abundance. The data obtained is added
to a reference table.
The script below collects the popup-data.
version[['version.string']]
## [1] "R version 4.4.2 (2024-10-31 ucrt)"
library(readr)
library(readxl)
library(htmltools)
library(osmdata)
library(sf)
library(leaflet)
library(leaflet.extras)
library(leaflet.extras2)
library(tidyverse)
options(width = 2500)
# Cycled segment data from Strava/VeloViewer (up-to-date).
Segments <- read_csv2(file = "data/segmenten_R.csv", col_names = TRUE) %>%
filter(Status != 'deleted' & Status != 'unknown' & Status != 'flagged' & (Country == 'Nederland' | Country == 'Duitsland' | Country == 'België') & Type == 'Ride') %>%
mutate(Dist_km = round(Dist_km,1)) %>%
select(Segment_ID, Name, Dist_km, Pos_Score, n_Riders, Tries, Pos)
# Favorite tailwind based on straight line between start and end points.
Wind <- read_csv2(file = "data/Climbfinder beklimmingen NL windrichting.csv", col_names = TRUE) %>%
rename(Wind = Windrichting, Climb = Klim) %>%
mutate(Wind = str_replace_all(Wind, c("O" = "E", "Z" = "S")))
# Climbfinder data (2023).
Climbs_NL_complete <- read_excel("data/climbfinder beklimmingen in Nederland.xlsx" ) %>%
left_join(Wind, by = c('Naam' = 'Climb')) %>%
arrange(desc(Klimpunten), desc(Hoogtemeters), desc(Stijgingspercentage)) %>%
mutate(Climbfinder_ranking = row_number())
Total_number_of_NL_climbs <- nrow(Climbs_NL_complete)
# Cycled segments associated with Climbfinder climbs/ representative Strava segments.
Climbs_NL_cycled_segments <- read_excel("data/klimmen NL.xlsx",
sheet = "klimmen NL") %>%
left_join(Segments, by = "Segment_ID") %>%
rename(Climb = Naam_ClimbFinder) %>%
filter(!is.na (Pos_Score)) %>%
left_join(Climbs_NL_complete, by = c('Climb' = 'Naam')) %>%
mutate(Similar_segment = ifelse(stringr::str_detect(Name, 'Climbf|climbf'), 4,
ifelse(Dist_km == Lengte_in_km, 3,
ifelse(Dist_km / Lengte_in_km >= 0.7 & Dist_km / Lengte_in_km <= 1.3, 2,
ifelse(Dist_km / Lengte_in_km >= 0.5 & Dist_km / Lengte_in_km <= 1.5, 1, 0))))) %>%
arrange(Climb, desc(Similar_segment))
# Cycled Climbs.
Climbs_NL_cycled <- Climbs_NL_cycled_segments %>%
group_by(Climb) %>%
summarise(n_Segments = n(),
median_PosScore = round(median(Pos_Score),2),
Riders = max(n_Riders),
Tries = max(Tries),
Climbfinder_ranking = first(Climbfinder_ranking),
median_Position = round(median(Pos,1)),
Length_km = first(Lengte_in_km),
Grade = first(Stijgingspercentage),
Difficulty_points = first(Klimpunten),
Steepest_100_m = first(Steilste_100m),
Ascent_meters = first(Hoogtemeters),
Town = first(Plaats),
Province = first(Provincie),
Wind = first(Wind),
Segment_ID_1 = first(Segment_ID),
Segment_ID_2 = nth(Segment_ID, 2),
Segment_ID_3 = nth(Segment_ID, 3)) %>%
mutate(Segment_IDs = str_replace_all(trimws(str_replace_all(paste(Segment_ID_1,Segment_ID_2,Segment_ID_3),"NA", ""))," ", ", ")) %>%
mutate(Cycled = TRUE) %>% select(-c("Segment_ID_1", "Segment_ID_2", "Segment_ID_3"))
head(Climbs_NL_cycled)
## # A tibble: 6 × 17
## Climb n_Segments median_PosScore Riders Tries Climbfinder_ranking median_Position Length_km Grade Difficulty_points Steepest_100_m Ascent_meters Town Province Wind Segment_IDs Cycled
## <chr> <int> <dbl> <dbl> <dbl> <int> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <chr> <chr> <chr> <chr> <lgl>
## 1 Aagtdorpse Nok 5 94.4 44168 9 163 2386 0.74 5.1 29 6 38 Schoorl Noord-Holland SSE 2893455, 27886250, 10887132 TRUE
## 2 Aardaker via Oude Maastrichterweg 1 95.2 186 1 261 9 0.9 4.7 22 6.8 42 Gulpen Limburg E 37790754 TRUE
## 3 Aardmansberg vanuit Nieuw-Milligen 9 99.3 50516 11 327 193 4.1 1.7 18 4.5 71 Nieuw-Milligen Gelderland W,WSW 1936763, 1214125, 14745796 TRUE
## 4 Aardmansberg vanuit Uddel 8 98.8 35455 22 286 364 5.7 1.4 20 5.9 80 Uddel Gelderland NW 675531, 1675125, 2210951 TRUE
## 5 Aardmansberg via Hofweg 4 99.0 13569 3 235 120 4.8 1.6 23 3.8 79 Uddel Gelderland NW,WNW 1849213, 17628867, 19034184 TRUE
## 6 Abdissenbosch via Rimburgerweg 7 77.4 9936 1 460 1267 2 2 14 5.3 41 Landgraaf Limburg NW,WNW 2673784, 3105739, 16026332 TRUE
# How many cycled NL climbs?
nrow(Climbs_NL_cycled)
## [1] 726
GPX files of each climb can be downloaded via Cimbfinder, or
you can create them yourself. You copy them all into one folder and
import them as a group.
# Set names gpx files.
gpx_files = list.files(path = 'data/klimcoördinaten/', pattern = "gpx$", full.names = TRUE)
# for test purposes only.
#gpx_files <- gpx_files[1:300]
# Read all files.
gpx_files_stacked <- map_dfr(gpx_files, read_csv, id = "file_name", col_names = FALSE, trim_ws = TRUE) %>%
filter(substring(X1,1,10) == "<trkpt lat") %>%
mutate(record_nr = row_number()) %>%
mutate(eruit = ifelse(X1 == lag(X1), TRUE, FALSE)) %>%
filter(!eruit | record_nr == 1) %>%
mutate(Climb = str_sub(file_name, start=22, end=-5)) %>%
mutate_all(~gsub('<trkpt lat="|" lon="|">', " ", .)) %>%
mutate(X1 = str_trim(X1)) %>%
mutate(reeks = (gregexpr(' ', X1))) %>%
mutate(lat = substring(X1, 1, as.numeric(reeks)-1)) %>%
mutate(lon = substring(X1, as.numeric(reeks)+1)) %>%
group_by(file_name) %>%
mutate(Point_nr = row_number()) %>%
mutate(lat=as.numeric(lat)) %>%
mutate(lon=as.numeric(lon)) %>%
ungroup() %>%
mutate(Climb = str_replace_all(Climb, "_", " ")) %>%
select(Climb, Point_nr, lat, lon) %>%
arrange(Climb, Point_nr) %>%
left_join(Climbs_NL_cycled, by="Climb") %>%
filter(Cycled) %>%
select(Climb, Point_nr, lat, lon)
head(gpx_files_stacked)
## # A tibble: 6 × 4
## Climb Point_nr lat lon
## <chr> <int> <dbl> <dbl>
## 1 Aagtdorpse Nok 1 52.7 4.70
## 2 Aagtdorpse Nok 2 52.7 4.70
## 3 Aagtdorpse Nok 3 52.7 4.70
## 4 Aagtdorpse Nok 4 52.7 4.70
## 5 Aagtdorpse Nok 5 52.7 4.70
## 6 Aagtdorpse Nok 6 52.7 4.70
# climb finish coordinates.
top <- gpx_files_stacked %>%
group_by(Climb) %>%
filter(Point_nr == max(Point_nr)) %>%
ungroup() %>%
select(-Point_nr) %>%
arrange(Climb)
head(top)
## # A tibble: 6 × 3
## Climb lat lon
## <chr> <dbl> <dbl>
## 1 Aagtdorpse Nok 52.7 4.70
## 2 Aardaker via Oude Maastrichterweg 50.8 5.88
## 3 Aardmansberg vanuit Nieuw-Milligen 52.2 5.84
## 4 Aardmansberg vanuit Uddel 52.2 5.84
## 5 Aardmansberg via Hofweg 52.2 5.84
## 6 Abdissenbosch via Rimburgerweg 50.9 6.03
Climbfinder gives us a categorization based on their
difficulty points, see script below.
# Categorization based on difficulty points (Source: Climbfinder):
# Super Hors Catégorie (SHC): above 1500 climbing points.
# Hors Catégorie (HC). more than 900 difficulty points.
# 1st category. Climbs with 600+ difficulty points will be in this category.
# 2nd category. Climbs must have a minimum of 300 difficulty points to fit in this category.
# 3rd category. A climb needs at least 150 difficulty points to fit in this category.
# 4th category. With 75 difficulty points a climb falls into the fourth category.
# 5th category. The fifth category includes all climbs from 25 climbing points onwards.
# No category. Everything below 25 climbing points has no category. To be listed on climbfinder.
top <- top %>%
left_join(Climbs_NL_cycled, by = "Climb") %>%
arrange(Climb) %>%
mutate(Difficulty_points_cat = ifelse(Difficulty_points >= 75, ">= 75",
ifelse(Difficulty_points >= 50, "50-74",
ifelse(Difficulty_points >= 25, "25-49",
ifelse(Difficulty_points >= 15, "15-24",
ifelse(Difficulty_points < 15, "< 15", NA)))))) %>%
mutate(Difficulty_points_num = ifelse(Difficulty_points >= 75, 5,
ifelse(Difficulty_points >= 50, 4,
ifelse(Difficulty_points >= 25, 3,
ifelse(Difficulty_points >= 15, 2,
ifelse(Difficulty_points < 15, 1, NA)))))) %>%
mutate(Difficulty_points_cat_alt = ifelse(Difficulty_points >= 1500, "Super Hors CatC)gorie (SHC)",
ifelse(Difficulty_points >= 900, "Hors CatC)gorie (HC)",
ifelse(Difficulty_points >= 600, "1st category",
ifelse(Difficulty_points >= 300, "2nd category",
ifelse(Difficulty_points >= 150, "3rd category",
ifelse(Difficulty_points >= 75, "4th category",
ifelse(Difficulty_points >= 25, "5th category",
ifelse(Difficulty_points < 25, "no category", NA)))))))))
head(top)
## # A tibble: 6 × 22
## Climb lat lon n_Segments median_PosScore Riders Tries Climbfinder_ranking median_Position Length_km Grade Difficulty_points Steepest_100_m Ascent_meters Town Province Wind Segment_IDs Cycled Difficulty_points_cat Difficulty_points_num Difficulty_points_cat_alt
## <chr> <dbl> <dbl> <int> <dbl> <dbl> <dbl> <int> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <chr> <chr> <chr> <chr> <lgl> <chr> <dbl> <chr>
## 1 Aagtdorpse Nok 52.7 4.70 5 94.4 44168 9 163 2386 0.74 5.1 29 6 38 Schoorl Noord-Holland SSE 2893455, 27886250, 10887132 TRUE 25-49 3 5th category
## 2 Aardaker via Oude Maastrichterweg 50.8 5.88 1 95.2 186 1 261 9 0.9 4.7 22 6.8 42 Gulpen Limburg E 37790754 TRUE 15-24 2 no category
## 3 Aardmansberg vanuit Nieuw-Milligen 52.2 5.84 9 99.3 50516 11 327 193 4.1 1.7 18 4.5 71 Nieuw-Milligen Gelderland W,WSW 1936763, 1214125, 14745796 TRUE 15-24 2 no category
## 4 Aardmansberg vanuit Uddel 52.2 5.84 8 98.8 35455 22 286 364 5.7 1.4 20 5.9 80 Uddel Gelderland NW 675531, 1675125, 2210951 TRUE 15-24 2 no category
## 5 Aardmansberg via Hofweg 52.2 5.84 4 99.0 13569 3 235 120 4.8 1.6 23 3.8 79 Uddel Gelderland NW,WNW 1849213, 17628867, 19034184 TRUE 15-24 2 no category
## 6 Abdissenbosch via Rimburgerweg 50.9 6.03 7 77.4 9936 1 460 1267 2 2 14 5.3 41 Landgraaf Limburg NW,WNW 2673784, 3105739, 16026332 TRUE < 15 1 no category
And now we are able to generate the maps!
# Climb routes sf (lines on map).
routes_sf <- gpx_files_stacked %>%
arrange(Climb, Point_nr) %>%
st_as_sf( coords = c("lon", "lat")) %>%
group_by(Climb) %>%
summarize(do_union=FALSE) %>%
st_cast("LINESTRING") %>%
st_set_crs(4326)
head(routes_sf)
## Simple feature collection with 6 features and 1 field
## Geometry type: LINESTRING
## Dimension: XY
## Bounding box: xmin: 4.695193 ymin: 50.81353 xmax: 6.03187 ymax: 52.68854
## Geodetic CRS: WGS 84
## # A tibble: 6 × 2
## Climb geometry
## <chr> <LINESTRING [°]>
## 1 Aagtdorpse Nok (4.698142 52.68541, 4.698207 52.68554, 4.698466 52.68575, 4.698435 52.68589, 4.698494 ...
## 2 Aardaker via Oude Maastrichterweg (5.88817 50.81459, 5.8879 50.81445, 5.88754 50.81445, 5.8872 50.81449, 5.88685 50.8144...
## 3 Aardmansberg vanuit Nieuw-Milligen (5.779217 52.22107, 5.779586 52.22108, 5.779943 52.22112, 5.780309 52.22113, 5.780675 ...
## 4 Aardmansberg vanuit Uddel (5.78266 52.25757, 5.78294 52.25742, 5.78323 52.25729, 5.78358 52.25723, 5.78394 52.25...
## 5 Aardmansberg via Hofweg (5.78142 52.24589, 5.78174 52.24578, 5.78207 52.24568, 5.78239 52.24557, 5.78271 52.24...
## 6 Abdissenbosch via Rimburgerweg (6.00726 50.93584, 6.00759 50.93576, 6.00793 50.93568, 6.00826 50.9356, 6.00859 50.935...
# Mapping.
lf_map <- leaflet() %>%
addProviderTiles("OpenTopoMap", group = "OpenTopoMap", options = providerTileOptions(minZoom = 7)) %>%
addProviderTiles("Esri.WorldImagery", group = "Esri.WorldImagery", options = providerTileOptions(minZoom = 7)) %>%
addProviderTiles("OpenStreetMap", group = "OpenStreetMap", options = providerTileOptions(minZoom = 7)) %>%
addCircles(data = top, lng = ~lon, lat = ~lat, weight = case_when(top$Difficulty_points_num == 5 ~ 25,
top$Difficulty_points_num == 4 ~ 18,
top$Difficulty_points_num == 3 ~ 13,
top$Difficulty_points_num == 2 ~ 10,
top$Difficulty_points_num == 1 ~ 7),
opacity = 0.8, fill = FALSE, fillOpacity = 0.5, color="blue",
label = top$Climb,
popup = paste("<b>", top$Climb, "</b>", "<br/>",
"Climbfinder NL ranking (2023):", top$Climbfinder_ranking, " / ", Total_number_of_NL_climbs, "<br/>",
"Length (km):", top$Length_km, "<br/>",
"Grade (%):", top$Grade, "<br/>",
"Grade steepest 100 m (%):", top$Steepest_100_m, "<br/>",
"Total ascent (m):", top$Ascent_meters, "<br/>",
"Difficulty points:", top$Difficulty_points, "<br/>",
"Categorization:", top$Difficulty_points_cat_alt, "<br/>",
"Favorite tailwind:", top$Wind, "<br/>",
"Cycled segments:", top$n_Segments, "<br/>",
"median PosScore:", top$median_PosScore, "<br/>",
"median Position:", top$median_Position, "<br/>",
"Key segment ID's:", top$Segment_IDs, "<br/>",
"Tries:", top$Tries, "<br/>",
"Strava riders:", top$Riders, "<br/>",
"Town:", top$Town
),
popupOptions = popupOptions(autoPan = TRUE, closeOnClick = TRUE),
highlightOptions = highlightOptions(bringToFront = TRUE, opacity = 1, weight = 9, sendToBack = FALSE, color = "red"),
group = case_when(top$Difficulty_points_num == 5 ~ ">= 75 difficulty points (cat 4)",
top$Difficulty_points_num == 4 ~ "50-74 difficulty points (cat 5)",
top$Difficulty_points_num == 3 ~ "25-49 difficulty points (cat 5)",
top$Difficulty_points_num == 2 ~ "15-24 difficulty points",
top$Difficulty_points_num == 1 ~ "< 15 difficulty points")) %>%
addPolylines(smoothFactor = 0.4, data=routes_sf, opacity = 0.8, weight = 3, color = "blue",
label = top$Climb,
popup = paste("<b>", top$Climb, "</b>", "<br/>",
"Climbfinder NL ranking (2023):", top$Climbfinder_ranking, " / ", Total_number_of_NL_climbs, "<br/>",
"Length (km):", top$Length_km, "<br/>",
"Grade (%):", top$Grade, "<br/>",
"Grade steepest 100 m (%):", top$Steepest_100_m, "<br/>",
"Total ascent (m):", top$Ascent_meters, "<br/>",
"Difficulty points:", top$Difficulty_points, "<br/>",
"Categorization:", top$Difficulty_points_cat_alt, "<br/>",
"Favorite tailwind:", top$Wind, "<br/>",
"Cycled segments:", top$n_Segments, "<br/>",
"median PosScore:", top$median_PosScore, "<br/>",
"median Position:", top$median_Position, "<br/>",
"Key segment ID's:", top$Segment_IDs, "<br/>",
"Tries:", top$Tries, "<br/>",
"Strava riders:", top$Riders, "<br/>",
"Town:", top$Town),
group = case_when(top$Difficulty_points_num == 5 ~ ">= 75 (idem, routes)",
top$Difficulty_points_num == 4 ~ "50-74 (idem, routes)",
top$Difficulty_points_num == 3 ~ "25-49 (idem, routes)",
top$Difficulty_points_num == 2 ~ "15-24 (idem, routes)",
top$Difficulty_points_num == 1 ~ "< 15 (idem, routes)"),
popupOptions = popupOptions(autoPan = TRUE, closeOnClick = TRUE),
highlightOptions = highlightOptions(bringToFront = TRUE, opacity = 1, weight = 5, sendToBack = FALSE, color = "red")) %>%
addLayersControl(baseGroups = c("OpenStreetMap", "Esri.WorldImaginery", "OpenTopoMap"),
position = "topleft",
options = layersControlOptions(collapsed = TRUE),
overlayGroups = c(">= 75 difficulty points (cat 4)",
">= 75 (idem, routes)",
"50-74 difficulty points (cat 5)",
"50-74 (idem, routes)",
"25-49 difficulty points (cat 5)",
"25-49 (idem, routes)",
"15-24 difficulty points",
"15-24 (idem, routes)",
"< 15 difficulty points",
"< 15 (idem, routes)" )) %>%
groupOptions(c(">= 75 difficulty points (cat 4)", "50-74 difficulty points (cat 5)", "25-49 difficulty points (cat 5)","15-24 difficulty points", "< 15 difficulty points"), zoomLevels = 13:20) %>%
addScaleBar(position = "topleft", options = scaleBarOptions(metric = TRUE, imperial = FALSE, maxWidth = 250)) %>%
addFullscreenControl(position = "topright", pseudoFullscreen = FALSE)
lf_map
With 75 difficulty points a climb falls into the fourth
category. The figure below shows that the Netherlands has only ten
climbs in category 4, and none in higher categories.
Climbs_NL_cycled_Most_Points <- Climbs_NL_cycled %>%
arrange(desc(Difficulty_points)) %>%
top_n(60, Difficulty_points) %>%
mutate(Flag = c('No', 'Yes')[1+str_detect(Climb, as.character(Town))]) %>%
mutate(Climb = ifelse(Flag == "No",paste0(Climb, " (", Town,")"), Climb)) %>%
mutate(Climb = factor(Climb, levels = Climb))
ggplot(Climbs_NL_cycled_Most_Points, aes(x=Climb, y=Difficulty_points)) +
geom_bar(stat="identity", width=0.5, fill="tomato3") +
geom_hline(yintercept = 75, color ="red") +
geom_segment(aes(x=50, xend=50, y=81, yend=93),
arrow = arrow(length = unit(0.5, "cm")), color = 'red') +
annotate("text", x = 50, y = 104, label = "cat 4 climb", color = 'red') +
geom_text(aes(label=Difficulty_points), position=position_dodge(width=1), vjust = 0.1, hjust= -0.2, size = 3) +
labs(title=paste0(nrow(Climbs_NL_cycled_Most_Points), " Climbfinder NL hills with most difficulty points"),
subtitle="cycled by the author since 2014", caption=paste0("reference date: ", format(today(),"%d-%m-%Y"))) +
xlab("Climbfinder name") +
ylab("Difficulty points") +
theme(axis.text.x = element_text(angle=65, vjust=0.6)) +
scale_y_continuous(labels = function(x) format(x, big.mark = ".", decimal.mark = ",")) +
scale_x_discrete(limits = rev) +
coord_flip()
With over 210,000 Strava cyclists, the Cauberg is by far the
most popular climb in the Netherlands.
separator <- function(x){
format(as.numeric(x), big.mark = ".", decimal.mark = ",")
}
Climbs_NL_cycled_Most_Popular <- Climbs_NL_cycled %>%
arrange(desc(Riders)) %>%
mutate(Riders_sep = separator(Riders)) %>%
top_n(60, Riders) %>%
mutate(Flag = c('No', 'Yes')[1+str_detect(Climb, as.character(Town))]) %>%
mutate(Climb = ifelse(Flag == "No",paste0(Climb, " (", Town,")"), Climb)) %>%
mutate(Climb = factor(Climb, levels = Climb))
ggplot(Climbs_NL_cycled_Most_Popular, aes(x=Climb, y=Riders)) +
geom_bar(stat="identity", width=0.5, fill="tomato3") +
geom_text(aes(label=Riders_sep), position=position_dodge(width=1), vjust = 0.1, hjust= -0.2, size = 3) +
labs(title=paste0(nrow(Climbs_NL_cycled_Most_Popular), " most popular Climbfinder NL hills among Strava users"),
subtitle="cycled by the author since 2014", caption=paste0("reference date: ", format(today(),"%d-%m-%Y"))) +
theme(axis.text.x = element_text(angle=65, vjust=0.6)) +
scale_y_continuous(labels = function(x) format(x, big.mark = ".", decimal.mark = ",")) +
scale_x_discrete(limits = rev) +
ylab("Number of Strava riders") +
coord_flip()
Footnotes
Geographer, data-analist and cyclist↩︎