2025, Nov 13 09:01
NiceGUI и Plotly: как починить on_click и обновлять bbox на карте
Почему кнопка в NiceGUI не обновляет карту Plotly и bbox? Разбираем ошибку on_click, пересборку lon/lat, обновление трейса и центр карты на рабочем примере.
Поля ввода NiceGUI привязаны, карта Plotly рендерится, но ограничивающий прямоугольник упорно не двигается. Если график либо не обновляется вовсе, либо без видимых причин стартует пустым, почти наверняка дело в том, как подключён обработчик клика, плюс в небольшой тонкости: где и когда вы заново формируете данные для трейса.
Воспроизведение: неподвижный прямоугольник и преждевременно пустой график
Ниже — минимальный пример: четыре поля ввода, привязанные к словарю, карта Plotly с полигоном и кнопка, которая должна инициировать перерисовку. Кнопка подключена неправильно — отсюда и «неотзывчивый» график, и поведение “почему он пустой ещё до клика?”, если очищать fig.data.
from nicegui import ui
import plotly.graph_objects as go
region_cfg = {
'north_lat': '',
'south_lat': '',
'west_lon': '',
'east_lon': '',
}
ui.input(label='North Latitude', value=45, placeholder='eg 45.0').bind_value_to(region_cfg, 'north_lat')
ui.input(label='South Latitude', value=40, placeholder='eg 40.0').bind_value_to(region_cfg, 'south_lat')
ui.input(label='West Longitude', value=-90, placeholder='eg -90.0').bind_value_to(region_cfg, 'west_lon')
ui.input(label='East Longitude', value=-88, placeholder='eg -88.0').bind_value_to(region_cfg, 'east_lon')
x_lon = [region_cfg['west_lon'], region_cfg['east_lon'], region_cfg['east_lon'], region_cfg['west_lon']]
y_lat = [region_cfg['north_lat'], region_cfg['north_lat'], region_cfg['south_lat'], region_cfg['south_lat']]
ctr_lon = (region_cfg['west_lon'] + region_cfg['east_lon']) / 2
ctr_lat = (region_cfg['north_lat'] + region_cfg['south_lat']) / 2
chart = go.Figure(go.Scattermap(
fill="toself",
lon=x_lon, lat=y_lat,
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': ctr_lon, 'lat': ctr_lat},
'zoom': 5,
},
)
canvas = ui.plotly(chart)
def do_refresh():
# chart.data = [] # если раскомментировать, график стартует пустым
canvas.update()
ui.button('Update', on_click=do_refresh()) # неверно: выполняется сразу
ui.run()
Что на самом деле идёт не так
Ключевая проблема — обработчик события. В GUI-фреймворках обработчик должен быть колбэком: вы передаёте объект функции, а не результат её вызова. Когда вы пишете on_click=do_refresh(), Python выполняет do_refresh сразу при настройке, возвращаемое значение присваивается on_click, и на реальный клик уже нечему реагировать. Если обработчик очищает данные фигуры, график стартует пустым, потому что этот код уже отработал на этапе запуска.
Есть и вторая, более скрытая проблема. Привязка инпутов обновляет словарь, но ранее вычисленные списки вроде x_lon и y_lat сами собой не пересчитаются. Их нужно формировать заново из текущих значений словаря внутри обработчика и записывать обратно в существующий трейс Plotly. Если инпуты отдают строки, понадобится преобразование в числа; простой вариант — поручить это самой привязке.
Решение: передать колбэк и обновить существующий трейс
Кнопка должна получить имя функции без скобок. Пересоберите массивы долгот и широт внутри этой функции, передайте новые данные в существующий трейс через chart.data[0], при желании обновите центр карты и попросите NiceGUI перерисовать компонент.
from nicegui import ui
import plotly.graph_objects as go
region_cfg = {
'north_lat': '',
'south_lat': '',
'west_lon': '',
'east_lon': '',
}
ui.input(label='North Latitude', value=45, placeholder='eg 45.0')\
.bind_value_to(region_cfg, 'north_lat', forward=float)
ui.input(label='South Latitude', value=40, placeholder='eg 40.0')\
.bind_value_to(region_cfg, 'south_lat', forward=float)
ui.input(label='West Longitude', value=-90, placeholder='eg -90.0')\
.bind_value_to(region_cfg, 'west_lon', forward=float)
ui.input(label='East Longitude', value=-88, placeholder='eg -88.0')\
.bind_value_to(region_cfg, 'east_lon', forward=float)
x_lon = [region_cfg['west_lon'], region_cfg['east_lon'], region_cfg['east_lon'], region_cfg['west_lon']]
y_lat = [region_cfg['north_lat'], region_cfg['north_lat'], region_cfg['south_lat'], region_cfg['south_lat']]
ctr_lon = (region_cfg['west_lon'] + region_cfg['east_lon']) / 2
ctr_lat = (region_cfg['north_lat'] + region_cfg['south_lat']) / 2
chart = go.Figure(
go.Scattermap(
fill='toself',
lon=x_lon,
lat=y_lat,
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': ctr_lon, 'lat': ctr_lat},
'zoom': 5,
},
)
canvas = ui.plotly(chart)
def handle_redraw():
xs = [region_cfg['west_lon'], region_cfg['east_lon'], region_cfg['east_lon'], region_cfg['west_lon']]
ys = [region_cfg['north_lat'], region_cfg['north_lat'], region_cfg['south_lat'], region_cfg['south_lat']]
chart.data[0].lon = xs
chart.data[0].lat = ys
mid_x = (region_cfg['west_lon'] + region_cfg['east_lon']) / 2
mid_y = (region_cfg['north_lat'] + region_cfg['south_lat']) / 2
chart.update_layout(map={'center': {'lon': mid_x, 'lat': mid_y}})
canvas.update()
ui.button('Update', on_click=handle_redraw)
ui.run()
Почему это справедливо для любых GUI
Это универсальный паттерн в событийно-ориентированных интерфейсах. Вы передаёте фреймворку колбэк, чтобы он мог вызвать его позже. Передача ссылки на функцию позволяет рантайму выполнить её в ответ на событие; вызов во время инициализации запускает код немедленно — задолго до того, как пользователь что-то нажмёт. Та же идея колбэков встречается в tkinter, PyQt и даже в JavaScript, где слушатели событий регистрируются, но не выполняются, пока событие не произойдёт.
Почему это стоит запомнить
Две привычки экономят время при разработке интерактивных инструментов. Во-первых, всегда передавайте ссылку на функцию в обработчики событий и вычисляйте динамическое состояние только внутри этих колбэков. Во-вторых, помните, что делает привязка, а чего нет: связанный словарь обновляется, но любые массивы или производные значения, созданные раньше, устаревают, пока вы не пересоберёте их из текущего состояния. Для числовых доменов, например координат на карте, убедитесь, что значения — числа; конвертер в привязке упрощает обработчик и предотвращает сюрпризы с типами.
Вывод
Подключайте кнопку колбэком, а не вызовом. Пересчитывайте массивы долготы/широты внутри этого колбэка, обновляйте существующий трейс через chart.data[0], при необходимости сдвигайте центр карты и попросите NiceGUI обновить компонент Plotly. С этими шагами ограничивающий прямоугольник станет интерактивным, а карта будет отражать каждое изменение в полях ввода ровно тогда, когда вы этого захотите.