2025, Nov 10 15:02

Почему Scattermap в Plotly на NiceGUI не обновляется при клике и как исправить

Почему Scattermap в Plotly на NiceGUI не обновляется при клике и как это исправить: увеличьте reconnect_timeout или выполните её в фоне через run.cpu_bound.

NiceGUI + Plotly: почему обновление Scattermap работает вне функции, но «зависает» при клике

Интерактивные обновления в приложении на NiceGUI могут выглядеть безупречно во время запуска и внезапно перестать срабатывать, как только тот же код перенести в обработчик кнопки. Типичный пример — Scattermap в Plotly: если выполнить его до старта интерфейса, карта рисуется корректно, но при попытке добавить точки после загрузки данных внутри функции новые элементы не появляются.

Как воспроизвести проблему

Ниже показан фрагмент, который привязывает кнопку к загрузке данных и обновлению карты. Логика простая, однако при клике в браузере новые точки так и не отображаются.

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')

Что на самом деле происходит

Загрузка данных через nwis.get_info() может занять заметное время. Когда этот долгий вызов выполняется внутри обработчика клика, он блокирует сервер. Браузер в это время старается удерживать соединение, но сервер не отвечает, потому что всё ещё занят. В итоге браузер сообщает о потере соединения и переподключается. После переподключения он уже не знает, что появились новые данные для отображения. В противоположность этому, вариант, который выполняется до запуска интерфейса, завершает получение данных до того, как подключится браузер, поэтому страница сразу рендерится с обновлениями.

Два практичных способа решить задачу

Нужен быстрый обходной путь — расширьте окно переподключения. Это даст блокирующему вызову время завершиться до того, как браузер решит переподключиться. Если же важно сохранить отзывчивость интерфейса при длительных операциях, вынесите работу на фон с помощью вспомогательных средств NiceGUI для фонового выполнения.

Быстрая мера: увеличить reconnect_timeout

Увеличение reconnect_timeout заставляет браузер дольше ждать перед переподключением, давая обновлению завершиться и отрисоваться.

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.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)

Этот подход частичный: если получение данных занимает ещё больше времени, задержку переподключения придётся увеличивать пропорционально. В таких случаях лучше перенести работу с главного потока.

Надёжный подход: выполнять длительную задачу в фоне

NiceGUI предоставляет run.cpu_bound() для ресурсоёмкого по CPU кода и run.io_bound() для операций ввода-вывода. Они принимают имя функции вместе с позиционными и/или именованными аргументами и выполняют её вне UI-потока. На практике передача именованных аргументов напрямую в nwis.get_info через run.cpu_bound может неправильно сформировать bBox, поэтому небольшая обёртка помогает передать kwargs как ожидается.

import plotly.graph_objects as go
import dataretrieval.nwis as nwis
from nicegui import ui, run
# Небольшая обёртка, чтобы корректно передавать именованные аргументы
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')

С таким устройством долгий вызов больше не блокирует сервер, и обновления отрисовываются без настройки reconnect_timeout.

Почему этот нюанс важен

Обработчики событий интерфейса, выполняющие длительную работу, могут временно «подвешивать» сервер, из‑за чего браузер теряет сессию и переподключается. Понимание этой механики помогает избежать сбивающих с толку симптомов вроде пропадающих обновлений и сохраняет интерактивность графиков. В зависимости от длительности операции уместны оба варианта: вынести тяжёлую задачу в фон или увеличить окно переподключения.

Выводы

Если Scattermap в Plotly внутри приложения NiceGUI обновляется при запуске, но не реагирует на клик, дело во времени. Предзагрузка данных успевает завершиться до соединения с браузером, а выполнение внутри обработчика блокирует сервер. Для быстрого исправления начните с увеличения reconnect_timeout. Для более устойчивого решения используйте run.cpu_bound или run.io_bound в NiceGUI, чтобы выполнять задачу в фоне, и сбрасывайте chart.data, когда не хотите наслаивать несколько трейсов между обновлениями.

Статья основана на вопросе на StackOverflow от DrewGIS и ответе furas.