I'm having some problems changing polygon styles when selecting and deselecting polygons in a Leaflet Shiny app I'm working on. In my current app, when you click on a polygon, that polygon is highlighted with a different color. Ideally, I want the user to be able to select and highlight multiple polygons. I also want the user to be able to re-click a single highlighted polygon to deselect it.
The best that I've been able to manage is to select multiple polygons, give them the same group ID "selected", then deselect that entire group when a polygon is re-clicked. Here's some example/reproducible code:
library(raster)
library(shiny)
library(leaflet)
#load shapefile
rwa <- getData("GADM", country = "RWA", level = 1)
shinyApp(
ui = fluidPage(
leafletOutput("map")
),
server <- function(input, output, session){
#initial map output
output$map <- renderLeaflet({
leaflet() %>%
addTiles() %>%
addPolygons(data = rwa,
fillColor = "white",
fillOpacity = 1,
color = "black",
stroke = T,
weight = 1,
layerId = rwa@data$OBJECTID,
group = "regions")
}) #END RENDER LEAFLET
observeEvent(input$map_shape_click, {
#create object for clicked polygon
click <- input$map_shape_click
#define leaflet proxy for second regional level map
proxy <- leafletProxy("map")
#subset regions shapefile by the clicked on polygons
selectedReg <-rwa[rwa@data$OBJECTID == click$id,]
#map clicked on polygons
proxy %>% addPolygons(data = selectedReg,
fillColor = "red",
fillOpacity = 1,
weight = 1,
color = "black",
stroke = T,
group = "selected",
# layerId = "selected")
layerId = selectedReg@data$OBJECTID)
#remove polygon group that are clicked twice
if(click$group == "selected"){
proxy %>%
clearGroup(group = "selected")
} #END CONDITIONAL
}) #END OBSERVE EVENT
}) #END SHINYAPP
In the above example, every clicked polygon turns red. If a previously-selected red polygon is clicked again, every red polygon is cleared from the map, leaving the initial white polygon renderings.
I can accomplish the desired selecting/deselecting effect when I'm working with only one polygon at a time by using the string layerId "selected" (commented out in the above code), but doing that removes my ability to select and highlight multiple polygons at the same time.
I'm open to any and all suggestions!
The answer lies in layerIds. I wasn't understanding how these were applied to my polygons and removing shapes--understanding this is key. This might not be the most elegant solution, but it gets the job done!
In the below code, the initial map rendering of Rwanda has a layerId
of rwa@data$NAME_1
, which are the region names. You can see this in action with the label
also being set as rwa@data$NAME_1
. So in the below image, the leftmost polygon is labeled as Iburengerazuba, its attribute in the NAME_1
column. This layerId sets the click$id
for any click events you have on this initial map rendering. So, just as this polygon is labeled Iburengerazuba, its click$id
will also be set as Iburengerazuba. As stated in the Leaflet Shiny documentation, if you've got more than one polygon, this needs to be a vectorized argument. If you only need to select and deselect ONE polygon (so only one region at a time, in this example), you could use a layerId
string, as I mentioned in my question (such as layerId = "selected"
).
Next up is the observeEvent
for your shape click. Thanks to the help of user @John Paul, I figured out how to save all click events (click ids specifically in this case) made on the map. I saved those in a reactive vector, then subset my shapefile by those click ids. The code is pretty thoroughly commented, so hopefully anyone else looking for this same solution can figure out exactly what's going on.
The final bit of code (housed in the if...else
conditional statement) is probably the most confusing. Let's look at the else
portion of the code first. (Note: Your initial map click is going to trigger this event because there's no way for the if
conditions to have been met upon first click.) If any white polygon is clicked, the addPolygons()
call is triggered, adding the clicked polygon onto the map with different styling (in this case, it's red). This is plotting an entirely different polygon on top of the leafletProxy
object!
The key to removing the red clicked polygons is giving these polygons a different layerId
than the initial map rendering. Note that in the above image, the white polygon that was labeled Iburengerazuba is now labeled as 3. This is because the layerId
in the second addPolygons
call is set as CC_1
INSTEAD OF NAME_1
. So, bottom layer white map has a NAME_1 layerID and therefore NAME_1 click ids, whereas any red clicked polygon plotted on top of that has a CC_1 layerId and therefore CC_1 click ids.
The if
statements states that if your click$id
already exists in the clickedPolys
polygon, that this shape is removed. This is kind of confusing, so again, it might help to go through each line of code and play around with it to truly understand.
Again using the above example, clicking the leftmost polygon adds the layerId
Iburengerazuba to the clickedIds$ids
vector. This click event triggers a second map drawing, plotting the clicked polygon on top of itself in a different style and with a layerId
of 3 (from the CC_1
column). We want to say that if any red polygon is clicked twice (if(click$id %in% clickedPolys@data$CC_1)
), it counts as a deselection, and that polygon should be removed from the map. So if you click on the red leftmost polygon with a layerId
of 3, the clickedIds$ids
vector will be comprised of Iburengerazuba
and 3
. Iburengerazuba in the NAME_1
column of the clickedPolys
polygon corresponds to 3 in the CC_1
column, triggering the if
statement. The call removeShape(layerId = click$id)
means to remove the shape that corresponds to that click$id. So in this case, the clickedPolys
polygon with a CC_1
layerId
of 3.
Keep in mind that every click id, both NAME_1
and CC_1
are being recorded in your clickedIds$ids
vector. This vector is subsetting your Rwanda shapefile to map all clicked polygons, so as you're clicking polygons, the clickedPolys
polygon is dynamically updating (use print
calls to check every bit of code if this isn't making sense to you!). Removing any double-clicked shape isn't enough to plot everything correctly--you need to remove deselected layerIds, both NAME_1 and CC_1, from the clickedIds$ids
vector. I matched each deselected CC_1 layerId
to its corresponding NAME_1
value and removed both of those attributes from the clickedIds$ids
vector so that they are removed from the clickedPolys
polygon.
Voila! Now you can select and deselect any polygons you want!
library(raster)
library(shiny)
library(leaflet)
#load shapefile
rwa <- getData("GADM", country = "RWA", level = 1)
shinyApp(
ui = fluidPage(
leafletOutput("map")
),
server <- function(input, output, session){
#create empty vector to hold all click ids
clickedIds <- reactiveValues(ids = vector())
#initial map output
output$map <- renderLeaflet({
leaflet() %>%
addTiles() %>%
addPolygons(data = rwa,
fillColor = "white",
fillOpacity = 1,
color = "black",
stroke = T,
weight = 1,
layerId = rwa@data$NAME_1,
group = "regions",
label = rwa@data$NAME_1)
}) #END RENDER LEAFLET
observeEvent(input$map_shape_click, {
#create object for clicked polygon
click <- input$map_shape_click
#define leaflet proxy for second regional level map
proxy <- leafletProxy("map")
#append all click ids in empty vector
clickedIds$ids <- c(clickedIds$ids, click$id)
#shapefile with all clicked polygons - original shapefile subsetted by all admin names from the click list
clickedPolys <- rwa[rwa@data$NAME_1 %in% clickedIds$ids, ]
#if the current click ID [from CC_1] exists in the clicked polygon (if it has been clicked twice)
if(click$id %in% clickedPolys@data$CC_1){
#define vector that subsets NAME that matches CC_1 click ID
nameMatch <- clickedPolys@data$NAME_1[clickedPolys@data$CC_1 == click$id]
#remove the current click$id AND its name match from the clickedPolys shapefile
clickedIds$ids <- clickedIds$ids[!clickedIds$ids %in% click$id]
clickedIds$ids <- clickedIds$ids[!clickedIds$ids %in% nameMatch]
#remove that highlighted polygon from the map
proxy %>% removeShape(layerId = click$id)
} else {
#map highlighted polygons
proxy %>% addPolygons(data = clickedPolys,
fillColor = "red",
fillOpacity = 1,
weight = 1,
color = "black",
stroke = T,
label = clickedPolys@data$CC_1,
layerId = clickedPolys@data$CC_1)
} #END CONDITIONAL
}) #END OBSERVE EVENT
}) #END SHINYAPP
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With