Black Lives Matter. Please consider donating to Black Girls Code today.

Loop through different apps after a specified period of time

I used the guide at https://plot.ly/dash/urls to produce 4 apps that produce charts on different pages. I’m able between with the help of html.Link and html.Location. What I want to do now is to loop through the different pages after a specified period of time(say 30 secs) automatically without having to click on a link or change the url. I need this for a dashboard I am creating.

Yep, Dash can do this for you. You’ll want to use the interval component for changing the page periodically. The callback that takes this as in input will then need to target the children property of the element you want to swap out the contents of. Then you can use the number of times the interval component has fired to determine which layout to return (eg n_intervals modulo the number of layouts). The slightly tricky part is wiring all your current apps into the one Dash app that has all your 4 app’s callbacks registered, but the 4 layouts are pulled out only for the interval callback.

Here’s how I might do it, which will involve modifying your 4 apps slightly to import the parent app they all share, and not attaching their layouts to it. Have not tested this though, so hopefully it works!

Edit: I thought you meant you had 4 distinct apps with 4 different Dash instances, but just re-read your post and you mentioned you were using the URL routing. This should be even easier to modify as you just use the layouts that you were returning from your router callback as the ones that get returned from the interval function. You should be able to adapt the following to your needs anyway.

server.py

# use this module to run your app
from dash import Dash

app = Dash(__name__)

app.py

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

from server import app
from app1 import layout as layout1
from app2 import layout as layout2
from app3 import layout as layout3
from app4 import layout as layout4

# Change contents every 10 seconds                                                                                                                                                             
INTERVAL = 10

# app layouts to choose from                                                                                                                                                                   
APP_LAYOUTS = [
    layout1,
    layout2,
    layout3,
    layout4,
]

app.config.supress_callback_exceptions = True
app.layout = html.Div([
    html.Div(id='app-container'),  # your rotating apps will be inserted in here
    dcc.Interval(
        id='interval-component',
        interval=INTERVAL*1000, # in milliseconds                                                                                                                                              
        n_intervals=0
    )
])


@app.callback(Output('app-container', 'children'),
              [Input('interval-component', 'n_intervals')])
def update_metrics(n_intervals):
    return APP_LAYOUTS[n_intervals % len(APP_LAYOUTS)]

app1.py

# a dummy app as an example
from server import app

layout = html.Div([
    html.Div(id='target'),
    dcc.Input(id='input', type='text', value=''),
    html.Button(id='submit', n_clicks=0, children='Save')
])

@app.callback(Output('target', 'children'), [Input('submit', 'n_clicks')],
              [State('input', 'value')])
def callback(n_clicks, state):
    return "callback received value: {}".format(state)

And the remaining apps similar to app1.py.

Thanks. It makes a lot of sense. Though, when I try it out it works well for the first 2 pages, however the 3rd page does not populate and the logs show the error: TypeError: unsupported operand type(s) for %: 'NoneType' and 'int'. For some reason it looks like on the second iteration when n_intervals is 2, the value of n_intervals is not passed to the callback.

That’s odd. I don’t know why it would get None at that specific point. Perhaps double check that there are no errors on the client side in the console of the browser’s dev tools. and that you’re running the latest dash-core-components.

Here’s a minimal example that works in my environment:

from dash import Dash                          
from dash.dependencies import Input, State, Output                                             
import dash_core_components as dcc
import dash_html_components as html

# Change contents every 10 seconds                                                                                                                                                             
INTERVAL = 1

# app layouts to choose from                                                                                                                                                                   
APP_LAYOUTS = [
    html.Div("hello1"),                        
    html.Div("hello2"),                        
    html.Div("hello3"),                        
    html.Div("hello4"),                        
]                                              

app = Dash()                                   
#app.config.supress_callback_exceptions = True                                                                                                                                                 
app.layout = html.Div([                        
    html.Div(id='app-container'),
    dcc.Interval(
        id='interval-component',
        interval=INTERVAL*1000, # in milliseconds                                                                                                                                              
        n_intervals=0
    )
])


@app.callback(Output('app-container', 'children'),                                             
              [Input('interval-component', 'n_intervals')])                                    
def update_metrics(n_intervals):               
    return APP_LAYOUTS[n_intervals % len(APP_LAYOUTS)]                                         


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

Thanks a lot @nedned. I run your mini example above and it has helped me figure out where the issue is. Inside each of my apps, they each had callbacks that update the graphs using an interval component in each app except the very first app. It seems those extra interval elements in the apps somehow corrupted the main interval element that was updating the pages. I had not yet implemented live updates on the first app since its data calls are a bit expensive and take a bit of time, I had pushed this to later which is why the first two pages would load. What I have done now is move the interval events to the main app (what would be app.py in your case) and then reference them in the callbacks in the individual apps. Using your example above, I’m doing something like this:

app.py

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

from server import app
from app1 import layout as layout1
from app2 import layout as layout2
from app3 import layout as layout3
from app4 import layout as layout4

# Change contents every 10 seconds                                                                                                                                                             
INTERVAL = 10

#Interval for app layouts updates every 2 seconds
INTERVAL_APP =2


# app layouts to choose from                                                                                                                                                                   
APP_LAYOUTS = [
    layout1,
    layout2,
    layout3,
    layout4,
]

app.config.supress_callback_exceptions = True
app.layout = html.Div([
    html.Div(id='app-container'),  # your rotating apps will be inserted in here
    dcc.Interval(
        id='interval-component',
        interval=INTERVAL*1000, # in milliseconds                                                                                                                                              
        n_intervals=0
    ),
    dcc.Interval(
        id='interval-layout',
        interval=INTERVAL_APP*1000, # in milliseconds                                                                                                                                              
    )
])


@app.callback(Output('app-container', 'children'),
              [Input('interval-component', 'n_intervals')])
def CHANGE_PAGE(n_intervals):
    return APP_LAYOUTS[n_intervals % len(APP_LAYOUTS)]

app1.py

from server import app

layout = html.Div([
    html.Div(id='target'),
])

@app.callback(Output('target', 'children'), [Input('submit', 'n_clicks')],
              events=[Event('interval-layout', 'interval')])
def callback(n_clicks, state):
    return update_graphs_in_target

This works well for the most part, all pages load. However now it seems that the when a page loads, it first loads the original graph that was loaded at startup and then updates it using the most recent data. After it cycles back round to a particular page, it still loads the older data first before updating What I’d want is for each page to update in the background via it’s own interval elements and for it to present the most recent data it has immediately. Any ideas on this?

Can you describe how the data for the different pages is updated?

I created functions that generate the different kinds of charts that I use (line, bar etc) using data and styling passed as parameters . I use these to generate the graphs when the page loads and to update the graph data via the callbacks in each page. The data is sourced from different database tables. Each of the apps.pages has content similar to this:

from server import app

layout = html.Div([
    dcc.Graph(id="graph1", figure=generate_bar_graph(data),
    dcc.Graph(id="graph2", figure=generate_line_graph(data)
])

@app.callback(Output('graph1', 'figure')
              events=[Event('interval-layout', 'interval')])
def callback():
	#obtain latest data from db
    return generate_bar_graph(latest_data)
    
@app.callback(Output('graph2', 'figure')
              events=[Event('interval-layout', 'interval')])
def callback():
	#obtain latest data from db
    return generate_line_graph(latest_data)

So the problem is that you’re initialising the Graph objects in layout with figure values, which means that the layout has a graph with an initial set of data baked into it that it will always start with before a callback updated its. Typically you’d avoid this problem by not providing an initial figure value and just letting it be initialised by the first execution of the callback on page load.

However in your case it sounds like you actually just need static graphs that aren’t updated until they’re swapped back in, which means I don’t think you want any callbacks at all.

What you probably want to do is not have any callbacks at all other than the one for the interval. Then, rather than the interval callback returning a pre-computed layout, it should evaluate a function which generates the layout you need containing the graph with the current data. For example:

APP_LAYOUTS = [
    layout_func1,
    layout_func2,
    layout_func3,
    layout_func4,
]

def layout_func1():
    data = get_data()
    return dcc.Graph(id="graph1", figure=generate_bar_graph(data))

@app.callback(Output('app-container', 'children'),
              [Input('interval-component', 'n_intervals')])
def CHANGE_PAGE(n_intervals):
    func = APP_LAYOUTS[n_intervals % len(APP_LAYOUTS)]
    return func()

Thanks again @nedned. I implemented this in my application and it does the job. I was worried about load times for the different pages though so far it does seem to be an issue. It has all come together now.

1 Like