Hi there,
I’m looking for some help in building a callback in Dash that updates the frames of an animation.
Specifically, my main figure is a plotly Mesh3d, which has a mesh of a cube, and an animation that makes the cube move: The x,y,z coordinates of the verts change per frame. Now, I’d like users to be able to change the animation of motion to an animation of color change, via a dropdown menu.
However, I’m having trouble updating the figure in a callback: The callback unexpectedly leads the animation slider to influence the camera angle.
Attempt 1
First, I tried simply updating the data
, frames
, and layout
keys by returning these in a dict
(sorry MWE is a bit long…):
import dash
import dash_core_components as dcc
import dash_html_components as html
import plotly.graph_objects as go
import numpy as np
def make_frames(x, y, z, i, j, k,
animation='motion', n_frames=10):
frames = []
for frame_n in np.arange(n_frames, dtype='int'):
if animation == 'motion':
data = go.Mesh3d(x=x+frame_n*.1, y=y+frame_n*.1, z=z, i=i, j=j, k=k, color='rgb(0.5,0.5,0.5)')
elif animation == 'color':
data = go.Mesh3d(x=x, y=y, z=z, i=i, j=j, k=k, color='rgb('+str(frame_n*.1)+',0,0)')
frames.append(go.Frame(data=data, name=str(frame_n)))
return frames
def build_sliders():
# slider for animation
sliders_dict = {
"active": 0,
"yanchor": "top",
"xanchor": "left",
"currentvalue": {
"font": {"size": 20},
"prefix": "Step:",
"visible": True,
"xanchor": "right"
},
"transition": {"duration": 300, "easing": "cubic-in-out"},
"pad": {"b": 10, "t": 50},
"len": 0.9,
"x": 0.1,
"y": 0,
"steps": []
}
# add frames
frames = make_frames(x,y,z,i,j,k, animation='motion')
n_frames = 10
for frame_n in np.arange(n_frames, dtype='int'):
slider_step = {"args": [
[int(frame_n)],
{"frame": {"duration": 300, "redraw": True},
"mode": "immediate",
"transition": {"duration": 300}}
],
"label": int(frame_n),
"method": "animate"}
sliders_dict["steps"].append(slider_step)
return sliders_dict
# verts, faces for cube
x = [0, 0, 1, 1, 0, 0, 1, 1]
y = [0, 1, 1, 0, 0, 1, 1, 0]
z = [0, 0, 0, 0, 1, 1, 1, 1]
i = [7, 0, 0, 0, 4, 4, 6, 6, 4, 0, 3, 2]
j = [3, 4, 1, 2, 5, 6, 5, 2, 0, 1, 6, 3]
k = [0, 7, 2, 3, 6, 7, 1, 1, 5, 5, 7, 6]
# Make figure
fig = go.Figure(data=[
go.Mesh3d(
# 8 vertices of a cube
x=x,
y=y,
z=z,
color='rgb(0.5,0.5,0.5)',
# i, j and k give the vertices of triangles
i=i,
j=j,
k=k,
name='y'
)
])
# add frames, slider, update layout
frames = make_frames(x,y,z,i,j,k, animation='motion')
sliders_dict = build_sliders()
fig['layout']['scene'] = {'aspectmode': 'cube',
'xaxis': {'range': [-3, 3], 'title': 'x', 'gridcolor': '#FFFFFF'},
'yaxis': {'range': [-3, 3], 'title': 'y', 'gridcolor': '#FFFFFF'},
'zaxis': {'range': [-3, 3], 'title': 'z', 'gridcolor': '#FFFFFF'}}
fig['layout']['sliders'] = [sliders_dict]
fig['frames'] = frames
# Create app
external_stylesheets = ['https://codepen.io/chriddyp/pen/bWLwgP.css']
app = dash.Dash(__name__, external_stylesheets=external_stylesheets)
app.layout = html.Div(children=[
html.H1(children='MWE', style={'textAlign': 'center'}),
html.Div([ # Div containing everything
html.Div([ # div for mesh: Left
html.Div([
dcc.Dropdown(id='animation_type',
options=[{'label': i, 'value': i} for i in ['motion', 'color']],
value='motion')
]),
html.Div([
dcc.Graph(
id='mesh_fig',
figure=fig)
])
])
])
])
# Callback
@app.callback(
dash.dependencies.Output('mesh_fig', 'figure'),
[dash.dependencies.Input('animation_type', 'value')])
def update_figure(animation_type):
if animation_type == 'motion':
data = go.Mesh3d(x=x, y=y, z=z,
color='rgb(0.5,0.5,0.5)', i=i, j=j, k=k, name='y')
else:
data = go.Mesh3d(x=x, y=y, z=z,
color='rgb(0,0,0)', i=i, j=j, k=k, name='y')
frames = make_frames(x,y,z,i,j,k,animation=animation_type)
layout_dict = {'scene': {'aspectmode': 'cube',
'xaxis': {'range': [-3, 3], 'title': 'x', 'gridcolor': '#FFFFFF'},
'yaxis': {'range': [-3, 3], 'title': 'y', 'gridcolor': '#FFFFFF'},
'zaxis': {'range': [-3, 3], 'title': 'z', 'gridcolor': '#FFFFFF'}},
'sliders': [build_sliders()]}
return {'data': [data],
'frames': frames,
'layout': layout_dict}
if __name__ == '__main__':
app.run_server(debug=True, host="localhost", port=8050)
At first, this seems to work as intended: After selecting color
from the dropdown menu, the animation slider indeed changes the color (instead of the location) of the cube. However, there’s an annoying side effect: after selecting color
from the dropdown menu, using the animation slider always resets the camera to the default. Hence, you can’t change the angle with which you want to view the animation.
(NB: refreshing the browser window actually ‘solves’ this: if you change the dropdown to color
, and then refresh the window, you can actually use the animation slider to change color without changing the camera angle. Curious, eh?)
Attempt 2
I also tried to use the animate
property of dcc.Graph
as follows:
html.Div([
dcc.Graph(
id='mesh_fig',
figure=fig,
animate=True,
animation_options={'frame': { 'redraw': True}})
])
Which does appear to solve the problem of Attempt 1 (i.e., the camera angle isn’t affected by the animation slider after changing the dropdown menu’s motion
to color
); but unfortunately, this introduces a new problem, namely that the data
seem to change due to the callback, but not the frames
. That is, after selecting color
from the dropdown menu, the animation slider still animates motion (and also reverts the cube’s color to grey). (I know animate is in beta, so I’m guessing this may just not be implemented yet here).
Does anyone know if I’m doing something wrong? Or, if this (especially Attempt 1) is a bug, is there any workaround? (I’m thinking, since the browser window refresh actually makes the figure work as intended again, is there any way to trigger a “refresh” of the div containing the figure or something?
Thanks in advance!