Slider with play button for animations (independent of plotly)

As far as I can see there is no possibility to trigger animations using the usual slider in dash. E.g. there is an option for Shiny which adds a play button to the slider (Shiny - Using sliders).

I know there are sliders with animation button for plotly. However, I would like to use it to trigger an animation for dash-leaflet, hence this does not help. Is there a way to get a slider moving automatically?

Hi @AlexanderGerber and welcome to the Dash community

That would be a nice feature to add to dcc.Slider()

But in the meantime you can use dcc.Interval() to animate manually. Here is an example:

import dash
import dash_core_components as dcc
import dash_html_components as html
from dash.dependencies import Input, Output, State
import plotly.express as px

import pandas as pd

external_stylesheets = ["https://codepen.io/chriddyp/pen/bWLwgP.css"]

app = dash.Dash(__name__, external_stylesheets=external_stylesheets)


df = pd.read_csv(
    "https://raw.githubusercontent.com/plotly/datasets/master/gapminderDataFiveYear.csv"
)
years = list(set(df["year"]))
years.sort()


def make_fig(year):
    return px.scatter(
        df[df.year == year],
        x="gdpPercap",
        y="lifeExp",
        size="pop",
        color="continent",
        hover_name="country",
        log_x=True,
        size_max=55,
    )


app.layout = html.Div(
    [
        dcc.Interval(id="animate", disabled=True),
        dcc.Graph(id="graph-with-slider", figure=make_fig(1952)),
        dcc.Slider(
            id="year-slider",
            min=df["year"].min(),
            max=df["year"].max(),
            value=df["year"].min(),
            marks={str(year): str(year) for year in df["year"].unique()},
            step=None,
        ),
        html.Button("Play", id="play"),
    ]
)


@app.callback(
    Output("graph-with-slider", "figure"),
    Output("year-slider", "value"),
    Input("animate", "n_intervals"),
    State("year-slider", "value"),
    prevent_initial_call=True,
)
def update_figure(n, selected_year):
    index = years.index(selected_year)
    index = (index + 1) % len(years)
    year = years[index]
    return make_fig(year), year


@app.callback(
    Output("animate", "disabled"),
    Input("play", "n_clicks"),
    State("animate", "disabled"),
)
def toggle(n, playing):
    if n:
        return not playing
    return playing

Thank you @AnnMarieW! This will work well enough for me at the moment.

Thank you @AlexanderGerber for asking and thank you @AnnMarieW for the great example.
It appears to me, implementing the animation makes it impossible to manually select a year using the slider. Is there a workaround for that too?

Hi @StefanB and welcome to the Dash community :slight_smile:

Nice catch - you can fix this by changing

State("year-slider", "value"),

to

Input("year-slider", "value"),

in the first callback.

And I was just thinking… This would be a great use-case for the new All-in-One component!. Then you would be able to add a slider with a play button to a graph with one line of code :tada:

3 Likes

Hi guys, I wanted to ask how the current situation looks. I am also interested in the feature. Are you considering to implement this natively?

I actually started to work on an All In One Component to do this. The following code should accomplish and simple slider with playback. I also have some pseudo code down for adding forward/backward and speed adjustment buttons, but I haven’t had time to add those yet. The text at the bottom of the gif below is the output.

Here is the basic slider in action:
2022-04-18-09-21-07

Inside of slider_player_aio.py

# package imports
from dash import html, dcc, Output, Input, State, callback, MATCH
from dash.exceptions import PreventUpdate
import dash_bootstrap_components as dbc
import uuid

class PlaybackSliderAIO(html.Div):

    class ids:
        play = lambda aio_id: {
            'component': 'PlaybackSliderAIO',
            'subcomponent': 'button',
            'aio_id': aio_id
        }
        play_icon = lambda aio_id: {
            'component': 'PlaybackSliderAIO',
            'subcomponent': 'i',
            'aio_id': aio_id
        }
        slider = lambda aio_id: {
            'component': 'PlaybackSliderAIO',
            'subcomponent': 'slider',
            'aio_id': aio_id
        }
        interval = lambda aio_id: {
            'component': 'PlaybackSliderAIO',
            'subcomponent': 'interval',
            'aio_id': aio_id
        }

    ids = ids

    def __init__(
        self,
        button_props=None,
        slider_props=None,
        interval_props=None,
        aio_id=None
    ):
        if aio_id is None:
            aio_id = str(uuid.uuid4())

        button_props = button_props.copy() if button_props else {}
        slider_props = slider_props.copy() if slider_props else {}
        interval_props = interval_props.copy() if interval_props else {}
        
        button_props['active'] = False

        super().__init__([
            dbc.Button(
                html.I(id=self.ids.play_icon(aio_id)),
                id=self.ids.play(aio_id),
                **button_props
            ),
            dcc.Slider(id=self.ids.slider(aio_id), **slider_props),
            dcc.Interval(id=self.ids.interval(aio_id), **interval_props),
        ])

    @callback(
        Output(ids.play(MATCH), 'active'),
        Output(ids.play_icon(MATCH), 'className'),
        Output(ids.interval(MATCH), 'disabled'),
        Input(ids.play(MATCH), 'n_clicks'),
        State(ids.play(MATCH), 'active')
    )
    def toggle_play(clicks, curr_status):
        if clicks:
            text = 'fa-solid fa-play' if curr_status else 'fa-solid fa-pause'
            return not curr_status, text, curr_status
        return curr_status, 'fa-solid fa-play', not curr_status

    @callback(
        Output(ids.slider(MATCH), 'value'),
        Input(ids.play(MATCH), 'active'),
        Input(ids.interval(MATCH), 'n_intervals'),
        State(ids.slider(MATCH), 'min'),
        State(ids.slider(MATCH), 'max'),
        State(ids.slider(MATCH), 'step'),
        State(ids.slider(MATCH), 'value')
    )
    def start_playback(play, interval, min, max, step, value):
        if not play:
            raise PreventUpdate
        
        new_val = value + step
        if new_val > max:
            new_val = min

        return new_val

Then for use cases, add this code to app.py and run it

from playback_slider_aio import PlaybackSliderAIO
from dash import Dash, html, callback, Output, Input
import dash_bootstrap_components as dbc

app = Dash(
    __name__,
    external_stylesheets=[
        dbc.themes.BOOTSTRAP,
        dbc.icons.FONT_AWESOME
    ]
)
app.layout = html.Div([
    PlaybackSliderAIO(
        aio_id='bruh',
        slider_props={'min': 0, 'max': 10, 'step': 1, 'value': 0},
        button_props={'className': 'float-left'}
    ),
    html.Div(id='text')
])


@callback(
    Output('text', 'children'),
    Input(PlaybackSliderAIO.ids.slider('bruh'), 'value')
)
def update_children(val):
    return val


if __name__ == "__main__":
    app.run_server(debug=True)

3 Likes

Looks very good! An option to set the speed from the beginning would be very useful indeed.

You set the speed of the interval by passing in the props of the dcc.Interval to the AIO component as a dictionary.

    PlaybackSliderAIO(
        aio_id='bruh',
        slider_props={'min': 0, 'max': 10, 'step': 1, 'value': 0},
        button_props={'className': 'float-left'},
        interval_props={'interval': 500}  # number of milliseconds, default is 1000
    )
1 Like

How can I specify that it stops when it reaches the last value, i.e. the end of the slider? Currently it automatically starts from the beginning again.

There is not an easy way to do it with passing a property to the AIO component. Instead you’ll want to modify the AIO component’s callbacks to not reset. The start_playback method is where it resets to the minimum. If you want to stop at the maximum, you’ll need to disable the interval, so the dash application stops updating every time.

There might be a better way to accomplish this.

   @callback(
        Output(ids.slider(MATCH), 'value'),
        Output(ids.interval(MATCH), 'disabled'),
        Input(ids.play(MATCH), 'active'),
        Input(ids.interval(MATCH), 'n_intervals'),
        State(ids.slider(MATCH), 'min'),
        State(ids.slider(MATCH), 'max'),
        State(ids.slider(MATCH), 'step'),
        State(ids.slider(MATCH), 'value')
    )
    def start_playback(play, interval, min, max, step, value):
        if not play:
            raise PreventUpdate
        
        new_val = value + step
        if new_val > max:
            return max, True

        return new_val, False
1 Like