leaflet icon indicating copy to clipboard operation
leaflet copied to clipboard

Leaflet map fails to recognize flexible UI

Open tiernanmartin opened this issue 9 years ago • 12 comments

I noticed that leaflet maps embedded in shinydashboards fail to render fully after their container expands horizontally.

Here's a reproducible example (hint: click the hamburger icon to minimize the sidebar, then try exploring the leaflet map at different zoom levels and locations).

leaflet_bug_ss (The gray space on the right should be leaflet map)

tiernanmartin avatar Mar 08 '16 06:03 tiernanmartin

The SuperZip example could be one solution for your problem: http://shiny.rstudio.com/gallery/superzip-example.html.

Try this gist: https://gist.github.com/byzheng/7e38d36cc260fff5cba3

byzheng avatar Mar 08 '16 07:03 byzheng

@byzheng Thanks! This is a pretty good work-around. I suppose that keeping the map at full width regardless of what the sidebar is doing is better than what I had before.

That said, this solution is still unideal with the example's UI design because the way the map is centered leaves much of the content obscured by the sidebar. This gets annoying if you have lots of buttons that allow the user to zoom to different locations (which is part of my actual project design).

Perhaps this is better left for SO, but I'm wondering if there's a way to get the leaflet map to adjust its width dynamically (i.e., when the sidebar is collapsed or expanded) without redrawing the map. This was the behavior I was hoping for.

tiernanmartin avatar Mar 08 '16 17:03 tiernanmartin

I have the same problem or feature request, but cannot figure out one way to solve it. It may be require some knowledge of css.

byzheng avatar Mar 08 '16 20:03 byzheng

This is happening because htmlwidgets only detect size changes when 1) the browser window size actually changes, or 2) a "shown" or "hidden" jQuery event is triggered somewhere on the document. @wch can we modify shinydashboard to do that when the sidebar is shown/hidden?

In the meantime, here's a workaround you can insert into your UI (anywhere):

tags$script(
  '$(".sidebar-toggle").on("click", function() { $(this).trigger("shown"); });'
)

This is a bit sloppy in that it's not triggering the right event (shown/hidden) on the right element (should be the sidebar container itself, not the sidebar toggle button) but it does work.

jcheng5 avatar Mar 08 '16 20:03 jcheng5

@jcheng5 Thanks. It works very well.

byzheng avatar Mar 08 '16 20:03 byzheng

@jcheng5 Can we make the height of leaflet element into 100% to use the remain space and dynamic resize?

See example here which I can only set the height into 90vh: https://gist.github.com/byzheng/7e38d36cc260fff5cba3

byzheng avatar Mar 08 '16 21:03 byzheng

You can give the map's outer div 0px padding and make the map's div position: fixed. You can also assign multiple boostrap 3 classes to the containers to make it responsive. More information.

ui.R

shinyUI(fluidPage(
  includeCSS("www/styles.css"),
  fluidRow(
    h3("Title", id = "app-title")
  ),
  fluidRow(
    column(4,
      h4("Menu"),     
      class = "col-lg-2 col-md-4", id = "menu-div"
    ),
    column(8,
      leafletOutput("mainmap", width = "100%", height = "100%"),
      class = "col-lg-10 col-md-8", id = "map-div"
    )
  )
))

www/styles.css

#mainmap {
  position: fixed;
}

#map-div {
  padding: 0px;
}

#app-title {
  float: right;
  margin-right: 4px;
  text-align: center;
  margin-top: 2px;
  margin-bottom: 0px;
}

Note: I didn't use shinydashboard, but hopefully this helps. Adding bars and using that to show and hide the menu here would be adding bsButton("menuShown", "", icon = icon("bars"), type = "toggle", value = TRUE), (from shinyBS) and useShinyjs(), to the ui.R. and this in server.R:

  observeEvent(input$menuShown, {
    if (input$menuShown) {
      shinyjs::show("menu-div")
    } else {
      shinyjs::hide("menu-div")
    }
  })

chris-holcomb avatar Mar 08 '16 22:03 chris-holcomb

@jcheng5 Thanks for the workaround – works perfectly!

tiernanmartin avatar Mar 08 '16 22:03 tiernanmartin

@ideamotor Thanks for your suggestion. I found the padding in my previous gist comes from section.content. Now I can make the leaflet map into all available spaces. See here: https://gist.github.com/byzheng/7e38d36cc260fff5cba3

I am just not sure about the height of header (50px comes from the Chrome develop tools).

byzheng avatar Mar 08 '16 22:03 byzheng

So if you use what I shared, your map will have the wrong map_bounds, and fitBounds and setView won't work properly. Here's a partially working fix which non-ideally uses CSS calc.

library(shiny);library(leaflet);library(shinyBS);library(shinyjs)
ui <- shinyUI(fluidPage(
  tags$head(
    tags$title("appTitle"),
    tags$style(HTML("
    #mainmap {
       position: relative;
       height: calc(100vh - 32px) !important;
    }
    #map-div {
      padding: 0px;
    }
    #appTitle {
      float: right;
      margin-right: 4px;
      text-align: center;
      margin-top: 2px;
      margin-bottom: 0px;
    }
    #menuShown {
      border: none;
      float: right;
      border-radius: 0px;
      margin: 0px;
    }
    @media(max-width:767px) {
      #map-div {
        height: 100vh;
        z-index: 1;
      }
      #mainmap {
        position: relative;
      }
      #menu-div {
        z-index: 2;
        position: absolute;
        background-color: white;
      }
    }
    "))
    ),
  useShinyjs(),
  fluidRow(
    bsButton("menuShown", "", icon = icon("bars"), type = "toggle", value = TRUE),
    h3("appTitle", id = "appTitle")
  ),
  fluidRow(
    column(4,
           h4("Menu"),     
           class = "col-lg-2 col-md-4", id = "menu-div"
    ),
    column(8,
           leafletOutput("mainmap", width = "100%", height = "100%"),
           class = "", id = "map-div"
    )
  )
    ))
server <- shinyServer(function(input, output, session) {
  output$mainmap <- renderLeaflet({
    leaflet("mainmap") %>% addTiles() %>% setView(lng = -97.743, lat = 30.267, zoom = 13)
  })
  observeEvent(input$menuShown, {
    if (input$menuShown) {
      shinyjs::show("menu-div")
      addClass("map-div", "col-lg-10 col-md-8 col-sm-8")
      leafletProxy("mainmap") %>% setView(lng = -97.743, lat = 30.267, zoom = 13)
    } else {
      shinyjs::hide("menu-div")
      removeClass("map-div", "col-lg-10 col-md-8 col-sm-8")
      leafletProxy("mainmap") %>% setView(lng = -97.743, lat = 30.267, zoom = 13)
    }
  })
})
shinyApp(ui,server)

The issue is that when you close the menu, the map is still thinking it has it's old size and location. So when we setView again, it's off-center. Additionally, the map has a hard time rendering on the right side. I believe we need a function that just calls Leafet's invalidateSize() on a specified map at any necessary time.

chris-holcomb avatar Mar 18 '16 22:03 chris-holcomb

I think this may have been solved by rstudio/shinydashboard#185 (not yet on CRAN)

bborgesr avatar Mar 03 '17 05:03 bborgesr

Not sure if this is related, or a separate bug, but when trying to use CSS calc(), leaflet spits an error.

  leafletOutput("map", height ="calc(100vh - 80px)"),

throws:

ERROR: "calc(100vh - 80px)" is not a valid CSS unit (e.g., "100%", "400px", "auto")

Which seems incorrect. Leaflet should pass that through. Note "100vh" works fine.

naught101 avatar Jul 29 '17 04:07 naught101