zauberzeug / nicegui

Create web-based user interfaces with Python. The nice way.
https://nicegui.io
MIT License
8.8k stars 535 forks source link

ui.leaflet and bind_visibility: tiles not loading #2338

Open kleynjan opened 8 months ago

kleynjan commented 8 months ago

Description

  1. Using ui.leaflet, create a map that is initially hidden and later (when location etc are known) made visible through bind_visibility.
  2. Expected behavior: when the bound value changes to True, the map is shown with the given center & zoom.
  3. Instead, the map tiles are not shown -- they appear only if/when the window is resized.

There seems to be a known issue with Leaflet when the container size is not known when initializing, but I don't know how to further isolate it for Nicegui. Likely a CSS issue?

See example below. Both maps are initially hidden. The left map is then made visible using tailwind classes, that works. Visibility for the map on the right hand side is enabled with bind_visibility; the map tiles are not correctly shown.

Screenshot 2024-01-10 at 16 32 39
from nicegui import ui

def toggle_visibility():
    a, r = ("visible", "invisible") if onoff1.value else ("invisible", "visible")
    mapcard.classes(add=a, remove=r)

onoff1 = ui.switch("via tailwind classes", value=False, on_change=toggle_visibility)

onoff2 = ui.switch("via bind_visibility", value=False)

with ui.card().classes("w-full flex flex-row"):
    with ui.card().classes("w-1/3 flex invisible") as mapcard:
        m = ui.leaflet(center=(51.505, -0.09)).classes("h-screen visible")

    with ui.card().classes("w-1/3 flex").bind_visibility_from(onoff2, "value"):
        m = ui.leaflet(center=(51.505, -0.09)).classes("h-screen")

ui.run()

The flex classes are only for display here, AFAICT the difference between the two methods remains with all styling I've tried. As a workaround I can work with the Tailwind classes, but that is of course a bit more clumsy.

falkoschindler commented 7 months ago

Hi @kleynjan,

This is indeed an annoying bug we also noticed in one of our projects. I took the opportunity to further investigate different methods to show and hide a Leaflet map:

ui.label('Map 1: set_visibility')
ui.button('Show', on_click=lambda: m1.set_visibility(True))
ui.button('Hide', on_click=lambda: m1.set_visibility(False))
with ui.card() as m1:
    ui.leaflet(center=(51.505, -0.09)).classes('w-96 h-64')
    m1.set_visibility(False)

ui.label('Map 2: classes "visible" and "hidden"')
ui.button('Show', on_click=lambda: m2.classes('visible', remove='hidden'))
ui.button('Hide', on_click=lambda: m2.classes('hidden', remove='visible'))
with ui.card() as m2:
    ui.leaflet(center=(51.505, -0.09)).classes('w-96 h-64')
    m2.classes('hidden')

ui.label('Map 3: CSS display "block" and "none"')
ui.button('Show', on_click=lambda: m3.style('display: block'))
ui.button('Hide', on_click=lambda: m3.style('display: none'))
with ui.card() as m3:
    ui.leaflet(center=(51.505, -0.09)).classes('w-96 h-64')
    m3.style('display: none')

ui.label('Map 4: classes "visible" and "invisible"')
ui.button('Show', on_click=lambda: m4.classes('visible', remove='invisible'))
ui.button('Hide', on_click=lambda: m4.classes('invisible', remove='visible'))
with ui.card() as m4:
    ui.leaflet(center=(51.505, -0.09)).classes('w-96 h-64')
    m4.classes('invisible')

ui.label('Map 5: CSS visibility "visible" and "hidden"')
ui.button('Show', on_click=lambda: m5.style('visibility: visible'))
ui.button('Hide', on_click=lambda: m5.style('visibility: hidden'))
with ui.card() as m5:
    ui.leaflet(center=(51.505, -0.09)).classes('w-96 h-64')
    m5.style('visibility: hidden')

The first three methods are basically the same: set_visibility sets classes visible and hidden which change the visibility CSS property. The Leaflet map is broken like in your screenshot.

The last two methods change the display CSS property. The Leaflet map looks ok, but occupies space while hidden. This might be a useful workaround, unless you really need the map to be gone without taking up space.

In this StackOverflow post I found a workaround using the CSS height property:

ui.button('Show', on_click=lambda: m.style(remove='height: 0px;'))
ui.button('Hide', on_click=lambda: m.style('height: 0px;'))
with ui.element() as m:
    ui.leaflet(center=(51.505, -0.09)).classes('w-96 h-64')
    m.style('height: 0px; overflow: hidden')
falkoschindler commented 7 months ago

Oh and here is another workaround using the map method invalidateSize:

ui.button('Show', on_click=lambda: (e.set_visibility(True), m.run_map_method('invalidateSize')))
ui.button('Hide', on_click=lambda: e.set_visibility(False))
with ui.element() as e:
    m = ui.leaflet(center=(51.505, -0.09)).classes('w-96 h-64')
    e.set_visibility(False)
falkoschindler commented 7 months ago

It seems like this issue will be fixed in Leaflet 2.0: https://github.com/Leaflet/Leaflet/issues/9010 So I think we need to sit and wait for its release.

kleynjan commented 7 months ago

Of course, Falko, that's fine. For the time being there are workarounds available.

Thanks for investigating, -Peter