Janith Wanniarachchi
Interactive maps in shiny dashboards using the leaflet package
Janith Wanniarachchi
R Shiny Developer at Appsilon
Follow along at https://abuja-use-r-september-2022-leaflet.netlify.app/#/
👨💻
An R Shiny Developer at Appsilon
🎓
BSc. (Hons.) in Statistics Graduate from University of Sri Jayewardenepura, Sri Lanka.
👨🔬
An author of R packages on CRAN such as, scatteR and DSjobtracker.
Lucky for him, there is a dataset on bird observations 1 on GBIF 2, the Global Biodiversity Information Facility.
After downloading the dataset, Dayo set out to clean the dataset as much as he can and saved it online.
After looking at the structure of the dataset, Dayo decided to make a dashboard that showed the bird-watching sites and the birds that could be seen in all those sites during the year 2021.
Shiny is a framework for creating web applications using R code. There are two components to a Shiny application: the ui (user interface) and the server (business logic).
The logic of the server is defined through reactive programming where the connections between inputs and outputs are defined in a graph.
The starting point of a leaflet map is …
the function leaflet()
This returns a Leaflet map widget, which stores a list of objects that can be modified or updated later. We can also update the properties of the leaflet map such as the minimum and maximum zoom level and the rendering engine between SVG and Canvas.
Just the leaflet function alone will not show anything meaningful. To add an actual view of the world we need to add tiles of world map on it. Right now, Dayo wants to focus on his homeland Nigeria.
The default tiles are from the OpenStreetMap contributors. You can change the style of the map by using provider tiles.
To test things out, Dayo wants to put a marker on where his home is.
Including a large number of markers can be visually noisy.
Instead, Dayo can cluster the markers automatically.
Dayo was not sure whether he needs an entire marker to showcase the site orif a simple dot would suffice. For that, he decided to give CircleMarkers a try. There’s also the option to draw circles as shapes on the map.
makeCircles()
are similar to makeCircleMarkers()
The only difference is that circles have their radius specified in meters, while circle markers are specified in pixels making circles scale with zoom level.
Popups are shown when clicked on while labels are shown when hovered over.
leaflet(df) %>%
addProviderTiles(providers$Esri.NatGeoWorldMap) %>%
setView(lat = 11.4384,lng = 6.2319,zoom = 8) %>%
addMarkers(lat= ~decimalLatitude,lng= ~decimalLongitude,
clusterOptions = markerClusterOptions(),
popup = ~htmltools::htmlEscape(
glue::glue("{species_count} birds were spotted here")))
leaflet(df) %>%
addProviderTiles(providers$Esri.NatGeoWorldMap) %>%
setView(lat = 11.4384,lng = 6.2319,zoom = 8) %>%
addMarkers(lat= ~decimalLatitude,lng= ~decimalLongitude,
clusterOptions = markerClusterOptions(),
label = ~htmltools::htmlEscape(
glue::glue("{species_count} birds were spotted here")))
Now Dayo needs to bring this to Shiny.
library(shiny)
library(leaflet)
df <- arrow::read_parquet("data/data.parquet") %>%
dplyr::filter(year == 2021)
ui <- bootstrapPage(
# front end interface
tags$style(type = "text/css",
"html, body {width:100%;height:100%}"),
leafletOutput("map",
width="100%",
height="100%"),
)
server <- function(input, output, session) {
# back end logic
output$map <- renderLeaflet({
leaflet(data = df) %>%
addTiles() %>%
addMarkers(~decimalLongitude,
~decimalLatitude)
})
}
shinyApp(ui, server)
Let’s say we add a slider to show the sites that were recorded during a specific day and month.
library(shiny)
library(leaflet)
library(reactlog)
reactlog_enable()
df <- arrow::read_parquet("data/data.parquet") %>%
dplyr::filter(year == 2021)
ui <- bootstrapPage(
tags$style(type = "text/css",
"html, body {width:100%;height:100%}"),
# front end interface
leafletOutput("map",
width="100%",
height="100%"),
absolutePanel(bottom = 10,right = 10,
sliderInput(
"day_month",
"Select Day of year",
min = as.Date("2021-01-01","%Y-%m-%d"),
max = as.Date("2021-12-31","%Y-%m-%d"),
value=c(as.Date("2021-01-01"),
as.Date("2021-12-31")),
timeFormat="%Y-%m-%d"))
)
server <- function(input, output, session) {
# back end logic
data_reactive <- reactive({
df %>% tidyr::unite("sight_date",
year,month,day,
sep="-") %>%
dplyr::mutate(sight_date = as.Date(sight_date)) %>%
dplyr::filter(sight_date > input$day_month[1],
sight_date < input$day_month[2])
})
output$map <- renderLeaflet({
leaflet(data = data_reactive()) %>%
addTiles() %>%
addMarkers(~decimalLongitude,
~decimalLatitude)
})
}
shinyApp(ui, server)
Demo
In the above example, the Shiny application redraws the entire map each time and that is time-consuming. So we need to divide the basemap and the reactive components separately using leafletProxy()
. The following changes will have to be made in the server function.
data_reactive <- reactive({
df %>% tidyr::unite("sight_date",
year,month,day,
sep="-") %>%
dplyr::mutate(sight_date = as.Date(sight_date)) %>%
dplyr::filter(sight_date > input$day_month[1],
sight_date < input$day_month[2])
})
output$map <- renderLeaflet({
leaflet() %>%
addTiles() %>%
fitBounds(lng1 = 3,lat1 = 4,lng2 = 14,lat2 = 14)
})
observe({
leafletProxy("map", data = data_reactive()) %>%
clearMarkers() %>%
addMarkers(lng = ~decimalLongitude,
lat = ~decimalLatitude)
})
Now Dayo wants to display a table containing the scientific names of the top 10 most popular birds in the area. For that, he can use the events coming in from the map such as the bounds of the currently viewed map area.
input$MAPID_bounds
provides the latitude/longitude bounds of the currently visible map area; the value is a list() that has named elements north, east, south, and west.
# data to use to generate the gt table
df_bounds <- reactive({
if (is.null(input$map_bounds))
return(df[FALSE,])
bounds <- input$map_bounds
latRng <- range(bounds$north, bounds$south)
lngRng <- range(bounds$east, bounds$west)
subset(df,
decimalLatitude >= latRng[1] &
decimalLatitude <= latRng[2] &
decimalLongitude >= lngRng[1] &
decimalLongitude <= lngRng[2])
})
output$species_in_area <- renderTable({
df_bounds() %>%
select(species_list) %>%
separate_rows(species_list,sep = ",") %>%
count(species_list,sort=T,name = "Count") %>%
slice_max(Count,n=10)
})
Demo
Following his success in getting the map bounds, Dayo decided to add the functionality to get the list of species in a site by clicking on a marker. He could use the marker events given by leaflet but before that he needs to define ids for each site.
observe({
leafletProxy("map", data = data_reactive()) %>%
clearMarkers() %>%
clearMarkerClusters() %>%
addMarkers(lng = ~decimalLongitude,
lat = ~decimalLatitude,
layerId = ~site_id,
clusterOptions = markerClusterOptions())
})
output$species_list_text <- renderUI({
if(!is.null(input$map_marker_click)){
df %>%
filter(site_id == input$map_marker_click$id) %>%
select(species_list) %>%
unlist() %>%
str_trim() %>%
str_split(",") %>%
unlist() %>%
head(10) %>%
paste(collapse = " <br/> ") %>%
HTML()
}
})
Demo
Dayo thinks the Shiny application is ready to be given to his friends overseas.
For that, he can use shinyapps.io
to host his Shiny application.
But, Dayo isn’t done yet.
He’s got some new ideas he wants to try out:
The finished Shiny application is at https://janithwanni.shinyapps.io/abuja-bird-watcher/
The Github source code for the site is at https://github.com/janithwanni/abuja-use-r-bird-watcher
Email: janithcwanni@gmail.com
Twitter: @janithcwanni
Github: @janithwanni
Linkedin: Janith Wanniarachchi