Reputation: 177
I'm developing a tool to visualize the effects of a set of parameters on a mathematical function by modifying those parameters via Dash sliders. I'm using the approach from a few of the Dash tutorial examples which use a callback to replace the figure.
This works, but the plot isn't as responsive to slider changes as built-in operations such as rotating the plot via dragging. This is especially the case when there are many elements in the figure.
Is there a different approach that will improve responsiveness? For example, is there a way to target only the plot elements that changed rather than replacing the entire figure?
Here's a minimal working example consisting of a static circle (with many samples) and a line segment that we rotate via a slider. Rotation of the line segment is quite choppy.
import dash
import dash_core_components as dcc
import dash_html_components as html
import plotly.graph_objects as go
from dash.dependencies import Input, Output, State
import numpy as np
app = dash.Dash(__name__)
# plot a circle
t = np.linspace(0, 2*np.pi, 10000) # intentionally use many points to exaggerate the issue
x = np.cos(t)
y = np.sin(t)
z = np.zeros_like(t)
marker_size = 4
fig = go.Figure()
fig.add_trace(go.Scatter3d(x=x, y=y, z=z, mode='lines'))
fig.add_trace(go.Scatter3d(
x=[0.0, 0.0], y=[0.0, 0.0], z=[0.0, 0.0],
marker=go.scatter3d.Marker(size=marker_size),
line=dict(width=3.0),
showlegend=False,
))
fig.update_layout(
uirevision='constant',
autosize=False,
width=900,
height=900,
scene=dict(
xaxis=dict(range=[-1, 1]),
yaxis=dict(range=[-1, 1]),
zaxis=dict(range=[-1, 1.0]),
aspectratio=dict(x=2, y=2, z=2),
),
)
app.layout = html.Div(children=[
dcc.Graph(
id='example-graph',
figure=fig
),
html.Div(
[
dcc.Slider(
id='slider-phi',
min=0.0,
max=360.0,
step=1.0,
value=0.0,
marks={0: '0', 180: '180', 360: '360'},
updatemode='drag',
),
],
style=dict(width='50%'),
),
html.Div(children='', id='output-box'),
])
@app.callback(
Output('example-graph', 'figure'),
[Input('slider-phi', 'value')],
[State('example-graph', 'figure')]
)
def display_structure(phi, myfig):
myfig['data'][1]['x'][1] = np.cos(np.radians(phi))
myfig['data'][1]['y'][1] = np.sin(np.radians(phi))
myfig['data'][1]['z'][1] = 0
return myfig
if __name__ == '__main__':
app.run_server(debug=True)
Upvotes: 2
Views: 2421
Reputation: 6014
The lack of responsiveness can be attributed to two key factors. The first is, as you note, that the whole figure is updated each time rather than just the desired trace. You can avoid this by targeting the extendData
property instead of the figure
property,
@app.callback(Output('example-graph', 'extendData'), [Input('slider-phi', 'value')])
def update_data(phi):
# tuple is (dict of new data, target trace index, number of points to keep)
return dict(x=[[0, np.cos(np.radians(phi))]], y=[[0, np.sin(np.radians(phi))]]), [1], 2
The second factor is that the callback is executed server side rather than client side, i.e. requests are exchange between client and server each time you move the slider. To avoid this, you can move the update to the client by converting the callback to a clientside callback,
app.clientside_callback(
"""
function(phi) {
// tuple is (dict of new data, target trace index, number of points to keep)
return [{x: [[0, Math.cos(phi/180*Math.PI)]], y:[[0, Math.sin(phi/180*Math.PI)]]}, [1], 2]
}
""", Output('example-graph', 'extendData'), [Input('slider-phi', 'value')]
)
which should yield reasonable responsiveness. Here is how it looks on my laptop,
For completeness, here is the full code,
import dash
import dash_core_components as dcc
import dash_html_components as html
import plotly.graph_objects as go
from dash.dependencies import Input, Output, State
import numpy as np
app = dash.Dash(__name__)
# plot a circle
t = np.linspace(0, 2*np.pi, 10000) # intentionally use many points to exaggerate the issue
x = np.cos(t)
y = np.sin(t)
z = np.zeros_like(t)
marker_size = 4
fig = go.Figure()
fig.add_trace(go.Scatter3d(x=x, y=y, z=z, mode='lines'))
fig.add_trace(go.Scatter3d(
x=[0.0, 0.0], y=[0.0, 0.0], z=[0.0, 0.0],
marker=go.scatter3d.Marker(size=marker_size),
line=dict(width=3.0),
showlegend=False,
))
fig.update_layout(
uirevision='constant',
autosize=False,
width=900,
height=900,
scene=dict(
xaxis=dict(range=[-1, 1]),
yaxis=dict(range=[-1, 1]),
zaxis=dict(range=[-1, 1.0]),
aspectratio=dict(x=2, y=2, z=2),
),
)
app.layout = html.Div(children=[
dcc.Graph(
id='example-graph',
figure=fig
),
html.Div(
[
dcc.Slider(
id='slider-phi',
min=0.0,
max=360.0,
step=1.0,
value=0.0,
marks={0: '0', 180: '180', 360: '360'},
updatemode='drag',
),
],
style=dict(width='50%'),
),
html.Div(children='', id='output-box'),
])
app.clientside_callback(
"""
function(phi) {
// tuple is (dict of new data, target trace index, number of points to keep)
return [{x: [[0, Math.cos(phi/180*Math.PI)]], y:[[0, Math.sin(phi/180*Math.PI)]]}, [1], 2]
}
""", Output('example-graph', 'extendData'), [Input('slider-phi', 'value')]
)
if __name__ == '__main__':
app.run_server(debug=True)
Upvotes: 3
Reputation: 107
It looks like although display_structure
modifies myfig
in place, the return value is taken as a brand new plot which plotly will render all over again.
Is there any chance that the callback does not require a return value? If your function modifies the plot attributes this could be in place.
FYI: I use bokeh
and have limited knowledge about plotly
.
Upvotes: 0