rshinyr-leaflet

Zoom to selected province, add polylines from shapefiles, and highlight clicked marker


I'm building a Shiny app that visualizes rainfall data using a Leaflet map. Here's what I want to achieve:

1. Zoom to a Province When Selected When a province is selected from a dropdown, I want the map to automatically zoom to that province's boundary. I have a shapefile of Indonesian provinces loaded as provinsi_sf. I’m using leafletProxy() and st_bbox() to get bounding boxes, but I’m unsure how to set the correct zoom and center.

2. Add Polylines from Shapefiles I have three shapefiles:

How can I overlay these on the Leaflet map as polylines or polygons in different colors?

3. Highlight Clicked Station : I’m plotting weather stations as markers.

When one is clicked, I want:

Data:

https://drive.google.com/drive/folders/19R9cVcBbLOvzzlxwkIEYc4V51e6it6gu?usp=drive_link

library(shiny)
library(leaflet)
library(leaflet.extras)
library(dplyr)
library(ggplot2)
library(plotly)
library(sf)

## -------------------------------
## Data 
## -------------------------------
shp_gambut <- readRDS("Gambut/gambut.rds")
provinsi_sf <- readRDS("indonesia.rds")
kabupaten_sf <- readRDS("kab.rds")
bmkg_st <- readRDS("bmkg_st.rds")

## -------------------------------
## UI
## -------------------------------
ui <- fluidPage(
  titlePanel("Monitoring Stasiun Cuaca"),
  sidebarLayout(
    sidebarPanel(
      selectInput("prov", "Select Province", choices = unique(bmkg_st$provinsi)),
      uiOutput("kabupaten_ui"),
      selectInput("scale_id", "Skala Waktu:",
                  choices = c("Harian" = "Daily", 
                              "Bulanan" = "Monthly", 
                              "Tahunan" = "Yearly"),
                  selected = "Daily")
    ),
    mainPanel(
      leafletOutput("mymap", height = "500px"),
      plotlyOutput("rainPlot", height = "300px")
    )
  )
)

## -------------------------------
## Server
## -------------------------------
server <- function(input, output, session) {
  
  # Update pilihan kabupaten berdasarkan provinsi
  output$kabupaten_ui <- renderUI({
    req(input$prov)  # Ensure that the prov input is available
    kab <- bmkg_st %>% filter(provinsi == input$prov)
    selectInput("kab", "Pilih Kabupaten:", choices = unique(kab$kabupaten))
  })
  
  # Ambil geometri provinsi yang dipilih
  selected_prov_geom <- reactive({
    provinsi_sf %>% filter(name == input$prov)
  })
  
  # Render peta awal
  output$mymap <- renderLeaflet({
    leaflet() %>%
      addProviderTiles(providers$CartoDB.Positron) %>%
      setView(lng = 108, lat = -2, zoom = 5.5)
  })
  
  # Update peta ketika provinsi berubah
  observe({
    prov_geom <- selected_prov_geom()
    leafletProxy("mymap") %>%
      clearShapes() %>%
      clearMarkers() %>%
      addPolygons(data = prov_geom,
                  fillColor = "lightgrey",
                  color = "brown", weight = 2, fillOpacity = 0.3) %>%
      addMarkers(data = bmkg_st %>% filter(provinsi == input$prov),
                 lng = ~x, lat = ~y,
                 popup = ~ID,
                 layerId = ~WMO,
                 icon = awesomeIcons(icon = 'cloud', markerColor = 'blue'))
  })
  
  # Filter dan agregasi data sesuai skala waktu
  filtered_scaled_data <- reactive({
    req(input$prov)  # Ensure prov input is available
    data <- bmkg_st %>%
      filter(provinsi == input$prov)
    
    if (input$scale_id == "Daily") {
      return(data)
    } else if (input$scale_id == "Monthly") {
      return(data %>%
               mutate(Year = format(date, "%Y"),
                      Month = format(date, "%m"),
                      Month = factor(Month, levels = sprintf("%02d", 1:12))) %>%
               group_by(Year, Month, WMO, ID) %>%
               summarise(rain = sum(rain, na.rm = TRUE), .groups = 'drop'))
    } else {
      return(data %>%
               mutate(Year = format(date, "%Y")) %>%
               group_by(Year, WMO, ID) %>%
               summarise(rain = sum(rain, na.rm = TRUE), .groups = 'drop'))
    }
  })
  
  # Tampilkan plot ketika marker diklik
  observeEvent(input$mymap_marker_click, {
    click <- input$mymap_marker_click
    req(click$id)
    
    data <- filtered_scaled_data() %>%
      filter(WMO == as.numeric(click$id))
    
    output$rainPlot <- renderPlotly({
      if (input$scale_id == "Daily") {
        p <- ggplot(data, aes(x = date, y = rain)) +
          geom_line() +
          labs(title = "Curah Hujan Harian", x = "Tanggal", y = "Curah Hujan (mm)") +
          theme_minimal()
        
      } else if (input$scale_id == "Monthly") {
        p <- ggplot(data, aes(x = Month, y = rain, group = Year, color = Year)) +
          geom_line() +
          labs(title = "Curah Hujan Bulanan", x = "Bulan", y = "Curah Hujan (mm)") +
          theme_minimal()
        
      } else {
        p <- ggplot(data, aes(x = Year, y = rain)) +
          geom_col(fill = "steelblue") +
          labs(title = "Curah Hujan Tahunan", x = "Tahun", y = "Curah Hujan (mm)") +
          theme_minimal()
      }
      
      # Explicitly define the trace type and mode to avoid warnings
      ggplotly(p, tooltip = "text") %>%
        layout(
          title = list(text = "Rainfall Plot"),
          showlegend = FALSE,
          xaxis = list(title = "Date"),
          yaxis = list(title = "Rainfall (mm)")
        ) %>%
        config(displayModeBar = FALSE)  # To hide modebar if not needed
    })
  })
  
}
shinyApp(ui, server)


Solution

    1. To zoom into a particular polygon, you can find its bounding box and then use fitBoundary() to zoom in. For example:
        prov_geom <- selected_prov_geom()
        z <- unname(st_bbox(prov_geom))
        leafletProxy("mymap") %>%
          clearShapes() %>%
          clearMarkers() %>%
          addPolygons(data = prov_geom,
                      fillColor = "lightgrey",
                      color = "brown", weight = 2, fillOpacity = 0.3)  %>% 
          addMarkers(data = bmkg_st %>% filter(provinsi == input$prov) %>% slice_head(n=1),
                     lng = ~x, lat = ~y,
                     popup = ~ID,
                     layerId = ~WMO,
                     icon = list(iconUrl = "www/blue_cloud.svg", iconSize=c(15,15))) %>% 
          fitBounds(lng1 = z[1], lat1 = z[2], lng2 = z[3], lat2 = z[4])
    

    When you pick a province, it will zoom into that one.

    1. To add the different polygons or lines, you could just include a checkbox group to identify which lines you want and then plot them on the map:

    UI

    checkboxGroupInput("boundaries", 
                       "Boundaries to Show", 
                       choices = c("Province", "District", "Peat Area"), 
                       selected = NULL)
    

    Server

    output$mymap <- renderLeaflet({
        map <- leaflet() %>%
          addProviderTiles(providers$CartoDB.Positron) %>%
          setView(lng = 108, lat = -2, zoom = 5.5)  
        
        if("Province" %in% input$boundaries){
          map <- map %>% 
            addPolygons(data=provinsi_sf,  weight=2, fill=FALSE, color="green")
        }
        if("District" %in% input$boundaries){
          map <- map %>% 
            addPolygons(data=kabupaten_sf,  weight=2, fill=FALSE, color="red")
        }
        if("Peat Area" %in% input$boundaries){
          map <- map %>% 
            addPolygons(data=shp_gambut,  weight=2, fill=FALSE, color="blue")
        }
        map
      })
    
    1. Change marker color. This is the hardest one because the markers are not points with a color attribute that can be changed via javascript. They are images that are re-encoded by leaflet, so they don't even point to the image you specified in the code. What I did, was I downloaded the svg of a blue cloud and red cloud and saved them in the folder called www as blue_cloud.svg and red_cloud.svg. I have the markers print as blue initially.
     addMarkers(data = bmkg_st %>% filter(provinsi == input$prov),
                     lng = ~x, lat = ~y,
                     popup = ~ID,
                     layerId = ~WMO,
                     icon = list(iconUrl = "www/blue_cloud.svg", iconSize=c(15,15)))
    

    However, when you select one, I have it change that marker to the red cloud:

      observeEvent(input$mymap_marker_click, {
        leafletProxy("mymap") %>%
          removeMarker(layerId = input$mymap_marker_click$id) %>% 
          addMarkers(data = bmkg_st %>% filter(provinsi == input$prov) %>% filter(WMO == input$mymap_marker_click$id), 
                    lng = ~x, lat = ~y,
                    popup = ~ID,
                    layerId = ~WMO,
                    icon = list(iconUrl = "www/red_cloud.svg", iconSize=c(15,15)))
      })
    

    Note, in the code below, I used slice_head(n=1) to have the map render faster for debugging. You'll have to make sure all this works with the full data as you intend. One potential problem is that the data you're plotting have lots of days for the same station. If you're just using it for people to select a station, perhaps plotting the markers as one one observation per station would speed up the process.

    library(shiny)
    library(leaflet)
    library(leaflet.extras)
    library(dplyr)
    library(ggplot2)
    library(plotly)
    library(sf)
    
    ## -------------------------------
    ## Data 
    ## -------------------------------
    shp_gambut <- readRDS("gambut.rds")
    provinsi_sf <- readRDS("indonesia.rds")
    kabupaten_sf <- readRDS("kab.rds")
    bmkg_st <- readRDS("bmkg_st.rds")
    # input <- list(prov = "Bengkulu", scalse_id = "Yearly", kab = "Bengkulu")
    ## -------------------------------
    ## UI
    ## -------------------------------
    ui <- fluidPage(
      titlePanel("Monitoring Stasiun Cuaca"),
      actionButton("browser", "Browse"), 
      sidebarLayout(
        sidebarPanel(
          selectInput("prov", "Select Province", choices = unique(bmkg_st$provinsi)),
          uiOutput("kabupaten_ui"),
          selectInput("scale_id", "Skala Waktu:",
                      choices = c("Harian" = "Daily", 
                                  "Bulanan" = "Monthly", 
                                  "Tahunan" = "Yearly"),
                      selected = "Daily"), 
          checkboxGroupInput("boundaries", 
                             "Boundaries to Show", 
                             choices = c("Province", "District", "Peat Area"), 
                             selected = NULL)
                            
          
        ),
        mainPanel(
          leafletOutput("mymap", height = "500px"),
          plotlyOutput("rainPlot", height = "300px")
        )
      )
    )
    
    ## -------------------------------
    ## Server
    ## -------------------------------
    server <- function(input, output, session) {
      observeEvent(input$browser, {
        browser()
      })
      # Update pilihan kabupaten berdasarkan provinsi
      output$kabupaten_ui <- renderUI({
        req(input$prov)  # Ensure that the prov input is available
        kab <- bmkg_st %>% filter(provinsi == input$prov)
        selectInput("kab", "Pilih Kabupaten:", choices = unique(kab$kabupaten))
      })
      
      # Ambil geometri provinsi yang dipilih
      selected_prov_geom <- reactive({
        provinsi_sf %>% filter(name == input$prov)
      })
      
      # Render peta awal
      output$mymap <- renderLeaflet({
        map <- leaflet() %>%
          addProviderTiles(providers$CartoDB.Positron) %>%
          setView(lng = 108, lat = -2, zoom = 5.5)  
        
        if("Province" %in% input$boundaries){
          map <- map %>% 
            addPolygons(data=provinsi_sf,  weight=2, fill=FALSE, color="green")
        }
        if("District" %in% input$boundaries){
          map <- map %>% 
            addPolygons(data=kabupaten_sf,  weight=2, fill=FALSE, color="red")
        }
        if("Peat Area" %in% input$boundaries){
          map <- map %>% 
            addPolygons(data=shp_gambut,  weight=2, fill=FALSE, color="blue")
        }
        map
      })
      
    #  observeEvent()
      
      
      # Update peta ketika provinsi berubah
      observe({
        prov_geom <- selected_prov_geom()
        z <- unname(st_bbox(prov_geom))
        leafletProxy("mymap") %>%
          clearShapes() %>%
          clearMarkers() %>%
          addPolygons(data = prov_geom,
                      fillColor = "lightgrey",
                      color = "brown", weight = 2, fillOpacity = 0.3)  %>% 
          addMarkers(data = bmkg_st %>% filter(provinsi == input$prov) %>% slice_head(n=1),
                     lng = ~x, lat = ~y,
                     popup = ~ID,
                     layerId = ~WMO,
                     icon = list(iconUrl = "www/blue_cloud.svg", iconSize=c(15,15))) %>% 
          fitBounds(lng1 = z[1], lat1 = z[2], lng2 = z[3], lat2 = z[4])
      })
      
      observeEvent(input$mymap_marker_click, {
        leafletProxy("mymap") %>%
          removeMarker(layerId = input$mymap_marker_click$id) %>% 
          addMarkers(data = bmkg_st %>% filter(provinsi == input$prov) %>% slice_head(n=1) %>% filter(WMO == input$mymap_marker_click$id), 
                    lng = ~x, lat = ~y,
                    popup = ~ID,
                    layerId = ~WMO,
                    icon = list(iconUrl = "www/red_cloud.svg", iconSize=c(15,15)))
      })
      
      
      # Filter dan agregasi data sesuai skala waktu
      filtered_scaled_data <- reactive({
        req(input$prov)  # Ensure prov input is available
        data <- bmkg_st %>%
          filter(provinsi == input$prov)
        
        if (input$scale_id == "Daily") {
          return(data)
        } else if (input$scale_id == "Monthly") {
          return(data %>%
                   mutate(Year = format(date, "%Y"),
                          Month = format(date, "%m"),
                          Month = factor(Month, levels = sprintf("%02d", 1:12))) %>%
                   group_by(Year, Month, WMO, ID) %>%
                   summarise(rain = sum(rain, na.rm = TRUE), .groups = 'drop'))
        } else {
          return(data %>%
                   mutate(Year = format(date, "%Y")) %>%
                   group_by(Year, WMO, ID) %>%
                   summarise(rain = sum(rain, na.rm = TRUE), .groups = 'drop'))
        }
      })
      
      # Tampilkan plot ketika marker diklik
      observeEvent(input$mymap_marker_click, {
        click <- input$mymap_marker_click
        req(click$id)
        
        data <- filtered_scaled_data() %>%
          filter(WMO == as.numeric(click$id))
        
        output$rainPlot <- renderPlotly({
          if (input$scale_id == "Daily") {
            p <- ggplot(data, aes(x = date, y = rain)) +
              geom_line() +
              labs(title = "Curah Hujan Harian", x = "Tanggal", y = "Curah Hujan (mm)") +
              theme_minimal()
            
          } else if (input$scale_id == "Monthly") {
            p <- ggplot(data, aes(x = Month, y = rain, group = Year, color = Year)) +
              geom_line() +
              labs(title = "Curah Hujan Bulanan", x = "Bulan", y = "Curah Hujan (mm)") +
              theme_minimal()
            
          } else {
            p <- ggplot(data, aes(x = Year, y = rain)) +
              geom_col(fill = "steelblue") +
              labs(title = "Curah Hujan Tahunan", x = "Tahun", y = "Curah Hujan (mm)") +
              theme_minimal()
          }
          
          # Explicitly define the trace type and mode to avoid warnings
          ggplotly(p, tooltip = "text") %>%
            layout(
              title = list(text = "Rainfall Plot"),
              showlegend = FALSE,
              xaxis = list(title = "Date"),
              yaxis = list(title = "Rainfall (mm)")
            ) %>%
            config(displayModeBar = FALSE)  # To hide modebar if not needed
        })
      })
      
    }
    shinyApp(ui, server)