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.
Do you have any suggestions or ways I could troubleshoot?
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:
req(input$population)
to stop the typical shiny initialization NULL errors.getpal
that tries out a different values starting on equally space quantiles.colorBin
as colorQuantile
colors everything the same in that case - probably a bug.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: