Stopwatch / countdown timer

I have implemented a countdown timer using dcc.Interval triggered callbacks tied to the output of a daq.LEDDisplay element as part of a web app. However, when the user navigates away from the app on their mobile device, timing accuracy is lost (the countdown timer seems to miss interval updates while away from the web app). Is there a way to implement such a timer in dash where the timing itself is controlled on the client side such that timing accuracy is always maintained?

Try this:

# yourapp/assets/clientside.js

if (!window.dash_clientside) { window.dash_clientside = {}; }
window.dash_clientside.clientside = {
    update_timer: function (value) {
        return new Date().toUTCString();
    }
}
# yourapp/timer.py

import dash
import dash_html_components as html
import datetime
import dash_bootstrap_components as dbc
import dash_core_components as dcc
import time

app = dash.Dash(name=__name__)

app.layout = dbc.Container([dbc.Row([dbc.Col(dbc.Label("Client time is:"), width=2),
                                     dbc.Col(html.H2('', id='client-time'), width=10)]),
                            dbc.Row([dbc.Col(dbc.Label("Server time is:"), width=2),
                                     dbc.Col(html.H2('', id='server-time'), width=10)]),
                                     dcc.Interval(id='interval', interval=16, n_intervals=0)])

# using serverside callback
@app.callback(dash.dependencies.Output('server-time', 'children'),
              [dash.dependencies.Input('interval', 'n_intervals')])
def update_timer(_):
    return datetime.datetime.now().strftime("%c")

# using clientside callback (remember to create the clientside.js file in the assets folder!)
app.clientside_callback(
    dash.dependencies.ClientsideFunction(
        namespace='clientside',
        function_name='update_timer'
    ),
    dash.dependencies.Output('client-time', 'children'),
    [dash.dependencies.Input('interval', 'n_intervals')])


if __name__ == '__main__':
    app.run_server(port=8889)

This demonstrates two ways to update a timer node with the current timestamp: one using a typical serverside callback, the other using a (much faster) clientside callback. The clientside callback also has the advantage of using the browser’s local time to generate the timestamp, so timezone differences are not a factor.

1 Like

Thanks @neoncontrails – looks like I’m catching up on clientside callbacks!

As a followup, is there a way to keep a clientside javascript stopwatch running with setInterval (e.g. https://jsbin.com/IgaXEVI/167/edit?html,js,output) in response to dash n_clicks events without using dash server interval triggers?

without using dash server interval triggers?

I would have to look at the implementation before I could say this for sure, but my hunch is that dcc.Interval is in fact already setting up a clientside event loop (e.g., using Javascript’s setInterval function) that increments the value of its n_intervals property every x milliseconds.

This hunch is somewhat corroborated by the observation that instantiating a dcc.Interval component that isn’t registered to a running Dash application (like in a REPL or something), its n_intervals property remains unchanged:

In [1]: import dash_core_components as dcc                                                       

In [2]: foo = dcc.Interval(id='foo', interval=60, n_intervals=0)                                 

In [3]: foo.n_intervals                                                                          
Out[3]: 0

In [4]: foo.n_intervals                                                                          
Out[4]: 0

That’s consistent with what I would expect to see if n_intervals were incremented by the browser.

(Perhaps @alexcjohnson could confirm/deny?)

Yep, it’s handled by the browser. See the handleTimer logic in dash_core_components.js below:

var $o = function(e) {
    function t(e) {
        var n;
        return function(e, t) {
            if (!(e instanceof t)) throw new TypeError("Cannot call a class as a function")
        }(this, t), (n = function(e, t) {
            return !t || "object" !== Jo(t) && "function" != typeof t ? Ko(e) : t
        }(this, Qo(t).call(this, e))).intervalId = null, n.reportInterval = n.reportInterval.bind(Ko(n)), n.handleTimer = n.handleTimer.bind(Ko(n)), n
    }
    var n, o, r;
    return function(e, t) {
        if ("function" != typeof t && null !== t) throw new TypeError("Super expression must either be null or a function");
        e.prototype = Object.create(t && t.prototype, {
            constructor: {
                value: e,
                writable: !0,
                configurable: !0
            }
        }), t && qo(e, t)
    }(t, e), n = t, (o = [{
        key: "handleTimer",
        value: function(e) {
            0 === e.max_intervals || e.disabled || e.n_intervals >= e.max_intervals && -1 !== e.max_intervals ? this.intervalId && this.clearTimer() : this.intervalId || (this.intervalId = window.setInterval(this.reportInterval, e.interval))
        }
    }, {
        key: "resetTimer",
        value: function(e) {
            this.clearTimer(), this.handleTimer(e)
        }
    }, {
        key: "clearTimer",
        value: function() {
            window.clearInterval(this.intervalId), this.intervalId = null
        }
    }, {
        key: "reportInterval",
        value: function() {
            var e = this.props;
            (0, e.setProps)({
                n_intervals: e.n_intervals + 1
            })
        }
    }, {
        key: "componentDidMount",
        value: function() {
            this.handleTimer(this.props)
        }
    }, {
        key: "componentWillReceiveProps",
        value: function(e) {
            e.interval !== this.props.interval ? this.resetTimer(e) : this.handleTimer(e)
        }
    }, {
        key: "componentWillUnmount",
        value: function() {
            this.clearTimer()
        }
    }, {
        key: "render",
        value: function() {
            return null
        }
    }]) && Uo(n.prototype, o), r && Uo(n, r), t
}(i.Component);
$o.propTypes = {
    id: r.a.string,
    interval: r.a.number,
    disabled: r.a.bool,
    n_intervals: r.a.number,
    max_intervals: r.a.number,
    setProps: r.a.func
}, $o.defaultProps = {
    interval: 1e3,
    n_intervals: 0,
    max_intervals: -1
};

Nice – thanks for the sleuthing @neoncontrails. I will use dcc.Interval together with a clientside callback without any more worries!

For reference, you can also check out the unminified JS code on GitHub here: https://github.com/plotly/dash-core-components/blob/dev/src/components/Interval.react.js

The logic for all components is entirely in JavaScript. The input parameters are simply sent up to the browser and new changed property values are simply sent back to the callbacks from the web browser.

This architecture is what makes Dash easily portable in other languages, like R!

1 Like