Animations in Dash

@chriddyp Hi Chris, I want to add a animation in Dash, but It seems that the animation does not work in dash。
Here is simple code:

import dash
import dash_core_components as dcc
import dash_html_components as html
import pandas as pd
import numpy as np
import plotly

app = dash.Dash()


# Figure
def make_slider():
    data = [dict(
        visible = False,
        line=dict(color='00CED1', width=6),
        name = str(step),
        x = np.arange(0,10,0.01),
        y = np.sin(step*np.arange(0,10,0.01))) for step in np.arange(0,5,0.1)]
    data[10]['visible'] = True
    steps = []
    for i in range(len(data)):
        step = dict(
            method = 'restyle',
            args = ['visible', [False] * len(data)],
        )
        step['args'][1][i] = True # Toggle i'th trace to "visible"
        steps.append(step)

    sliders = [dict(
        active = 10,
        currentvalue = {"prefix": "Frequency: "},
        pad = {"t": 50},
        steps = steps
    )]

    layout = dict(sliders=sliders)
    fig = dict(data=data, layout=layout)
    return fig

def make_animations():
    url = 'https://raw.githubusercontent.com/plotly/datasets/master/gapminderDataFiveYear.csv'
    dataset = pd.read_csv(url)

    years = ['1952', '1962', '1967', '1972', '1977', '1982', '1987', '1992', '1997', '2002', '2007']
    # make list of continents
    continents = []
    for continent in dataset['continent']:
        if continent not in continents:
            continents.append(continent)
    # make figure
    figure = {
        'data': [],
        'layout': {},
        'frames': []
    }

    # fill in most of layout
    figure['layout']['xaxis'] = {'range': [30, 85], 'title': 'Life Expectancy'}
    figure['layout']['yaxis'] = {'title': 'GDP per Capita', 'type': 'log'}
    figure['layout']['hovermode'] = 'closest'
    figure['layout']['sliders'] = {
        'args': [
            'transition', {
                'duration': 400,
                'easing': 'cubic-in-out'
            }
        ],
        'initialValue': '1952',
        'plotlycommand': 'animate',
        'values': years,
        'visible': True
    }
    figure['layout']['updatemenus'] = [
        {
            'buttons': [
                {
                    'args': [None, {'frame': {'duration': 500, 'redraw': False},
                                 'fromcurrent': True, 'transition': {'duration': 300, 'easing': 'quadratic-in-out'}}],
                    'label': 'Play',
                    'method': 'animate'
                },
                {
                    'args': [[None], {'frame': {'duration': 0, 'redraw': False}, 'mode': 'immediate',
                    'transition': {'duration': 0}}],
                    'label': 'Pause',
                    'method': 'animate'
                }
            ],
            'direction': 'left',
            'pad': {'r': 10, 't': 87},
            'showactive': False,
            'type': 'buttons',
            'x': 0.1,
            'xanchor': 'right',
            'y': 0,
            'yanchor': 'top'
        }
    ]

    sliders_dict = {
        'active': 0,
        'yanchor': 'top',
        'xanchor': 'left',
        'currentvalue': {
            'font': {'size': 20},
            'prefix': 'Year:',
            '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': []
    }

    # make data
    year = 1952
    for continent in continents:
        dataset_by_year = dataset[dataset['year'] == year]
        dataset_by_year_and_cont = dataset_by_year[dataset_by_year['continent'] == continent]

        data_dict = {
            'x': list(dataset_by_year_and_cont['lifeExp']),
            'y': list(dataset_by_year_and_cont['gdpPercap']),
            'mode': 'markers',
            'text': list(dataset_by_year_and_cont['country']),
            'marker': {
                'sizemode': 'area',
                'sizeref': 200000,
                'size': list(dataset_by_year_and_cont['pop'])
            },
            'name': continent
        }
        figure['data'].append(data_dict)

    # make frames
    for year in years:
        frame = {'data': [], 'name': str(year)}
        for continent in continents:
            dataset_by_year = dataset[dataset['year'] == int(year)]
            dataset_by_year_and_cont = dataset_by_year[dataset_by_year['continent'] == continent]

            data_dict = {
                'x': list(dataset_by_year_and_cont['lifeExp']),
                'y': list(dataset_by_year_and_cont['gdpPercap']),
                'mode': 'markers',
                'text': list(dataset_by_year_and_cont['country']),
                'marker': {
                    'sizemode': 'area',
                    'sizeref': 200000,
                    'size': list(dataset_by_year_and_cont['pop'])
                },
                'name': continent
            }
            frame['data'].append(data_dict)

        figure['frames'].append(frame)
        slider_step = {'args': [
            [year],
            {'frame': {'duration': 300, 'redraw': False},
             'mode': 'immediate',
           'transition': {'duration': 300}}
         ],
         'label': year,
         'method': 'animate'}
        sliders_dict['steps'].append(slider_step)


    figure['layout']['sliders'] = [sliders_dict]
    return figure

app.layout = html.Div([
    html.H4('Slider'),
    dcc.Graph(
        id='graph1',
        figure = make_slider()
    ),
    html.H4('Animations'),
    dcc.Graph(
        id='graph2',
        animate=True,
        figure = make_animations()
    ),
])


if __name__ == '__main__':
    app.run_server(host='0.0.0.0', port=8099, debug=True)

2 Likes

Thanks for reporting! Yeah, frames is not supported in figure right now. We’ll need to modify with something like:

const {data, layout, config, frames} = this.props;
Plotly.newPlot(id, data, layout, config).then(() => {
    Plotly.addFrames(id, frames);
    Plotly.animate()
})

In this case, the graph will animate whenever the graph’s frames property changes. We’ll need to experiment with this.

Another approach to the general animations is to use the animate=True property of the dcc.Graph and to tie a “play” button with a dcc.Interval component. I haven’t played around with this yet myself. A solution like this would allow you to supply the “frames” (in this case they’re just new figure properties) one by one instead of having to precompute them all upfront.

If any company out there would like for us to prioritize this work, please reach out: https://plot.ly/products/consulting-and-oem/

3 Likes

@chriddyp thank you for the detailed reply.

Hi,
I’ve been trying to add animate=True as an argument of dcc.Graph, but that doesn’t seem to work (it makes my whole app bug actually). Would there be a nice example available to explain how to animate a figure in Dash? (like the Getting started example, for which I couldn’t find the code with the animation).
Cheers!

animate=True only works in a couple of contexts right now:
1 - Graphs that aren’t adding or removing traces
2 - Scatter or line plots
3 - SVG plots, not their webgl equivalents (e.g. scatter not scattergl)

(1) is relatively easy to fix inside dcc.Graph. .animate method that can handle any type of figure diff · Issue #1849 · plotly/plotly.js · GitHub is the ultimate solution but before that gets merged we can just patch dcc.Graph inside here: dash-core-components/src/components/Graph.react.js at master · plotly/dash-core-components · GitHub with a few checks to see if the user is adding or removing traces and, if so, call newPlot instead of animate. This would be a great first community PR.

(2) and (3) require deeper changes to plotly.js, there might be issues created for them already in Issues · plotly/plotly.js · GitHub

If your organization or company has a budget for software, these fixes and features can always be prioritized and sponsored directly, Consulting, Training & Open-Source Development

1 Like

Thanks a lot for this detailed answer. It seems that I respect the three conditions you mentioned, but I think that I’m facing a very weird kind of bug, which may involve interaction with the datatable or the tab components. I’ve been trying to isolate my code to capture where exactly the bug is occurring, with no success for now. I’ll post again in this topic if I find time to do this.
Cheers!

That would be really helpful, thanks!

So, for those facing the same bug as me, I’ve actually found where the problem is coming from. Say you want to have an animated figure, but you don’t have yet the data to generate your figure. You can normally use dcc.Graph(id='graph-anim', animate = True) without setting a figure argument, and when the data is ready, you can use a callback like:

@app.callback(Output('graph-anim', 'figure'),
            [Input('hidden-data', 'children')])
def update_figure(data):
            #yourcode

Problem : if the callback for the figure argument of an animated figure return None or 'none', the whole app will start behaving in a very bad way (the other callbacks won’t respond anymore), but no problem will be raised in the console.

I solved the problem with a very simple trick:

if data is None:
  return {'data' : [go.Scatter( x=[np.nan], y=[np.nan])]}

Hope this helps!

2 Likes

Hello Chris,
I am looking for some hack to use Frames inside Dash App. Can you help me explain this by using an example on how to use this inside App.

const {data, layout, config, frames} = this.props;
Plotly.newPlot(id, data, layout, config).then(() => {
Plotly.addFrames(id, frames);
Plotly.animate()
})

Thank You in advance.
Best Regards,
Apurva

I don’t think this is possible right now. However, it seems like it’d be a good addition to the dcc.Graph component.

Thank You for your reply.
I will try creating plotly.js graph and call that in div tag of dash app.

Another way to do this would be to set the srcDoc property of the html.Iframe component with the output of the plotly.offline.plot call. Note that graphs created this way won’t be able to be the callback Inputs the way that the dcc.Graph is able to be (Part 3. Interactive Graphing and Crossfiltering | Dash for Python Documentation | Plotly).
For more on srcDoc property, see Folium maps and Dash - #2 by chriddyp.

I’d still like for Dash to support this “the right way” as described in the comments I made above (e.g. Animations in Dash - #2 by chriddyp)

This was really helpful. Thanks a lot.

@chriddyp Does this mean that we cannot port the gapminder plot.ly demo to our own cloud server, AWS for instance?

I am able to successfully run that demo in Jupyter notebook and would like to port it to my EC2 docker container so I can prove that animations in ploty.ly/Dash will work in the cloud.

Should I not try this or have you extended Dash to include frames as of yet?

This makes for a crucial step in proof of concept in mine and many other efforts to serve interactive data science animations using your frameworks.