Drawing Polygons with leaflet in shiny? - r

I have a geospatial dataset of monthly average temperatures in the US. I want to display this as a leaflet map in a Shiny app. With a time-slider, users should be able to select a visualisation of each month.
When I try to run my data with codes I found online I run into a number of problems and unfortunately I don't understand exactly where which data is needed.
On Wetransfer I uploaded my dataset Data.
Relevant info about the dataset: I want the slider to run by either the "Valid_Seas" column (monthly values by parts of the US) or "values". The polygons (column: Geometry) should be colored by the column "Prob", this is the monthly average temperature.
Regarding the R.skript: Starting at line 215 is my attempt to create the ShinyApp map, just a you can see here:
ui <- bootstrapPage(
tags$style(type = "text/css", "html, body {width:100%;height:100%}"),
leafletOutput("map", width = "100%", height = "100%"),
absolutePanel(top = 10, right = 10,
style="z-index:500;", # legend over my map (map z = 400)
tags$h3("Average Temperature"),
sliderInput("periode", "Months 2021",
min(tempyear21$values),
max(tempyear21$values),
value = range(tempyear21$values),
step = 1,
sep = ""
)
)
)
#bis hier hin stimmt es
server <- function(input, output, session) {
# reactive filtering data from UI
reactive_data_chrono <- reactive({
tempyear21 %>%
filter(Valid_Seas >= input$periode[1] & Valid_Seas <= input$periode[2])
})
# static backround map
output$map <- renderLeaflet({
leaflet(tempyear21) %>%
addTiles() %>%
fitBounds(-49.57,24.91,-166.99,68.00)
})
# reactive circles map
observe({
leafletProxy("map", data = reactive_data_chrono()) %>%
clearShapes() %>%
addMarkers(lng=~lng,
lat=~lat,
layerId = ~id) # Assigning df id to layerid
})
}
shinyApp(ui, server)
I am very much looking forward to any advice. Kind regards
Pernilla

I spotted three problems with your code. First, your input slider returns number(s), while your data set column Valid_Seas is character ("Jan 2021", etc.). Hence, after you apply filter the dataset is reduced to zero rows. Better use the values column instead.
Second, if you wanted to display month by month, you should pass only one single number as value argument to sliderInput, like
ui <- bootstrapPage(
tags$style(type = "text/css", "html, body {width:100%;height:100%}"),
leafletOutput("map", width = "100%", height = "100%"),
absolutePanel(top = 10, right = 10,
style="z-index:500;", # legend over my map (map z = 400)
tags$h3("Average Temperature"),
sliderInput("periode", "Months 2021",
min(tempyear21$values),
max(tempyear21$values),
value = min(tempyear21$values), # !
step = 1,
animate=TRUE, # add play button
sep = ""
)
)
)
Otherwise, you get an overlay of several months.
Third problem: your dataset has polygons, in your server function you use addMarkers. You need to use addPolygons instead. In order to fill the polygons, you need to determine a color for each number. The classInt and RColorBrewer packages can help you with that:
library(classInt)
library(RColorBrewer)
n <- 3 # number of categories
pal <- RColorBrewer::brewer.pal(n, "Reds")
ivar <- classInt::classIntervals(
tempyear21$Prob, n=n, style="quantile"
)
tempyear21$colcode <- classInt::findColours(ivar, pal)
legend_names <- names(attr(tempyear21$colcode, "table"))
As for the server function, I think you are on the right track with leafletProxy.
server <- function(input, output, session) {
# static map elements
output$map <- renderLeaflet({
leaflet() |> addTiles() |>
fitBounds(-49.57,24.91,-166.99,68.00) |>
addLegend(position="topleft", colors=pal, labels=legend_names)
})
# map handler
map_proxy <- leafletProxy("map", session)
# react on slider changes
observeEvent(input$periode, {
dat <- subset(tempyear21, values == input$periode)
map_proxy |> leaflet::clearShapes() |>
leaflet::addPolygons(
data=dat,
weight=1,
color=dat$colcode, # border
opacity=1,
fillColor=dat$colcode
)
})
}

Related

R Shiny Leaflet Server Won't Change Map Output

I'm new to making maps with Shiny but my goal is to create an interactive map that changes when you select various inputs. For example: when you select the month, it will only display markers from that given month. When you select the year it will display observations from that year AND month (and so on).
Right now I am able to get my map to render but the points that appear on the map do not correspond with what the user selects in the drop down options.
Here is the code I have tried:
library(shiny)
library(dplyr)
library(leaflet)
SampleData <- data.frame(year = c('2017', '2018', '2019', '2020'),
lon = c(38.62893, 38.62681, 38.62797, 38.62972),
lat = c(-90.26233, -90.25272, -90.26232, -90.25703),
month = c('January', 'February', 'March', 'April', 'May'),
new_use = c('Industrial', 'Institutional', 'Commercial', 'Residential')
use <- sort(unique(SampleData$new_use))
years <- sort(unique(SampleData$year))
months <- sort(unique(SampleData$month))
ui <- bootstrapPage(
tags$style(type = "text/css", "html, body {width:100%;height:100%}"),
leafletOutput("map", width = "100%", height = "100%"),
absolutePanel(top = 10, right = 10,
selectInput("month", "Month",
choices = sort(unique(SampleData$month))),
selectInput("year", "Year",
choices = sort(unique(SampleData$year))),
selectInput("new_use", "Permit Use",
choices = sort(unique(SampleData$new_use))),
)
)
server <- function(input, output, session) {
output$map <- renderLeaflet({
leaflet() %>%
setView(lng = -90.1994, lat = 38.6270, zoom = 10)%>%
addProviderTiles(providers$CartoDB.Positron)
})
# Reactive expression for the data subsetted to what the user selected
filteredData <- reactive({
dplyr::filter(SampleData, years %in% input$year & use %in% input$new_use & months %in% input$month)
})
observe({
leafletProxy("map") %>%
clearShapes() %>%
addMarkers(data = filteredData(),
~lat, ~lon, popup = paste("<b>Year:</b> ", filteredData()$year, "<br>",
"<b>Permit Type:</b> ", filteredData()$new_use, "<br>"))
})
}
shinyApp(ui, server)
Does anyone have any suggestions? What am I missing?
The filtering in the reactive statement needs to be done by the column names in the data frame. For example, you need 'year' not 'years'.
So the reactive statement should read:
filteredData <- reactive({
dplyr::filter(SampleData, year %in% input$year & new_use %in% input$new_use & month %in% input$month)
})
And to clear the previous markers you want to use clearMarkers() not clearShapes().
However with the sample dataframe provided, a marker will only appear when a specific combination year, month and use are selected (this is after removing 'May' to make all the columns the same length). For example, the first marker will only show when '2017', 'January' and 'Industrial' are all selected by the user as these are the corresponding values associated with the first pair of coordinates (and so on).

Selecting fillColor based on user input

I have a function in R that I'm using for creating a map of demographic information.
draw_demographics <- function(map, input, data) {
pal <- colorQuantile("YlGnBu", domain = NULL, n = 7)
#browser()
map %>%
clearShapes() %>%
addPolygons(data = data,
fillColor = ~pal(input$population),
fillOpacity = 0.4,
color = "#BDBDC3",
weight = 1)
}
It's a pure function that takes the map data from Leaflet, the input from the user, and the data from a shapefile to create the map layers. The columns of the shapefile include information like population density, total population, and so on, and I'd like to fill the polygons based on the column name. But where I'm a bit lost is figuring out how to pass selectInput() properly to Leaflet.
Here's a very basic example:
library(shiny)
library(leaflet)
ui <- bootstrapPage(
fluidRow(
column(12, leafletOutput("map"))
),
fluidRow(
column(12, uiOutput("select_population"))
)
)
server <- function(input, output, session) {
output$select_population <- renderUI({
choices <- list("None" = "None",
"All population" = "totalPop",
"Population density" = "totalDens",
"Black population" = "totalAfAm",
"Asian population" = "totalAsian",
"Latino population" = "totalHispanic",
"Native population" = "totalIndian")
selectInput(inputId = "population", label = "Demographics",
choices = choices, selected = "totalDens")
})
output$map <- renderLeaflet({
map <- leaflet() %>%
addProviderTiles(provider = "CartoDB.Positron",
providerTileOptions(detectRetina = FALSE,
reuseTiles = TRUE,
minZoom = 4,
maxZoom = 8)) %>%
setView(lat = 43.25, lng = -94.30, zoom = 6)
map %>% draw_demographics(input, counties[["1890"]])
})
}
## Helper functions
# draw_demographics draws the choropleth
draw_demographics <- function(map, input, data) {
pal <- colorQuantile("YlGnBu", domain = NULL, n = 7)
#browser()
map %>%
clearShapes() %>%
addPolygons(data = data,
fillColor = ~pal(input$population),
fillOpacity = 0.4,
color = "#BDBDC3",
weight = 1)
}
shinyApp(ui, server)
Where I'm a bit lost is how to pass the vector values from the column totalDens from the user's input of totalDens from the dropdown (or, pass whichever column of data they choose to map) to Leaflet. In other words, if a user selects totalPop instead, how can I tell Leaflet to reapply the color palette to this new set of data and re-render the polygons? I attempted using a reactive to get the results of input$population, but to no avail.
Any suggestions, or ways I could troubleshoot? Thanks!
With the data you posted on the github I redid it. The central problem seems to be the generation of the color palette. This is pretty fragile as it assumes that you have selected a good values for the cuts.
It needs a function that tries out various methods, see the code for details The really challenging case (that I found) was the Asian population for 1890, that was very skewed but definitely had values, and the median method always mapped everything to one color.
The following changes were made:
Added some code to download and save the counties data
Read in the data you provided
Added a field to select the year
added a req(input$population) to stop the typical shiny initialization NULL errors.
Created a getpal that tries out a different values starting on equally space quantiles.
If the number of quantiles reduces to 2, then it falls back to colorBin as colorQuantile colors everything the same in that case - probably a bug.
If there is no population data it does not draw the county shapes as that takes a lot of time, and there are a lot of those cases.
Here is the code:
library(shiny)
library(leaflet)
library(sf)
ui <- bootstrapPage(
fluidRow(
column(12, leafletOutput("map"))
),
fluidRow(
column(12, uiOutput("select_year")),
column(12, uiOutput("select_population"))
)
)
choices <- list("None" = "None",
"All population" = "totalPop",
"Population density" = "totalDens",
"Black population" = "totalAfAm",
"Asian population" = "totalAsian",
"Latino population" = "totalHispanic",
"Native population" = "totalIndian")
fn <- Sys.glob("shp/*.shp")
counties <- lapply(fn, read_sf)
names(counties) <- c("1810", "1820","1830","1840","1850","1860","1870","1880","1890","1900",
"1910","1920","1930","1940","1950","1960","1970","1980","1990","2000","2010")
server <- function(input, output, session) {
output$select_population <- renderUI({
selectInput(inputId = "population", label = "Demographics",
choices = choices, selected = "totalDens")
})
output$select_year <- renderUI({
selectInput(inputId = "year", label = "Year",
choices = names(counties))
})
output$map <- renderLeaflet({
req(input$population)
req(input$year)
map <- leaflet() %>%
addProviderTiles(provider = "CartoDB.Positron",
providerTileOptions(detectRetina = FALSE,
reuseTiles = TRUE,
minZoom = 4,
maxZoom = 8)) %>%
setView(lat = 43.25, lng = -94.30, zoom = 6)
map %>% draw_demographics(input, counties[[input$year]])
})
}
# try out various ways to get an acceptable color palette function
getpal <- function(cpop,nmax){
if (length(cpop)>1){
# try out value from nmax down to 1
for (n in nmax:1){
qpct <- 0:n/n
cpopcuts <- quantile(cpop,qpct)
# here we test to see if all the cuts are unique
if (length(unique(cpopcuts))==length(cpopcuts)){
if (n==1){
# The data is very very skewed.
# using quantiles will make everything one color in this case (bug?)
# so fall back to colorBin method
return(colorBin("YlGnBu",cpop, bins=nmax))
}
return(colorQuantile("YlGnBu", cpop, probs=qpct))
}
}
}
# if all values and methods fail make everything white
pal <- function(x) { return("white") }
}
draw_demographics <- function(map, input, data) {
cpop <- data[[input$population]]
if (length(cpop)==0) return(map) # no pop data so just return (much faster)
pal <- getpal(cpop,7)
map %>%
clearShapes() %>%
addPolygons(data = data,
fillColor = ~pal(cpop),
fillOpacity = 0.4,
color = "#BDBDC3",
weight = 1)
}
shinyApp(ui, server)
Here is the output:
The challenging case of Asian population distribution in 1890 - very highly skewed data with the population concentrated in three counties. This means that the getpal function will be forced to give up on colorQuantile and fall back on colorBin in order to show anything:

Adding multiple layers to leaflet map in Shiny in R

I am having trouble adding different layers in my shiny app. I want to add a group of polygons along with a group of circle markers along with a group of arbitrary (.png) icons. I have the group of geojson files that are added in a for loop that is wrapped in an observe({}) statement with the function
map$addGeoJSON(x) where x is a feature with coordinates. The 'map' object is created by the command
map <- createLeafletMap(session, 'map')
This is all fine and dandy, and the polygons get added fine. I also want to commit to this way of adding the polygons. That should not have to change.
The error happens when I try to add markers onto that map object in the same way (e.g. with map$addMarkers(....) ) Below is the error and the code for the app that tries to add markers in the desired way and fails.
The shiny app below with the quakes data recreates my error
"Listening on ...
Warning: Error in observerFunc: attempt to apply non-function
Stack trace (innermost first):
56: observerFunc [C:/Users/jbz/Desktop/leaflet-map-question.R#35]
1: runApp
ERROR: [on_request_read] connection reset by peer"
library(shiny)
library(leaflet)
library(RColorBrewer)
ui <- bootstrapPage(
tags$style(type = "text/css", "html, body {width:100%;height:100%}"),
leafletMap("map", width = "100%", height = "100%",
options=list(center = c(40.736, -73.99), zoom = 14)),
absolutePanel(top = 10, right = 10,
sliderInput("range", "Magnitudes", min(quakes$mag), max(quakes$mag),
value = range(quakes$mag), step = 0.1
),
selectInput("colors", "Color Scheme",
rownames(subset(brewer.pal.info, category %in% c("seq", "div")))
),
checkboxInput("legend", "Show legend", TRUE)
)
)
server <- function(input, output, session) {
filteredData <- reactive({
quakes[quakes$mag >= input$range[1] & quakes$mag <= input$range[2],]
})
colorpal <- reactive({
colorNumeric(input$colors, quakes$mag)
})
map <- createLeafletMap(session, 'map')
observe({
df <- filteredData()
map$addMarkers(
lng=df$Lon, lat=df$Lat, popup = paste(as.character(df$mag)))
})
}
shinyApp(ui, server)
(How) can I add markers correctly while insisting on using the function createLeafletMap()?
map <- createLeafletMap(session, 'map')
try:
library(dplyr)
df <- filteredData()
leafletProxy("map") %>%
addMarkers(df, lng = ~Lon, lat = ~Lat, popup = paste(as.character(df$mag) )
under observe

R Shiny - Insert a Second Reactive Slider in Shiny using leafletProxy() - Edited and with data

I am a beginner in R mapping and I am trying to build an R Shiny app to visualise Student Satisfaction and Ranking for all the universities in the UK.
Through leaflet I have mapped the university locations with markers and added sliders with popups to see the student Satisfaction Score and the Ranking (see screenshot).
The idea is to be able to choose a set of values on the sliders (say "Satisfaction from 80 to 90" and "Ranking from 1 to 30" and the app would only display the ones that fit both criteria.
The problem lies within having multiple reactive sliders. If I code the Ranking slider the same way as the Satisfaction slider, the Satisfaction slider takes on the Ranking values instead of the two sliders working together.
Below you can see my code so far with a screenshot of how it looks as well as the data (parts of the Ranking slider are commented so they don't interfere).
Any tips how to continue so the two slider don't take values from each other?
Thanks a lot and sorry if the question is very basic.
library(dplyr)
library(shiny)
library(leaflet)
mapData <- read.csv("~/Desktop/Shiny app/Csv Shiny Data Clean.csv") %>%
filter(!is.na(Latitude) & !is.na(Longitude))
ui <- bootstrapPage(
tags$style(type = "text/css", "html,
body {width:100%;height:100%}"),
leafletOutput("uniSmap", width = "100%", height = "100%"),
#slider for student satisfaction
absolutePanel(
top = 50,
right = 50,
sliderInput(
"range",
"Satisfaction Score",
min = 1,
max = 100,
value = round(range(mapData$Satisfaction.....2016.Registered, na.rm = TRUE), 1),
step = 1
)
),
#slider for Ranking
absolutePanel(
top = 200,
right = 50,
sliderInput(
"range",
"QS University Ranking",
min = 1,
max = 128,
value = round(range(mapData$QS.Ranking, na.rm = TRUE), 1),
step = 1
)
),
#bottom right title
absolutePanel(
bottom = 10,
left = 10,
"Satisfaction Map 2016"
)
)
server <- function(input, output, session) {
filteredData <- reactive({
mapData %>%
filter(Satisfaction.....2016.Registered >= input$range[1] &
Satisfaction.....2016.Registered <= input$range[2])
})
#question here: can I just do the same for Ranking Data (as below)?
# filteredDataRanking <- reactive({
# mapData %>%
# filter(QS.Ranking >= input$range[1] &
# QS.Ranking <= input$range[2])
# })
output$uniSmap <- renderLeaflet({
# as the map is only drawn once
# use non-reactive dataframe, mapData
leaflet(mapData) %>%
addTiles() %>%
fitBounds(~min(Longitude), ~min(Latitude),
~max(Longitude), ~max(Latitude))
})
# Incremental changes to the map performed in an observer.
observe({
leafletProxy("uniSmap", data = filteredData()) %>%
clearShapes() %>%
clearPopups() %>%
clearMarkers() %>%
addMarkers(lat = ~Latitude,
lng = ~Longitude,
popup = ~paste(
Institution,
"<br>",
"Overall Satisfaction:",
Satisfaction.....2016.Registered,
"<br>"
)
)
}) #end of observe for satisfaction
#would I have to create another observe for ranking data (as below)?
# observe({
#
# leafletProxy("uniSmap", data = filteredDataRanking()) %>%
#
# clearShapes() %>%
# clearPopups() %>%
# clearMarkers() %>%
#
# addMarkers(lat = ~Latitude,
# lng = ~Longitude,
# popup = ~paste(
# Institution,
# "<br>",
# "QS University Ranking",
# QS.Ranking,
# "<br>"
# )
# )
#
# }) #end of observe for Ranking
} #end of server description
shinyApp(ui = ui, server = server)
#License: a substantial portion of code is Copyright by (c) 2017 Stephen McDaniel
Screenshot of app
link to the data: csv_shiny

Changing Leaflet map according to input without redrawing

I'm wondering how I can change Shiny and Leaflet to plot points according to the change in input without redrawing the whole map.
The code i'm using is:
library(leaflet)
library(shiny)
library(dplyr)
library(readr)
ui <- fluidPage(
titlePanel("Melbourne Urban Tree Visualisation"),
leafletOutput("treedat"),
uiOutput("precinct")
#Giving an input name and listing out types to choose in the Shiny app
)
server <- function(input, output){
#td <- read.csv("treedata.csv", header = TRUE)
#pal <- colorNumeric(
#palette = "RdYlGn",
#domain = td$LifeExpectencyValue
#)
output$precinct <- renderUI({
choices <- as.character(unique(td$Precinct))
choices <- c('All', choices)
selectInput(inputId = "precinct", label = "Precinct", choices = choices, selected = "CBD")
})
output$treedat <- renderLeaflet({
#if(is.null(td)) return()
## get the choice from teh drop-down box
PRECINCT = input$precinct
## supbset the data based on the choice
if(PRECINCT != 'All'){
td2 <- td[td$Precinct == PRECINCT, ]
}else{
td2 <- td
}
## plot the subsetted ata
td2 <- leafletProxy(td2) %>% addTiles(
urlTemplate = 'http://{s}.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}.png',
attribution='Map tiles by Stamen Design, CC BY 3.0 — Map data © OpenStreetMap') %>%
addCircleMarkers(radius= 5,
fillOpacity = 0.5,
stroke = FALSE,
color=~pal(LifeExpectencyValue),
popup=paste("<b>", td$CommonName,"</b>", "<br>",
"<b>","Years Left:", "</b>", td$LifeExpectency, "<br>",
"<b>","Genus:","</b>", td$Genus)) %>% addLegend(pal = pal,
values = ~LifeExpectencyValue,
opacity = 1,
title = "Life Expectency")
return(td2)
})
}
shinyApp(ui = ui, server = server)
The dataset used for the code is available at this link - Melbourne Urban Forest Data
There are a lot of points so I wouldn't want to re-draw each time the input is changed. The input is based on the "Precinct" column in the dataset. Any help here is deeply appreciated.
Okay, there you go: leafletProxy is used to add layers to an existing leaflet map. The usage ist just like normal leaflet additions, but you don't need the rendering part, since the map is already rendered in your document.
The first and easiest part is to render the leaflet map on a basic level, that is tiles, legend, static drawings, everything that you want to do just once. This is your starting point. From there on, altering the map is only done by direct commands instead of re-renderings.
This map can now be accessed via its shiny output id. In out case, we had leafletOutput("treedat"), so if we want to address this map, we use leafletProxy("treedat"). We use the same syntax as in regular leaflet modifications. E.g. leafletProxy("treedat") %>% addMarkers(lat = 1, lng = 1) adds a marker to the existing map without re-rendering it.
Thus, every modification to the map can / has to happen from inside some observe statement and not from inside the renderLeaflet. Note that every command is an addition to the original map, which is why I had to use clearMarkers in the example below.
Code:
library(leaflet)
library(shiny)
library(dplyr)
library(readr)
ui <- fluidPage(
titlePanel("Melbourne Urban Tree Visualisation"),
leafletOutput("treedat"),
uiOutput("precinct")
#Giving an input name and listing out types to choose in the Shiny app
)
server <- function(input, output){
td <- data.frame(
LifeExpectencyValue = sample(20:100, 10),
Precinct = c(rep("CBD", 3), rep("ABC", 4), rep("XYZ", 3)),
CommonName = sapply(1:10, function(x){paste(sample(LETTERS, 10, replace = TRUE), collapse = "")}),
Genus = rep(c("m","f"), each = 5),
lat = seq(5, 50, 5),
lng = seq(2, 65, 7)
)
pal <- colorNumeric(palette = "RdYlGn", domain = td$LifeExpectencyValue)
output$precinct <- renderUI({
choices <- as.character(unique(td$Precinct))
choices <- c('All', choices)
selectInput(inputId = "precinct", label = "Precinct", choices = choices, selected = "CBD")
})
output$treedat <- renderLeaflet({
leaflet() %>%
addTiles(
urlTemplate = 'http://{s}.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}.png',
attribution='Map tiles by Stamen Design, CC BY 3.0 — Map data © OpenStreetMap'
) %>%
addLegend(pal = pal, values = td$LifeExpectencyValue, opacity = 1, title = "Life Expectency")
})
observeEvent(input$precinct, {
#if(is.null(td)) return()
## get the choice from teh drop-down box
PRECINCT = input$precinct
## supbset the data based on the choice
if(PRECINCT != 'All'){
td2 <- td[td$Precinct == PRECINCT, ]
}else{
td2 <- td
}
## plot the subsetted ata
leafletProxy("treedat") %>%
clearMarkers() %>%
addCircleMarkers(lat = td2$lat, lng = td2$lng,
radius= 5, fillOpacity = 0.5, stroke = FALSE, color=pal(td2$LifeExpectencyValue),
popup = paste("<b>", td2$CommonName,"</b>", "<br>",
"<b>","Years Left:", "</b>", td2$LifeExpectency, "<br>",
"<b>","Genus:","</b>", td2$Genus))
})
}
shinyApp(ui = ui, server = server)

Resources