I am trying to create two linked plots. When you click a point in the fig_overview
plot, the fig_detail
plot should change to show a specific view for that.
To my eyes, I have followed this tutorial to the letter. The first plot and the initial dummy plot show up as expected. Unfortunately, the second figure does not update.
The code is running in WSL on Python 3.10.12, jupyterlab 4.1.8, dash 2.17.0.
import numpy as np
import plotly.express as px
from dash import Dash, dcc, html
from dash.dependencies import Input, Output, State
DASH_THEME = dbc.themes.SOLAR
pd.options.plotting.backend = "plotly"
def fig_overview(data):
n = 30
fig = px.scatter(x=np.random.random((n,)), y=np.random.random((n,)))
return fig
def fig_detail(data, task):
n = 30
fig = px.scatter(x=np.random.random((n,)), y=np.random.random((n,)))
return fig
app = Dash('prrdash', external_stylesheets=[dbc.themes.SOLAR])
app.layout = html.Div(children=[
dcc.Graph(id='fig_overview', figure=fig_overview(None)),
dcc.Graph(id='fig_detail', figure=fig_detail(None, None))
])
@app.callback(
Output('fig_detail', 'figure'),
Input('fig_overview', 'clickData')
)
def select_project(clickData):
if clickData is not None:
fig = fig_detail(None, None)
return fig
app.run(debug=True)
What I have tried so far:
task
extraction logic: I can print(task)
inside select_project()
and get the expected result.fig_detail()
: I can even do fig.show()
inside the callback and get the expected result.mode
and jupyter_mode
of app.run()
.id
s - that shows you how stumped I am. No, changing the names does not change anything.div
and replacing its children
with a whole new dcc.Graph
does work as intended. But I really would love to understand why this simplified approach fails!Do you have any suggestions for what to try next? Or even just a little pointer on how to debug.
(EDIT: provided full working code example)
I didn't have full code so I had to create own version.
And I run it with app.run(debug=True)
to see problems
First (in browser) it shows me problem with ['custom']
but I don't have your data and code so you may not have this problem.
It seems problem can be what select_project
returns.
If there is no clickData
then it (automatically) runs return None
but if I return fig
then code works for me
def select_project(clickData):
if clickData is not None:
print(clickData)
task = clickData['points'][0]['customdata'][0]
fig = fig_detail(data, task)
else:
fig = fig_detail(data, None)
return fig # <-- it has to always return figure
Minimal working code with random data directly in code
from dash import Dash, dcc, html, Input, Output, callback
import plotly.express as px
import random
import pandas as pd
SIZE = 30
random.seed(0)
data = pd.DataFrame({
'x': range(SIZE),
'y': [random.randint(0,100) for _ in range(SIZE)],
'size': [random.randint(1,5) for _ in range(SIZE)],
'color': random.choices(['red', 'green', 'blue', 'black', 'white'], k=SIZE),
})
def fig_overview(data):
fig = px.scatter(data, x="x", y="y", size="size", color="color")
return fig
def fig_detail(data, task):
#print('task:', task)
if task:
point = task['x']
temp_data = data[point-1:point+2]
else:
temp_data = data
#print('temp_data:', temp_data)
fig = px.scatter(temp_data, x="x", y="y", size="size", color="color")
return fig
app = Dash(__name__)
app.layout = html.Div(children=[
dcc.Graph(id='fig_overview', figure=fig_overview(data)),
dcc.Graph(id='fig_detail', figure=fig_detail(data, None))
])
@app.callback(
Output('fig_detail', 'figure'),
Input('fig_overview', 'clickData')
)
def select_project(clickData):
if clickData is not None:
#print(clickData)
task = clickData['points'][0]#['customdata'][0]
fig = fig_detail(data, task)
else:
fig = fig_detail(data, None)
#return None # <--- if I use it then it stop working but it doesn't show error
return fig # <-- it has to always return figure
if __name__ == '__main__':
app.run(debug=True)
EDIT:
Dash has also special method to inform that you don't want to update data - you have to raise error PreventUpdate()
from dash.exceptions import PreventUpdate
@app.callback(
Output('fig_detail', 'figure'),
Input('fig_overview', 'clickData')
)
def select_project(clickData):
if clickData is not None:
task = clickData['points'][0]#['customdata'][0]
fig = fig_detail(data, task)
return fig
raise PreventUpdate()
Doc: Advanced Callbacks | Dash for Python Documentation | Plotly
Doc also shows that you can use no_update
to skip some value in output
from dash import no_update
@app.callback(
Output('fig_detail', 'figure'),
Input('fig_overview', 'clickData')
)
def select_project(clickData):
if clickData is not None:
task = clickData['points'][0]#['customdata'][0]
fig = fig_detail(data, task)
return fig
return no_update
All this because system may execute this function automatically at start (even if you don't select element in first plot).