2025, Nov 07 19:00

NiceGUI + Plotly Scattermap not updating on button click: what blocks it and how to fix it

Fix a NiceGUI + Plotly Scattermap that won't update on click. Blocking calls stall the UI; use reconnect_timeout or run.cpu_bound to load data smoothly.

NiceGUI + Plotly: why a Scattermap update works outside a function but stalls on click

Interactive updates in a NiceGUI app can look perfectly fine during startup and suddenly stop working once the same code is moved into a button handler. A common case is Plotly’s Scattermap that renders correctly when run before the UI launches, yet refuses to add new points when the data fetch happens inside a function.

Reproducing the issue

The following snippet wires a button to load data and update a map. The logic is straightforward, but the on-click update doesn’t show new points in the browser.

import plotly.graph_objects as go
import dataretrieval.nwis as nwis
from nicegui import ui
chart = go.Figure(go.Scattermap(
    fill="toself",
    lon=[-90, -89, -89, -90],
    lat=[45, 45, 44, 44],
    marker={'size': 10, 'color': 'orange'},
    name='BBox'
))
chart.update_layout(
    margin=dict(l=0, r=0, t=0, b=0),
    width=400,
    showlegend=False,
    map={
        'style': 'carto-darkmatter',
        'center': {'lon': -90, 'lat': 44},
        'zoom': 5,
    },
)
panel = ui.plotly(chart)
def refresh_map():
    dataset, meta = nwis.get_info(bBox=[-90, 44, -89, 45])
    lat_series = dataset['dec_lat_va']
    lon_series = dataset['dec_long_va']
    labels = dataset['station_nm']
    chart.add_trace(go.Scattermap(
        lon=lon_series,
        lat=lat_series,
        fill=None,
        mode='markers',
        marker={'size': 15, 'color': 'blue'},
        text=labels,
        name='sites',
    ))
    panel.update()
ui.button('Update', on_click=refresh_map)
ui.run(title='Test')

What actually goes wrong

The data load via nwis.get_info() can take a while. When this long-running call executes inside the click handler, it blocks the server. While the browser tries to keep the connection alive, the server cannot respond because it is still busy. The browser eventually shows a connection lost state and reconnects. After reconnecting, it does not know that there is fresh data to display. In contrast, the version that runs before the UI starts finishes data retrieval before the browser is involved, so the page renders with the new data right away.

Two practical ways to fix it

If you need a quick workaround, extend the reconnection window. This gives the blocking call enough time to complete before the browser decides to reconnect. If you want the UI to remain responsive during long tasks, offload the work using NiceGUI’s helpers for background execution.

Quick mitigation: increase reconnect_timeout

Raising reconnect_timeout keeps the browser waiting longer before it reconnects, allowing the update to complete and render.

import plotly.graph_objects as go
import dataretrieval.nwis as nwis
from nicegui import ui
chart = go.Figure(go.Scattermap(
    fill="toself",
    lon=[-90, -89, -89, -90],
    lat=[45, 45, 44, 44],
    marker={'size': 10, 'color': 'orange'},
    name='BBox'
))
chart.update_layout(
    margin=dict(l=0, r=0, t=0, b=0),
    width=400,
    showlegend=False,
    map={
        'style': 'carto-darkmatter',
        'center': {'lon': -90, 'lat': 44},
        'zoom': 5,
    },
)
panel = ui.plotly(chart)
def refresh_map():
    dataset, meta = nwis.get_info(bBox=[-90, 44, -89, 45])
    lat_series = dataset['dec_lat_va']
    lon_series = dataset['dec_long_va']
    labels = dataset['station_nm']
    # Avoid stacking multiple traces on repeated clicks
    chart.data = []
    chart.add_trace(go.Scattermap(
        lon=lon_series,
        lat=lat_series,
        fill=None,
        mode='markers',
        marker={'size': 15, 'color': 'blue'},
        text=labels,
        name='sites',
    ))
    panel.update()
ui.button('Update', on_click=refresh_map)
ui.run(title='Test', reconnect_timeout=60)

This approach is partial: if the data retrieval takes even longer, the reconnection delay must grow accordingly. In such cases it’s better to move the work off the main thread.

Robust approach: run the long task in the background

NiceGUI provides run.cpu_bound() for CPU-heavy code and run.io_bound() for I/O-heavy operations. They accept a function name along with positional and/or named arguments, and execute it off the UI thread. In practice, sending named arguments directly to nwis.get_info through run.cpu_bound can format bBox incorrectly, so a tiny wrapper helps pass kwargs as expected.

import plotly.graph_objects as go
import dataretrieval.nwis as nwis
from nicegui import ui, run
# Small wrapper to forward keyword arguments correctly
def proxy_fetch(**kwargs):
    return nwis.get_info(**kwargs)
async def refresh_map():
    dataset, meta = await run.cpu_bound(proxy_fetch, bBox=[-90, 44, -89, 45])
    lat_series = dataset['dec_lat_va']
    lon_series = dataset['dec_long_va']
    labels = dataset['station_nm']
    chart.data = []
    chart.add_trace(go.Scattermap(
        lon=lon_series,
        lat=lat_series,
        fill=None,
        mode='markers',
        marker={'size': 15, 'color': 'blue'},
        text=labels,
        name='sites',
    ))
    panel.update()
chart = go.Figure(go.Scattermap(
    fill="toself",
    lon=[-90, -89, -89, -90],
    lat=[45, 45, 44, 44],
    marker={'size': 10, 'color': 'orange'},
    name='BBox'
))
chart.update_layout(
    margin=dict(l=0, r=0, t=0, b=0),
    width=400,
    showlegend=False,
    map={
        'style': 'carto-darkmatter',
        'center': {'lon': -90, 'lat': 44},
        'zoom': 5,
    },
)
panel = ui.plotly(chart)
ui.button('Update', on_click=refresh_map)
ui.run(title='Test')

With this setup, the long-running call no longer blocks the server, and updates render without tweaking reconnect_timeout.

Why this detail matters

UI event handlers that perform long work can temporarily stall the server, causing the browser to lose the session and reconnect. Understanding that behavior helps avoid confusing symptoms like missing updates, and it keeps interactive charts usable. Offloading long tasks or increasing the reconnection window are both valid strategies depending on how long the operation takes.

Takeaways

If a Plotly Scattermap in a NiceGUI app updates correctly at startup but not on click, the difference is timing. Fetching data ahead of time completes before the browser connects, while doing it inside a handler can block the server. Start with a larger reconnect_timeout if you need a fast fix. For a more resilient approach, use NiceGUI’s run.cpu_bound or run.io_bound to execute the work in the background, and reset chart.data when you don’t want to stack multiple traces across updates.

The article is based on a question from StackOverflow by DrewGIS and an answer by furas.