Introduction



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 (OpenStreetMap)

Cauberg climb, Valkenburg, the Netherlands (Esri.WorldImagery)

Cauberg climb, Valkenburg, the Netherlands (Esri.WorldImagery)

Cauberg climb, Valkenburg, the Netherlands (OpenTopoMap)

Cauberg climb, Valkenburg, the Netherlands (OpenTopoMap)



Collect your cycle data


1. Strava/VeloViewer

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.

2. Climbfinder


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



Read gpx files



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



Difficulty indicator



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



Leaflet mapping



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



Climb difficulty ranking



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() 



Climb popularity



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


  1. Geographer, data-analist and cyclist↩︎