Consider the following toy Python class, which can occasionally fetch additional data from some source, and updates its display:
# minimum_working_example.py
import plotly.graph_objects as go
import random
class MyData:
def __init__(self):
self.mydata = []
self.figure = go.FigureWidget()
self.figure.add_trace(go.Scatter(
name = 'mydata',
))
self.get_more_data()
def get_more_data(self):
self.mydata += [ random.random() for _ in range(10) ]
self.figure.update_traces(
selector=dict(name='mydata'),
y = self.mydata,
x = list(range(len(self.mydata))),
)
If I go into a Jupyter notebook and say things like
# interactive jupyter prompt
from minimum_working_example import MyData
mydata = MyData()
display(mydata.figure)
then I get a figure that I can fiddle around with: zooming the axes, toggling which traces are visible, etc. (My non-minimal example has multiple traces.) Because the figure is a FigureWidget
, calling mydata.get_more_data()
changes the trace on the graph, but it doesn't change any other feature.
[
I would like to move this display from an interactive notebook to a Dash app with a "get more data" button, but so far everything that I've tried causes the figure to reset all of its modifications, rather than updating in place. Here's an example fails in an educational way, based on the documentation page for Partial Property Updates:
# mwe_dash.py
from dash import Dash, html, dcc, Input, Output, Patch, callback
import plotly.graph_objects as go
import datetime
import random
import minimum_working_example
app = Dash(__name__)
mydata = minimum_working_example.MyData()
app.layout = html.Div(
[
html.Button("Append", id="append-new-val"),
dcc.Graph(figure=mydata.figure, id="append-example-graph"),
]
)
@callback(
Output("append-example-graph", "figure"),
Input("append-new-val", "n_clicks"),
prevent_initial_call=True,
)
def put_more_data_on_figure(n_clicks):
mydata.get_more_data()
return mydata.figure
if __name__ == "__main__":
app.run(port=8056)
If I run this by itself with python mwe_dash.py
, then the figure is completely reset every time I press the "get more data" button — just like the examples on the documentation page above. But if I run it within Jupyter, with
# in jupyter again
from mwe_dash import app, mydata
display(mydata.figure)
app.run(jupyter_server_url='localhost:8888',port=8057)
then appearance-changing interactions with the figure in the Jupyter notebook are applied in the Dash app when the figure updates. However, the figure in the Dash app resets to the version in the notebook every time the "get more data" button is pressed.
How can I get the figure in the Dash app to reproduce the update-without-resetting behavior that is in the Jupyter notebook? I assume I need some additional callback from the figure to the Dash app. Will it be necessary to have a Jupyter server running in the background, even when I get rid of the notebook part of this analysis tool?
If you want to "move this display from an interactive notebook to a Dash app", use a go.Figure()
instead of go.FigureWidget()
.
You are not doing partial update : fig.update_traces()
updates the figure on the backend and the callback just returns it (the whole figure), so on the clientside you loose the state of the previously loaded figure. You should use the Patch
class to extend the list of values in x and y.
Don't create/update the figure in MyData class, just create/update the data and consume them when needed (single-responsibility principle).
class MyData:
def __init__(self):
self.data = []
self.get_more_data()
def get_more_data(self):
newdata = [ random.random() for _ in range(10) ]
self.data += newdata
return newdata
app = Dash(__name__)
mydata = MyData()
y = mydata.data
x = list(range(len(y)))
fig = go.Figure(go.Scatter(x=x, y=y, name='mydata'))
app.layout = html.Div(
[
html.Button("Append", id="append-new-val"),
dcc.Graph(figure=fig, id="append-example-graph"),
]
)
@callback(
Output("append-example-graph", "figure"),
Input("append-new-val", "n_clicks"),
prevent_initial_call=True,
)
def put_more_data_on_figure(n_clicks):
i = len(mydata.data)
new_y = mydata.get_more_data()
new_x = list(range(i, i+len(new_y)))
patched_figure = Patch()
patched_figure['data'][0]['x'].extend(new_x)
patched_figure['data'][0]['y'].extend(new_y)
return patched_figure
if __name__ == "__main__":
app.run(debug=True)