2025, Nov 08 18:03

Обработка кликов на карте Plotly в NiceGUI: pointIndex и customdata из DataFrame

Краткое руководство по обработке plotly_click в NiceGUI: как извлечь pointIndex, найти строку pandas DataFrame и передавать атрибуты через customdata.

События клика на картах Plotly внутри NiceGUI насыщенные, но не волшебные. Если вы хотите показывать атрибуты из pandas DataFrame при клике по маркеру, нужно извлечь правильный индекс из полезной нагрузки события и сопоставить его с вашими данными. Ниже — краткое руководство, как сделать именно это, включая альтернативный подход с использованием customdata в Plotly для прямого доступа к атрибутам.

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

В следующем примере маркеры строятся из DataFrame и при клике пытаются показать название площадки. Он не срабатывает, потому что воспринимает аргументы события как простой индекс и ссылается на переменные, которых нет в области видимости.

import pandas as pd
import plotly.graph_objects as go
from nicegui import ui, events
frame = pd.DataFrame([
    [12345, -95, 45, 'Cross River'],
    [12346, -94, 43, 'Snake River'],
    [12347, -92, 45, 'Temple River'],
    [12348, -96, 46, 'Gold River'],
    [12349, -95.5, 44.5, '#FT Ty7']
], columns=['site', 'long', 'lat', 'name'])
chart = go.Figure(go.Scattermap(
    mode='markers',
    lon=frame['long'],
    lat=frame['lat'],
))
chart.update_layout(
    margin=dict(l=0, r=0, t=0, b=0),
    width=400,
    showlegend=False,
    map={
        'style': 'satellite-streets',
        'center': {
            'lon': sum(frame['long'] / len(frame)),
            'lat': sum(frame['lat'] / len(frame)),
        },
        'zoom': 5,
    },
)
plot_box = ui.plotly(chart)
info_text = ui.label('Click on a point for more information.')
def on_select(ev: events.GenericEventArguments):
    idx = ev.args
    info_text.text = f"Name: {name}, Site number: {site}."
plot_box.on('plotly_click', on_select)
ui.run()

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

Полезная нагрузка клика — это не простой целочисленный индекс. Это словарь со списком под ключом points. Каждый элемент этого списка — словарь с описанием нажатой точки, включая lat, lon и, что особенно важно, pointIndex (также доступен как pointNumber). Печать полезной нагрузки помогает увидеть доступные поля в наглядном виде.

import json
print(json.dumps(ev.args, indent=2))

В этой структуре строка DataFrame, которая породила кликнутый маркер, соответствует ev.args['points'][0]['pointIndex'].

Исправление: получаем строку DataFrame по pointIndex

Прямой способ показать атрибуты — достать pointIndex из события и с помощью iloc прочитать соответствующую строку. Затем сформатировать подпись из нужных полей.

import pandas as pd
import plotly.graph_objects as go
from nicegui import ui, events
frame = pd.DataFrame([
    [12345, -95, 45, 'Cross River'],
    [12346, -94, 43, 'Snake River'],
    [12347, -92, 45, 'Temple River'],
    [12348, -96, 46, 'Gold River'],
    [12349, -95.5, 44.5, '#FT Ty7']
], columns=['site', 'long', 'lat', 'name'])
chart = go.Figure(go.Scattermap(
    mode='markers',
    lon=frame['long'],
    lat=frame['lat'],
))
chart.update_layout(
    margin=dict(l=0, r=0, t=0, b=0),
    width=400,
    showlegend=False,
    map={
        'style': 'satellite-streets',
        'center': {
            'lon': sum(frame['long'] / len(frame)),
            'lat': sum(frame['lat'] / len(frame)),
        },
        'zoom': 5,
    },
)
plot_box = ui.plotly(chart)
info_text = ui.label('Click on a point for more information.')
def on_select(ev: events.GenericEventArguments):
    # Чтобы посмотреть «сырую» полезную нагрузку события:
    # import json; print(json.dumps(ev.args, indent=2))
    idx = ev.args['points'][0]['pointIndex']
    row = frame.iloc[idx]
    info_text.text = f"Name: {row['name']}, Site number: {row['site']}."
plot_box.on('plotly_click', on_select)
ui.run()

Если хотите подстраховаться, проверьте, что points присутствует и не пуст, прежде чем обращаться к нему. Это позволит избежать сбоев из‑за случайных событий.

def on_select(ev: events.GenericEventArguments):
    payload = ev.args
    if 'points' in payload and len(payload['points']) > 0 and 'pointIndex' in payload['points'][0]:
        idx = payload['points'][0]['pointIndex']
        row = frame.iloc[idx]
        info_text.text = f"Name: {row['name']}, Site number: {row['site']}."
    else:
        info_text.text = 'Wrong point'

Альтернатива: прикреплять атрибуты через customdata

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

import pandas as pd
import plotly.graph_objects as go
from nicegui import ui, events
frame = pd.DataFrame([
    [12345, -95, 45, 'Cross River'],
    [12346, -94, 43, 'Snake River'],
    [12347, -92, 45, 'Temple River'],
    [12348, -96, 46, 'Gold River'],
    [12349, -95.5, 44.5, '#FT Ty7']
], columns=['site', 'long', 'lat', 'name'])
chart = go.Figure(go.Scattermap(
    mode='markers',
    lon=frame['long'],
    lat=frame['lat'],
    customdata=frame[['site', 'name']],
))
chart.update_layout(
    margin=dict(l=0, r=0, t=0, b=0),
    width=400,
    showlegend=False,
    map={
        'style': 'satellite-streets',
        'center': {
            'lon': sum(frame['long'] / len(frame)),
            'lat': sum(frame['lat'] / len(frame)),
        },
        'zoom': 5,
    },
)
plot_box = ui.plotly(chart)
info_text = ui.label('Click on a point for more information.')
def on_select(ev: events.GenericEventArguments):
    site_id, place_name = ev.args['points'][0]['customdata']
    info_text.text = f"Name: {place_name}, Site number: {site_id}."
plot_box.on('plotly_click', on_select)
ui.run()

Если вы также хотите показывать атрибуты при наведении, настройте hovertemplate, чтобы выводить значения из customdata вместе с lat и lon.

chart = go.Figure(go.Scattermap(
    mode='markers',
    lon=frame['long'],
    lat=frame['lat'],
    customdata=frame[['site', 'name']],
    hovertemplate="Name: %{customdata[1]}, Site number: %{customdata[0]}<extra>lat: %{lat}, lon: %{lon}</extra>",
))

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

Интерактивные геопространственные интерфейсы часто держатся на точном соответствии между тем, куда кликает пользователь, и исходными данными. Полезная нагрузка клика в Plotly содержит всё необходимое, но структура вложенная. Понимание, что индекс строки DataFrame доступен как pointIndex, предотвращает хрупкий парсинг, лишние поиски и запутанные ошибки. Когда нужны лишь несколько полей, customdata упрощает поток и делает серверный колбэк минимальным.

Выводы

Один раз посмотрите на полезную нагрузку события и привяжите обработчик к pointIndex, чтобы получать строку через iloc. Если вашему случаю полезно передать атрибуты на клиент, прикрепляйте их через customdata и считывайте прямо из e.args в колбэке. Оба подхода просты, надёжны и делают карты NiceGUI + Plotly отзывчивыми и информативными.

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