inputMap()
Spatial vizualizations and especially interactive ones, are great tools to communicate physical location data. Building tools like this with R is possible via the leaflet
and shiny
packages. This post shows how to combine these libraries, to create a map input widget for a Shiny app.
In this example I’ll build a map of counties in North Carolina (using the data that comes in the sf
library) that listens for click events and filters the data set based on the selected county.
1: Make the map
I’m using R’s library sf
to handle the spatial data here, but there are alternatives. leaflet
maps will work with a variaty of other formats, including matrix
/data.frame
, sp
objects, and map
objects.
library(sf)
## Linking to GEOS 3.7.2, GDAL 2.4.2, PROJ 5.2.0
nc <- st_read(system.file("shape/nc.shp", package="sf"))
## Reading layer `nc' from data source `/Users/nathanday/Library/Application Support/renv/cache/v4/R-3.6/x86_64-apple-darwin15.6.0/sf/0.9-3/527e4f44640fa29d7a9174a4523d726e/sf/shape/nc.shp' using driver `ESRI Shapefile'
## Simple feature collection with 100 features and 14 fields
## geometry type: MULTIPOLYGON
## dimension: XY
## bbox: xmin: -84.32385 ymin: 33.88199 xmax: -75.45698 ymax: 36.58965
## CRS: 4267
library(leaflet)
map <- leaflet(nc,
options = leafletOptions(
zoomControl = FALSE,
dragging = FALSE,
minZoom = 6,
maxZoom = 6)
) %>%
addPolygons(
label = ~NAME,
highlight = highlightOptions(
fillOpacity = 1,
bringToFront = TRUE)
)
## Warning: sf layer has inconsistent datum (+proj=longlat +datum=NAD27 +no_defs ).
## Need '+proj=longlat +datum=WGS84'
widgetframe::frameWidget(map, height = '400')
The leafletOptions()
arguments here are important because I don’t want the input map moving. If you want this “style”, you will have to adjust the minZoom
/maxZoom
values, depending on the size of the map your select. The highlightOptions()
and label
args make the map better as a selection tool, IMO.
2: Add map to app
Using the standard leaflet
patterns, we add a renderLeaflet()
, to contruct the map inside the server
. Remember that nc
must be available for map to be built, but should probably be read in outside of the server
.
output$inputMap <- renderLeaflet({
leaflet(nc,
options = leafletOptions(
zoomControl = FALSE,
dragging = FALSE,
minZoom = 6,
maxZoom = 6)
) %>%
addPolygons(
layerID = ~NAME, # note this is new
label = ~NAME,
highlight = highlightOptions(
fillOpacity = 1,
bringToFront = TRUE)
)
})
Note the layerID
argument is essential for next step and I reccomend making this the same variable you want to filter the data set with.
Now in ui
use leafletOutput()
to create the container to display the map. This would be located with the other input widgets, maybe in a sidebar or fluid row.
leafletOutput("inputMap", height = 200)
3: Respond to clicks
To access leaflet
events there is a special syntax input$MAPID_OBJECT_EVENT
, because of this, you shouldn’t use "_"s in your MAPID
.
Following the Shiny pattern to watch for changes, we set up observer in server
to listen for the leaflet click event.
observeEvent(input$inputMap_shape_click, {
click <- input$inputMap_shape_click
req(click)
rv$nc <- filter(nc, NAME == click$id)
leafletProxy("inputMap", session, data = rv$nc) %>%
removeShape("selected") %>%
addPolygons(layerId = "selectecd",
fillColor = "red",
fillOpacity = 1)
})
In this observer we are doing two separte things, filtering the display data and updating the input map. I’m using rv
, which is a reactiveValues()
object, to hold the filtered data, but you can use any type of reactive object. Updating the input map is just visual sugar, not necessary for the filtering to occur, but adds a nice touch. Using a layerId
is critical for the removal step to work properly.
And that’s it, now you have a map you can use as an input widget.