zauberzeug / nicegui

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

ui.plotly `plotly_selected` event not triggered if many points are selected #3762

Open flooxo opened 1 week ago

flooxo commented 1 week ago

What are you trying to do?

I want to select points in a Plotly plot in NiceGUI and, based on the selection, convert the selection into a shape and display it in the plot. This is all working so far.

However, I happened to run into the problem that if there are more than 1000 data points plotted in the plot and I select more than 20 points with the lasso select, the plotly_selected event is no longer triggered

Minimal code

import numpy as np
import plotly.graph_objects as go
from nicegui import ui

x_positive = np.arange(1, 51)
y_positive = np.zeros_like(x_positive)

x_negative = np.random.uniform(-10, 0, 1000)
y_negative = np.random.uniform(-10, 0, 1000)

x = np.concatenate([x_positive, x_negative])
y = np.concatenate([y_positive, y_negative])

data = go.Scattergl(
    x=x,
    y=y,
    mode="markers",
    marker=dict(
        size=10,
        opacity=0.6,
    ),
    name="Original Data", 
).to_plotly_json()

layout = go.Layout(
    xaxis=dict(title="X-axis"), 
    yaxis=dict(title="Y-axis"),
    hovermode="closest",
    showlegend=False,
    dragmode="lasso",
).to_plotly_json()

fig = {
    "data": [data],
    "layout": layout,
    "config": {
        "scrollZoom": True,
    },
}

plot = ui.plotly(fig).classes("w-full h-full")

plot.on("plotly_selected", ui.notify)

ui.run(reload=True)

The points are specially created so that it is easier to count how many points have been selected in the positive area

What do you expect to happen?

event is triggered

What happens instead?

no event is triggered

flooxo commented 1 week ago

I did a bit more research on the problem and wanted to find out where the event is no longer triggered When I inject this js code I see in the browser console that the selection event is always recognized correctly but not always for the server.

Could the reason be that there is a message size limitation for the websocket? So that if too many points are selected, the event is too big and gets lost?

    ui.run_javascript("""
        setTimeout(function() {
            var plotElement = document.querySelector('div.js-plotly-plot');
            if (plotElement) {
                plotElement.on('plotly_selected', function(eventData) {
                    console.log('plotly_selected event detected:', eventData);
                });
            } else {
                console.log('Plotly plot element not found.');
            }
        }, 1000);
    """)
rodja commented 6 days ago

You could try to change the max_http_buffer_size as discussed in https://github.com/zauberzeug/nicegui/issues/3410 to verify if it's a package size problem.

falkoschindler commented 6 days ago

That's interesting: The "plotly_selected" event arguments have the following structure (here for selecting 2 out of 50 points):

{
    'points': [
        {'data': {'marker': {'opacity': 0.6, 'size': 10}, 'mode': 'markers', 'name': 'Original Data', 'x': [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50], 'y': [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], 'type': 'scattergl', 'selectedpoints': [0, 1]}, 'curveNumber': 0, 'pointNumber': 0, 'pointIndex': 0, 'x': 1, 'y': 0},
        {'data': {'marker': {'opacity': 0.6, 'size': 10}, 'mode': 'markers', 'name': 'Original Data', 'x': [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50], 'y': [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], 'type': 'scattergl', 'selectedpoints': [0, 1]}, 'curveNumber': 0, 'pointNumber': 1, 'pointIndex': 1, 'x': 2, 'y': 0}
    ],
    'lassoPoints': {'x': [0.40599744789451314, 0.48936622713738853, 1.114632071458954, 1.7398979157805194, 2.1150574223734586, 2.4068481497235226, 2.323479370480647, 2.3651637601020847], 'y': [-0.17037037037037037, -0.05925925925925926, 0.16296296296296298, 0.28888888888888886, 0.2814814814814815, 0.13333333333333333, -0.05925925925925926, -0.07407407407407407]},
    'selections': [{'xref': 'x', 'yref': 'y', 'line': {'width': 1, 'dash': 'dot'}, 'type': 'path', 'path': 'M0.40599744789451314,-0.17037037037037037L0.48936622713738853,-0.05925925925925926L1.114632071458954,0.16296296296296298L1.7398979157805194,0.28888888888888886L2.1150574223734586,0.2814814814814815L2.4068481497235226,0.13333333333333333L2.323479370480647,-0.05925925925925926L2.3651637601020847,-0.07407407407407407Z'}]
}

So the question is, how to get the information about selected points without lots of unnecessary copies of point clouds. Do we need to introduce some kind of args_filter parameter that accepts a JavaScript function like (e) => e.points.map(p => p.pointIndex)?


For reference, a condensed reproduction:

ui.plotly({
    'data': [{
        'x': np.concatenate([np.arange(1, 51), np.random.uniform(-10, 0, 1000)]),
        'y': np.concatenate([np.zeros(50), np.random.uniform(-10, 0, 1000)]),
        'mode': 'markers',
    }],
    'layout': {'dragmode': 'lasso'},
}).on('plotly_selected', lambda e: print(e.args))
flooxo commented 6 days ago

You could try to change the max_http_buffer_size as discussed in #3410 to verify if it's a package size problem.

Thanks, that was also my first guess. Setting max_http_buffer_size didn't change anything for me. Maybe someone else can confirm if I did it right

points seems to contain a copy of the original data for each of the selected points.

But it would make sense if the buffer size is the bottleneck, because the problem only occurs for larger amounts of data Because if I increase the total number of points in the plot, there is a certain level where you can no longer even select a single point

flooxo commented 5 days ago

If you add args so that the event becomes “smaller” and contains less data, then it also triggers for large amounts of data. The only problem is that i have not yet found a way with this method to determine the pointindex of all selectedpoints

plot.on('plotly_selected', lambda e: print(e.args), args:[“lassoPoints”])