Skip to content
Advertisement

Is there a way to extract the current frame from a plotly figure?

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:

Dash app with slider

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

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")
User contributions licensed under: CC BY-SA
4 People found this is helpful
Advertisement