Basically, I have a plotly animation which uses a slider and pause/play buttons to go through a dataset. I want to extract the number of the current frame (i.e., the current index in the ‘steps’/‘frames’ lists which the slider is on) in a Dash callback, so that I can update a table based on the main graph.
For example, in this situation:
I would like to be able to get ‘6’, the current step number, from the figure.
Here is some example code with a toy dataset, but the same basic UI and structure (from above, minus the buttons to reduce length of code block):
import pandas as pd import dash import dash_core_components as dcc import dash_html_components as html from dash.dependencies import Input, Output import plotly.graph_objects as go external_stylesheets = ['https://codepen.io/chriddyp/pen/bWLwgP.css'] app = dash.Dash(__name__, external_stylesheets=external_stylesheets) # Dataset x = [10, 1, 3, 4, 5, 6, 7, 8, 9, 10] y = [10, 1, 3, 4, 5, 6, 7, 8, 9, 10] df = pd.DataFrame(list(zip(x, y)), columns = ['x', 'y']) # Adding a trace trace = go.Scatter(x=df.x[0:2], y=df.y[0:2], name='Location', mode='markers', marker=dict(color="white", size=10, line=dict( color='DarkSlateGrey', width=2) ) ) # Adding frames frames = [dict(name=k,data= [dict(type='scatter', x=df.x[k:k + 1], y=df.y[k:k + 1], ), ], traces = [0], ) for k in range(len(df) - 1)] fig = go.Figure(data=[trace], frames=frames) # Adding a slider sliders = [{ 'yanchor': 'top', 'xanchor': 'left', 'active': 1, 'currentvalue': {'font': {'size': 16}, 'prefix': 'Steps: ', 'visible': True, 'xanchor': 'right'}, 'transition': {'duration': 200, 'easing': 'linear'}, 'pad': {'b': 10, 't': 50}, 'len': 0.9, 'x': 0.15, 'y': 0, 'steps': [{'args': [[k], {'frame': {'duration': 200, 'easing': 'linear', 'redraw': False}, 'transition': {'duration': 0, 'easing': 'linear'}}], 'label': k, 'method': 'animate'} for k in range(len(df) - 1) ]}] fig['layout'].update(sliders=sliders) app.layout = html.Div(children=[ html.Div([ dcc.Graph( id= 'my-graph', figure=fig ), html.Br(), html.Div(id='my-output'), ]) ]) @app.callback( Output(component_id='my-output', component_property='children'), Input(component_id='my-graph', component_property='figure') ) # How to get the current frame index here? def update_output_div(figure): return 'Output: {}'.format(figure['layout']['sliders'][0]) if __name__ == '__main__': app.run_server(debug=True)
Basically, in that callback, I just want to get the current index of the slider, i.e. the current frame that the animation is on. It’s displayed by the ‘Steps’ tag above the slider, so it clearly exists somewhere, but I can’t find it for the life of me (tried going through the Github source code, but couldn’t locate it).
I would really appreciate any help with this! My dataset is fairly large (20 mb) and doesn’t fit into browser memory, so I haven’t had much luck with a Dash solution using dcc.Slider and dcc.Graph that is still performant.
Advertisement
Answer
- https://community.plotly.com/t/trigger-callback-on-animation-frame-change/46049 there is no way to trigger a callback on a frame change
- have switched to using dash slider and button
- now animation can be controlled with callbacks and with
setFrame()
callback acting on a frame change from the slider - have included plotly button and slider as commented out code for reference
import plotly.graph_objects as go import numpy as np from jupyter_dash import JupyterDash import dash_core_components as dcc import dash_html_components as html from dash.dependencies import Input, Output, State # construct a figure with frames frames=[go.Frame(name=n, data=go.Scatter(y=np.random.uniform(1, 5, 50))) for n in range(8)] fig = go.Figure(data=frames[0].data, frames=frames) # fig = fig.update_layout( # updatemenus=[{"buttons": [{"args": [None, {"frame": {"duration": 500, "redraw": True}}], # "label": "▶", # "method": "animate",},], # "type": "buttons",}], # sliders=[{"steps": [{"args": [[f.name],{"frame": {"duration": 0, "redraw": True}, "mode": "immediate",},], # "label": f.name, "method": "animate",} # for f in frames], # }],) # Build App app = JupyterDash(__name__) app.layout = html.Div( [dcc.Graph(id="graph", figure=fig), html.Button("Play", id="dashPlay", n_clicks=0), dcc.Slider(id="dashSlider", min=0, max=len(frames)-1, value=0, marks={i:{"label":str(i)} for i in range(len(frames))}), dcc.Interval(id="animateInterval", interval=400, n_intervals=0, disabled=True), html.Div(id="whichframe", children=[]), ], ) # core update of figure on change of dash slider @app.callback( Output("whichframe", "children"), Output("graph", "figure"), Input("dashSlider", "value"), ) def setFrame(frame): if frame: tfig = go.Figure(fig.frames[frame].data, frames=fig.frames, layout=fig.layout) try: tfig.layout['sliders'][0]['active'] = frame except IndexError: pass return frame, tfig else: return 0, fig # start / stop Interval to move through frames @app.callback( Output("animateInterval","disabled"), Input("dashPlay", "n_clicks"), State("animateInterval","disabled"), ) def play(n_clicks, disabled): return not disabled @app.callback( Output("dashSlider", "value"), Input("animateInterval", "n_intervals"), State("dashSlider", "value") ) def doAnimate(i, frame): if frame < (len(frames)-1): frame += 1 else: frame = 0 return frame # Run app and display result inline in the notebook app.run_server(mode="inline")