Thanks for the great work on dash and plotly. I am trying to implement cross filtering in scatterplots with the selectedpoints field in form of a client side callback. The callback gets the selected points from a python callback and the resulting figures have the required structure but nothing gets selected.
Hey
this is another callback in Python that extracts the customdata
field from the list of selectedData for all plots. To define which points are selected.
It stores them in a dcc.Store.
Does the callback gets triggered? What are the values that data and selected assume when it does?
If you can add a short reproducible example of your app (you can use px.data.iris(), just one figure and the dcc.Store), then it would make it a lot easier to spot the issue.
Does the clientside callback work? If it does, then what is the expected outcome of it?
If you want to the selection to be propagated through all the graphs (like some sort of cross-filtering), then I think you’ll first need to apply a different color to selected points via the selected_marker_color attribute of go.Scatter (or modifying figure.selected.marker.color on the js side…). I am not sure that the transparency change in unselected points can happen automatically, meaning, the points might be selected but you won’t see any difference as you do in the graph where the selection is originally made. Therefore the color change.
A second issue has to do with Plotly.js not re-rendering figures if the figure object is modified in-place on the javascript side. I can’t find any references to this issue, but I have seen it popping up on the forum or on Github… For that, you can simply create a copy of the figure objects, modify them and return, so your function would look more or less like:
Your naming confused me at first - since you have ALL it’s not a single figure but many figures, so better to have the arg name reflect that
In-place modification is likely the issue here. If you want a simple (but low-performance if your figure is large) solution, you can:
return JSON.parse(JSON.stringify(fig))
or perhaps do that upfront:
const figCopy = JSON.parse(JSON.stringify(fig))
But a better solution is to use layout.datarevision - just make sure its value changes and the plot should recognize the need to update. layout | JavaScript | Plotly
You are right the naming was confusing. I got the json parsing working thanks a ton for that. But the datarevision isn’t working for me maybe I am doing something wrong:
import dash
import pandas as pd
import plotly.graph_objects as go
import dash.dcc as dcc
import dash.html as html
import plotly.express as px
from dash.dependencies import Input, Output, State, ALL
iris_df = px.data.iris()
setosa = iris_df[iris_df["species"] == "setosa"]
def layout():
figures = []
measures = ["sepal_length", "sepal_width", "petal_length", "petal_width"]
plots = [(x, y) for x in measures for y in measures]
for idx, plot in enumerate(plots):
if plot[0] == plot[1]:
continue
fig = go.Figure()
fig.add_trace(
go.Scatter(
x = setosa[plot[0]],
y = setosa[plot[1]],
customdata= list(setosa.index),
mode="markers",
selected= {"marker": {"color":"red"}}
)
)
fig.update_layout(
title="{}: {}".format(plot[0], plot[1]),
xaxis_title=str(plot[0]),
yaxis_title=str(plot[1]),
datarevision= 0,
selectionrevision = 0)
graph = dcc.Graph(id= {"type": "graph", "index": idx} , figure=fig)
figures.append(graph)
return(html.Div([dcc.Store(id="selection"),html.Div(figures)]))
app = dash.Dash(__name__)
app.layout = layout()
@app.callback(
Output("selection", 'data'),
Input({"type": "graph", "index": ALL}, 'selectedData'),
prevent_initial_call=True
)
def selection(sel):
points = []
for i in sel:
if i is None:
continue
for point in i['points']:
points.append(point['customdata'])
print(points)
return list(set(points))
app.clientside_callback(
"""
function(data, figures) {
for (var i = 0; i < figures.length; i++) {
const ids = figures[i]['data'][0]['customdata']
const intersect = data.filter(value => ids.includes(value));
const selected = intersect.map(x => ids.indexOf(x))
selected.sort();
figures[i]['data'][0]['selectedpoints'] = selected
figures[i]['layout']['datarevision'] = figures[i]['layout']['datarevision']+1 ;
}
console.error(figures[2]);
return figures;
}""",
Output({"type": "graph", "index": ALL}, "figure"),
Input("selection","data"),
State({"type": "graph", "index": ALL}, "figure"),
prevent_initial_call=True
)
if __name__ == "__main__":
app.run_server(debug=True)