Here is an abridged version of the code that reproduces the problem. You will need a TSV with a column “position” with x-values and “value” with y-values (from 0 to 100).
from typing import Any, List, Optional, Tuple
from attrs import define, field
import dash
import dash_bootstrap_components as dbc
import pandas as pd
import plotly.express as px
import plotly.graph_objs as go
from dash import dash_table, dcc, html
from dash.dependencies import Input, Output
@define
class MyData:
df: pd.DataFrame = field()
figure: go.Figure = field(init=False)
def __attrs_post_init__(self) -> None:
self.figure = self.make_figure()
def make_figure(self) -> None:
self.df["position"] = range(self.df.shape[0])
self.df["value"] = self.df["count"]
figure = go.Figure()
figure.add_trace(
go.Scatter(
x=self.df["position"],
y=self.df["value"],
mode="markers",
name="my_series",
)
)
figure.update_layout(yaxis_title="Value")
return figure
my_data: Optional[MyData] = None
app = dash.Dash("my_app", prevent_initial_callbacks="initial_duplicate")
app.layout = html.Div(
[
dcc.Graph(id="my_plot", figure=px.scatter(None)),
html.Div(dbc.Button("Load from file", id="load_btn")),
html.Div(id="selection_div"),
]
)
def expand_selection(indices: List[int]) -> List[int]:
if indices:
return list(range(min(indices), max(indices) + 1))
return []
@app.callback(
Output("my_plot", "figure", allow_duplicate=True),
Output("selection_div", "children"),
Input("my_plot", "selectedData"),
)
def adjust_selection(sel_data: Any) -> Tuple[go.Figure, html.Div]:
print("> adjust_selection()")
global my_data
if my_data is None:
print(" - no data")
return (go.Figure(), html.Div(html.P("Data not loaded")))
figure: go.Figure = my_data.figure
if sel_data is None or "range" not in sel_data:
print(" - no selection")
figure.for_each_trace(lambda trace: trace.update(selectedpoints=[]))
return (figure, html.Div(html.P("No data selected")))
print(" - selection")
selection_start = sel_data["range"]["x"][0]
selection_end = sel_data["range"]["x"][1]
selected_points = [point["pointIndex"] for point in sel_data["points"]]
selected_points = expand_selection(selected_points)
figure.for_each_trace(lambda trace: trace.update(selectedpoints=selected_points))
# figure.add_selection(x0=selection_start, y0=0, x1=selection_end, y1=100)
selected_rows = (my_data.df["position"] >= selection_start) & (my_data.df["position"] <= selection_end)
selected_df = my_data.df.loc[selected_rows]
my_table = html.Div(
[
html.P("Selected Data"),
dash_table.DataTable(
selected_df.to_dict("records"),
id="my_table",
columns=[{"name": c, "id": c} for c in selected_df.columns]
)
],
)
return (figure, my_table)
@app.callback(
Output("my_plot", "figure"),
Input("load_btn", "n_clicks"),
prevent_initial_call=True,
)
def button_clicked(load_btn: Any) -> go.Figure:
print("> button_clicked")
return load_data_from_file()
def load_data_from_file() -> go.Figure:
print("> load_data_from_file")
global my_data
df = pd.read_csv("my_data_table.tsv", sep="\t")
print(f" df is {df.shape[0]}x{df.shape[1]}")
my_data = MyData(df)
return my_data.figure
# Run the app
if __name__ == "__main__":
app.run_server(host="0.0.0.0", debug=True)
Using this code, when I load data and then select some points I see this output:
> adjust_selection()
- no data
> button_clicked
> load_data_from_file
df is 2000x10
> adjust_selection()
- selection
> adjust_selection() <<< ???
- no selection
As the first adjust_selection()
is triggered, all works as expected. But immediately after, a second event is triggered (with no selection), and it clear the table and deselects all points in the graph. I do not understand where the second event comes from.
To go back to the original graph, I need to double click on the graph - twice - as the first time nothing happens.
To persist the selection, one can uncomment the add_selection()
line. It gives me this output:
> adjust_selection()
- no data
> button_clicked
> load_data_from_file
df is 2000x10
> adjust_selection()
- selection
> adjust_selection() <<< selection persists
- selection
Now the table remains populated, and the graph shows the selected points, but also the selection box around them.
If I double-click on the graph, the table is cleared but the selection (and the box) remain in the graph. If I double-click a second time (and more), the original graph flashes and goes back to the selection.