Hi,
I want to create multiple views of the same data (plotly graphs) with selections in sync, so that the user can select/deselect data on any of the data views and it is reflected in others. I managed to achieve a working prototype following the online examples (listening to selectedData and writing on selectedPoints), but there’s a common case when this breaks down. If the user holds shift add to current selection, the selection goes out of sync. It seems that every plot remembers its own past selection boxes. So if, from the time of the previous selection box, any point were added programmatically to selection (i.e., coming from a click on another plot), these points are lost.
Here’s instructions to reproduce the problem, and below the code to run.
-In the 1st plot, click and drag to select the first point. It will light up in the 2nd plot, too.
-In the 2nd plot, hold shift and click and drag to select the second point. It will light up in the 1st plot too. Now two points are selected.
-In the 1st plot, hold shift and click and draft to select the third point. Bug! The selection now has lost the second point
You will see that the second point is lost at step three. Indeed, the 1st plot will show you the new selection box and the one added at the first step… but no box is present on the third point since it was added programmatically.
The problem occurs also with other selection modifiers. Does anybody know how to solve this?
Thanks.
Code:
import dash
from dash.dependencies import Input, Output, State, MATCH, ALL
import pandas as pd
from dash import dcc, html
import dash_bootstrap_components as dbc
import plotly.express as px
app = dash.Dash()
app.layout = html.Div(
dbc.Col([
dbc.Row(dcc.Graph(id='plot1')),
dbc.Row(dcc.Graph(id='plot2'))
])
)
data= pd.DataFrame( {'x':[1,2,3,4], 'y':[1,2,3,4], 'z':[-1,-2,-3,-4]})
@app.callback(
[Output('plot1', 'figure'),
Output('plot2', 'figure')],
[Input('plot1', 'selectedData'),
Input('plot2', 'selectedData')])
def sync_selection(sel1, sel2):
fired_info=dash.callback_context.triggered[0]['prop_id']
if fired_info=='.':
# first callack upon init
new_selection = []
else:
who_fired=fired_info.split('.')[0]
if who_fired=='plot1':
new_selection=[p['pointIndex'] for p in sel1['points']]
elif who_fired=='plot2':
new_selection=[p['pointIndex'] for p in sel2['points']]
print(fired_info, new_selection)
return [px.scatter(data_frame=data, x='x', y='y').update_layout(
clickmode='event+select',
dragmode='select').update_traces(selectedpoints=new_selection, marker_size=20),
px.scatter(data_frame=data, x='x', y='z').update_layout(
clickmode='event+select',
dragmode='select').update_traces(selectedpoints=new_selection, marker_size=20)]
if __name__ == '__main__':
app.run_server(debug=True)