An inputMap for your Shiny app

May 5, 2019 - 3 minutes
data viz leaflet shiny sf

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.

Working app

Is the weather getting wetter?

February 20, 2019 - 3 minutes
Exploring historical data from 1905 to 2015 from the World Bank
weather lm sf tidyverse

Geocoded crime reports for Charlottesville Virginia

November 27, 2018 - 5 minutes
Civic Data packages sf tidyverse

Extending R's GTFS abilities with simple features

June 2, 2018 - 9 minutes
Two functions to easily transition from library(gtfsr) into library(sf) for tidyier transit analysis.
Civic Data sf gtfsr tidyverse