I’m developing a multi-page app where I need to keep the x axes of graphs on several pages synchonized. My current solution (code at the end) works, but has a major problem which I could not yet solve.
Currently, I set up a “shared” dcc.Store
that keeps track of the current x axis range across all pages:
dcc.Store(id='shared-datarange', storage_type='local')
This Store updates whenever the user alters the x axis range, using the relayoutData
property of the relevant graphs as Input
via a pattern-matching callback. Each page in turn uses this Store’s data
as Input
to update their Graph’s figure
accordingly. In this example they calculate the according y values using mathematical functions, but in my use case this is a potentially costly fetch operation.
This works, as seen in this GIF:
The problem arises when I have multiple parts of my app open at the same time in different tabs, which is a common scenario in my use case. The synchronization is still there, however the callbacks are still executed even in the background tab. This can cause many unnecessary callbacks, e.g. when the user shifts the x axis ten times on /app1 without looking at the tab containing /app2, the callbacks for /app2 will also trigger. I put print statements in the callbacks to illustrate this:
I noticed that the behaviour differs depending on the storage_type
used for the shared dcc.Store
. If I use anything other than local
, the background callback does not happen anymore, but then the different tabs aren’t synchronized anymore.
My desired behaviour in the case of multiple open tabs would be this:
- Callbacks are not executed on background tabs, to avoid unnecessary callbacks.
- Upon selecting another tab, the current (most recently selected) datarange is used in that tab.
How could I achieve this? Is there any ways to prevent a callback from firing if the elements it targets are not currently in view (i.e. in another, not selected tab)? And how can I make it so that when the elements come into view again (i.e. the tab is selected), the callback does get fired to load the current datarange?
Here is my directory structe:
- app.py
- index.py
- apps
|--__init__.py
|-- app1.py
|-- app2.py
And the code (__init__.py
is empty):
app.py
import dash
app = dash.Dash(__name__, suppress_callback_exceptions=True)
index.py
import dash
import dash_core_components as dcc
import dash_html_components as html
from dash.dependencies import Input, Output, ALL
from app import app
from apps import app1, app2
app.layout = html.Div([
dcc.Location(id='url'),
dcc.Store(id='shared-datarange', data=[1,10], storage_type='local'),
html.Div(id='page-content')
])
@app.callback(Output('page-content', 'children'),
Input('url', 'pathname'))
def display_page(pathname):
if pathname == '/app1': return app1.layout
elif pathname == '/app2': return app2.layout
return [
dcc.Link('App 1', href='/app1'),
html.Br(),
dcc.Link('App 2', href='/app2'),
]
@app.callback(Output('shared-datarange', 'data'),
Input({'typ':'graph', 'page':ALL}, 'relayoutData'),
prevent_initial_call=True)
def apply_relayout(relayout_data):
try:
triggered = dash.callback_context.triggered[0]['value']
start = int(triggered['xaxis.range[0]'])
end = int(triggered['xaxis.range[1]'])
return [start, end]
except (KeyError, TypeError):
return dash.no_update
if __name__ == '__main__':
app.run_server(debug=True)
app1.py
import dash_core_components as dcc
import dash_html_components as html
from dash.dependencies import Input, Output
import plotly.graph_objects as go
from app import app
layout = html.Div([
html.H3('App 1', id='dummy1'),
dcc.Link('Go to App 2', href='/app2'),
dcc.Graph(id={'typ':'graph', 'page':'app1'}),
])
@app.callback(Output({'typ':'graph', 'page':'app1'}, 'figure'),
Input('shared-datarange', 'data'),
Input('dummy1', 'id')) # input needed for initial trigger?
def draw_graph(datarange, _):
print('App 1 - draw_graph() triggered')
start, end = datarange
x = [i for i in range(start, end+1)]
y = [i**2 for i in x]
fig = go.Figure(go.Scatter(x=x, y=y))
fig.update_yaxes(fixedrange=True)
return fig
app2.py
import dash_core_components as dcc
import dash_html_components as html
from dash.dependencies import Input, Output
import plotly.graph_objects as go
from app import app
layout = html.Div([
html.H3('App 2', id='dummy2'),
dcc.Link('Go to App 1', href='/app1'),
dcc.Graph(id={'typ':'graph', 'page':'app2'}),
])
@app.callback(Output({'typ':'graph', 'page':'app2'}, 'figure'),
Input('shared-datarange', 'data'),
Input('dummy2', 'id')) # input needed for initial trigger?
def draw_graph(datarange, _):
print('App 2 - draw_graph() triggered')
start, end = datarange
x = [i for i in range(start, end+1)]
y = [10*i-i**2 for i in x]
fig = go.Figure(go.Scatter(x=x, y=y))
fig.update_yaxes(fixedrange=True)
return fig