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.
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!