## Click to zoom in on polygon in Leaflet maps with Shiny

12 July, 2020

An easy way to add further interactivity to Leaflet maps rendered in R Shiny apps is to enable zoom-to-point functionality for your polygons. There currently isn't an out-of-the-box solution for this, but it's pretty easy to implement with a bit of manual work, as long as you don't have too many polygons.

The idea behind this implementation is to create and reference a variable that contains the coordinates of the center of each polygon. When the user clicks anywhere inside a polygon layer, we can access the polygon's `layerId` in Shiny, and match that with our reference table to get the coordinates of the polygon's center. Finally, we pass those coordinates to `setView` within a `leafletProxy` call to zoom into our desired point.

### Setup

We're going to create our own polygons so that we can make this an easily reproducible example. Therefore, we only need to load Shiny and Leaflet packages (and dplyr because I don't know how to do anything without it).

``````library(leaflet)
library(shiny)
library(dplyr)``````

Next, we'll manually create our polygons. There are a bunch of polygon classes that you can pass to Leaflet, but we're going to go as simple as it can get: a simple two-column matrix containing latitude and longitude values. We will create matrices for two shapes, and store them in a named list `my_shapes`:

``````my_shapes = list(
square_1 = matrix(c(175,176,176,175,-40,-40,-41,-41), nrow=4, ncol=2),
square_2 = matrix(c(175,176,176,175,-36,-36,-37,-37), nrow=4, ncol=2)
)``````

To load these shapes, we'll create our Leaflet map object, then loop over our list, calling `addPolygons` for each shape. An important note is that we set `layerId = shape`, so that each polygon has a unique `layerId` which will ultimately be used to get the coordinates to zoom to.

``````map <- leaflet() %>%

for (shape in names(my_shapes)) {
lng = my_shapes[[shape]][,1],
lat = my_shapes[[shape]][,2],
layerId = shape)
}``````

So we have a map with two square polygons. Next, we'll talk about creating our reference table with point coordinates.

### Creating our point references

This is the embarrassingly low-tech part of the process. Of course, for simple squares, it would be pretty easy to programatically calculate the center of each square; but most of the polygons you'll work with will be irregular shapes. So, we're going to take advantage of Shiny's `click` variables to manually specify the coordinates at the center of our polygons.

The way that we do this is to create a simple Shiny app with our Leaflet map created above. We'll create a list `my_centers` to store our coordinates, and use an `eventReactive` that responds to `input\$map_shape_click` to record the values. More information on the interpretation of `map_shape_click` can be found in the docs, but the general idea is that Leaflet can send interactions with the map to Shiny's backend in the form of `input\$MAPID_OBJCATEGORY_EVENTNAME`. In this case, `MAPID = map`, `OBJCATEGORY = shape` (i.e., our polygon shape), and `EVENTNAME = click` (our mouse click). Here's what our observer looks like:

``````my_centers <<- list()

output\$coords <- eventReactive(input\$map_shape_click, {
my_centers[[input\$map_shape_click\$id]] <<- input\$map_shape_click
})``````

The idea here is to start our app, and sequentially click on the center of the polygons. Each click will be recorded in our observer as a new element in the `my_centers` list. The names of each list element are `input\$map_shape_click\$id`, which turns out to be the names of the polygons that we initially provided in `my_shapes`: "square_1" and "square_2". We're assigning our list to the parent environment (which happens to be the global environment), so we can access this data after closing the app.

So, let's put this all together. For some reason that I don't understand, we also need to render the text of our observer in the UI (here we do that with `textOutput("coords")`). Let's see what our Shiny app looks like now:

``````library(leaflet)
library(shiny)

my_shapes <- list(
square_1 = matrix(c(175,176,176,175,-40,-40,-41,-41), nrow=4, ncol=2),
square_2 = matrix(c(175,176,176,175,-36,-36,-37,-37), nrow=4, ncol=2)
)

my_centers <<- list()

ui <- {
fluidPage(
fluidRow(
leafletOutput("map"),
textOutput("coords")
)
)
}

server <- function(input, output, session) {

output\$map <- renderLeaflet({

map <- leaflet() %>%

for (shape in names(my_shapes)) {
lng = my_shapes[[shape]][,1],
lat = my_shapes[[shape]][,2],
layerId = shape)
}

map

})

output\$coords <- eventReactive(input\$map_shape_click, {
my_centers[[input\$map_shape_click\$id]] <- input\$map_shape_click
})
}``````

Run the app with `shinyApp(ui, server)`, and simply click on the centers of the polygons. Then close the app. Examine `my_centers`:

``````str(my_centers)
List of 2
\$ square_2:List of 4
..\$ id    : chr "square_2"
..\$ .nonce: num 0.373
..\$ lat   : num -36.5
..\$ lng   : num 176
\$ square_1:List of 4
..\$ id    : chr "square_1"
..\$ .nonce: num 0.0413
..\$ lat   : num -40.5
..\$ lng   : num 176``````

We have a named list of length 2, with `lat` and `lng` values for each of our polygons (corresponding to the coordinates that we clicked on in our method above). Great! Now all we need to do is convert those into a clean, usable reference table. There are definitely cleaner and quicker ways of doing this, but I like writing loops, so...

``````for (i in 1:length(my_centers)) {
this_obj <- my_centers[[i]]
lat <- this_obj\$lat
lng <- this_obj\$lng
this_obj_df <- data.frame(
id = names(my_centers)[i],
lat = lat,
lng = lng)

if (!exists('reference_table')) {
reference_table <- this_obj_df
} else {
reference_table <- bind_rows(reference_table, this_obj_df)
}
}``````

Now we have a reference table, containing the ID of each of our polygons, as well as the latitute and longitude values of their centers (as defined by our mouse clicking!).

The last piece of the puzzle is to use our reference table to update the field of view of our map. This involves using the `input\$map_shape_click` that we used above, and matching the `id` that we get from that with our reference table to retrieve the coordinates. Once we have the coordinates, we pass them to `setView` in our call to a `leafletProxy`, and voila—zoom to point.

### Zooming to point

At this point, what I've done in the past is write my reference table out as a csv file so that I can reuse it without having to actually produce those lat/lng values again. But either way, let's keep or get this object in the environment because we're going to need it when we run out app.

``````write.csv(reference_table, "reference-table.csv", row.names = FALSE)

The key part here is to set up an observer that looks for `input\$map_shape_click`, and use that to update our map. We'll store our returned object in a variable that we'll call `click`; we'll get the relevant entry in our reference table and store it in `this_shape`; and then we'll set the view of our map using those data. The observer looks like this:

``````observeEvent(input\$map_shape_click, {

click <- input\$map_shape_click

this_shape <- reference_table[match(click\$id, reference_table\$id),]

leafletProxy("map") %>%
setView(
lng = this_shape\$lng,
lat = this_shape\$lat,
zoom = 8)
})``````

Note that we set `zoom = 8`. We can set this to whatever we want, and if you want to get fancy, you can set a different zoom level for each of your shapes—simply add `zoom` as a variable in your reference table, and reference it in the same way we reference longitude and latitude.

And that's it! Now when we run our Shiny app, clicking on any point in a polygon will zoom to the middle of it. The full code for the app is here:

``````library(leaflet)
library(shiny)

my_shapes = list(
square_1 = matrix(c(175,176,176,175,-40,-40,-41,-41), nrow=4, ncol=2),
square_2 = matrix(c(175,176,176,175,-36,-36,-37,-37), nrow=4, ncol=2)
)

# read in the reference coordinates if you've written them somewhere

my_centers <<- list()

ui <- {
fluidPage(
fluidRow(
leafletOutput("map")
)
)
}

server <- function(input, output, session) {

output\$map <- renderLeaflet({

map <- leaflet() %>%

for (shape in names(my_shapes)) {
lng = my_shapes[[shape]][,1],
lat = my_shapes[[shape]][,2],
layerId = shape)
}

map

})

observeEvent(input\$map_shape_click, {

click <- input\$map_shape_click

this_shape <- reference_table[match(click\$id, reference_table\$id),]

leafletProxy("map") %>%
setView(
lng = this_shape\$lng,
lat = this_shape\$lat,
zoom = 8)
})
}

shinyApp(ui, server)``````

Final thanks to SymbolixAU for this great Stack Overflow answer that inspired this post.