Hi Plotly community,
I’ve been trying to migrate my one page tab based dashboard to a multi-page app with tabs/sub-tabs in pages. However running into a rather stubborn problem:
A nonexistent object was used in an `Output` of a Dash callback. The id of this object is `plotly_figure_1` and the property is `figure`. The string ids in the current layout are: [table_1, table_2, message_string....etc]
Here is my stack post a few days ago: https://stackoverflow.com/questions/77167192/dash-multi-page-app-failing-to-detect-callback-output-ids-in-different-page-layo
I’ve struggled to reproduce the error with a MWE that emulates my use case. I don’t believe it’s the way I’ve modularised my code into different folders, as the app renders and routes exactly as I expect.
The issue comes down to callback output IDs referenced on only certain pages.
My work-around renders the app unusable (referenced in my stack post as “Bad solution”). I create a universal store where all my callback output ID’s are assigned to individual dcc.Store()
objects including plots. This collection of stores (x99) is wrapped in a div, which I then pass into each of the indidual page layouts. However, this is inefficient as plots are now in stores, which then render very slowly. In my original approach, this same collection of dcc.Store()
objects (x10) contain only dataframes and string variables.
Here is a MWE adapted from @AnnMarieW examples for mutli-page apps, that implements validation and modularises the code into different folders. I’m using Dash v2.13.0 and Plotly v5.14.1.
Folder structure:
- app.py
- app_scripts
- app_pages
|-- __init__.py
|-- barcharts.py
|-- heatmaps.py
|-- histograms.py
- callbacks
|-- all_callbacks.py
|-- callbacks.py
- navigation_bar
|-- nav_bar.py
app.py
from dash import Dash, dcc, html, Input, Output, callback
import dash_bootstrap_components as dbc
from app_scripts.pages import barcharts, heatmaps, histograms
from app_scripts.callbacks.all_callbacks import all_callbacks
from app_scripts.navigation_bar.nav_bar import create_navbar
app = Dash(__name__,
external_stylesheets=[dbc.themes.BOOTSTRAP]
)
# Create navigation bar.
navbar = create_navbar()
# App contents.
url_bar_and_content_div = html.Div([
navbar,
dcc.Location(id='url', refresh=False),
html.Div(id='page-content')
])
# Index layout.
app.layout = url_bar_and_content_div
# Validate complete layout.
app.validation_layout = html.Div([
url_bar_and_content_div,
barcharts.layout,
heatmaps.layout,
histograms.layout
])
# Index callbacks
@callback(Output('page-content', 'children'),
Input('url', 'pathname'))
def display_page(pathname):
if pathname == "/":
return barcharts.layout
elif pathname == "/heatmaps":
return heatmaps.layout
elif pathname == "/histograms":
return histograms.layout
# Callbacks.
all_callbacks(app)
if __name__ == '__main__':
app.run(debug=True)
barcharts.py
from dash import dcc, html
import plotly.express as px
df = px.data.tips()
days = df.day.unique()
layout = html.Div(
[
dcc.Dropdown(
id="dropdown",
options=[{"label": x, "value": x} for x in days],
value=days[0],
clearable=False,
),
dcc.Graph(id="bar-chart"),
]
)
heatmaps.py
from dash import dcc, html
import plotly.express as px
df = px.data.medals_wide(indexed=True)
layout = html.Div(
[
html.P("Medals included:"),
dcc.Checklist(
id="heatmaps-medals",
options=[{"label": x, "value": x} for x in df.columns],
value=df.columns.tolist(),
),
dcc.Graph(id="heatmaps-graph"),
]
)
histograms.py
from dash import dcc, html
layout = html.Div(
[
dcc.Graph(id="histograms-graph"),
html.P("Mean:"),
dcc.Slider(
id="histograms-mean", min=-3, max=3, value=0, marks={-3: "-3", 3: "3"}
),
html.P("Standard Deviation:"),
dcc.Slider(id="histograms-std", min=1, max=3, value=1, marks={1: "1", 3: "3"})
]
)
all_callbacks.py
from app_scripts.callbacks.callbacks import callbacks_function
def all_callbacks(app):
"""
Wrapper for individual callbacks
"""
callbacks_function()
callbacks.py
from dash import Input, Output, callback
import plotly.express as px
import numpy as np
df_1 = px.data.tips()
df_2 = px.data.medals_wide(indexed=True)
def callbacks_function():
# Page 1 callback
@callback(Output("bar-chart", "figure"), Input("dropdown", "value"))
def update_bar_chart(day):
mask = df_1["day"] == day
fig = px.bar(df_1[mask], x="sex", y="total_bill", color="smoker", barmode="group")
return fig
# Page 2 callback
@callback(Output("heatmaps-graph", "figure"), Input("heatmaps-medals", "value"))
def filter_heatmap(cols):
fig = px.imshow(df_2[cols])
return fig
# Page 3 callback
@callback(
Output("histograms-graph", "figure"),
Input("histograms-mean", "value"),
Input("histograms-std", "value"),
)
def display_color(mean, std):
data = np.random.normal(mean, std, size=500)
fig = px.histogram(data, nbins=30, range_x=[-10, 10])
return fig
nav_bar.py
import dash_bootstrap_components as dbc
def create_navbar():
return(
dbc.NavbarSimple(
children=[
#### Barchart page.
dbc.Button(dbc.NavLink(
'Barcharts',
href = '/',
class_name = 'nav-link',
active = 'exact'),
class_name = 'btn btn-info me-md-3 justify-content-md-end'),
#### Heatmap page.
dbc.Button(dbc.NavLink(
'Heatmaps',
href = '/heatmaps',
class_name = 'nav-link',
active = 'exact'),
class_name = 'btn btn-info me-md-3 justify-content-md-end'),
#### Histogram page.
dbc.Button(dbc.NavLink(
'histograms',
href = '/histograms',
class_name = 'nav-link',
active = 'exact'),
class_name = 'btn btn-info me-md-3 justify-content-md-end'),
],
brand = 'Data analysis app',
color = '#939597',
dark = True,
fluid = True)
)
This structure of MWE is effectively the same as my app, except that I have a more elaborate layout scheme for each page. I have a data upload page keyed to a dash-uploader button and callback. Upon upload of zipped files, the callback chain kicks in, with plots eventually rendered on the visualisation page in sub-tabs. The dataframes in code above, are place-holders for what are uploaded files and store objects, that in turn are inputs for my actual callbacks. My callbacks follow the same structure as in MWE, some outputs are referenced in data upload page but not in visualisation page and vice versa.
Here is my current app.py that uses routing and validation in extactly the same way as MWE:
# =============================================================================
#### Import packages into environment.
import dash_uploader as du
import dash_bootstrap_components as dbc
import dash
from dash import html, Input, Output, callback, dcc
# =============================================================================
#### Import functions.
from app_scripts.navigation_bar.nav_bar import create_navbar
from app_scripts.callbacks.all_callbacks import all_callbacks
from app_scripts.app_data_store.software_1_data_store import data_store_1
from app_scripts.app_pages import home, data_upload, visualisation, about
# =============================================================================
#### Initialise the application dashboard.
app = dash.Dash(__name__,
suppress_callback_exceptions = True,
prevent_initial_callbacks = True,
external_stylesheets = dbc.themes.BOOTSTRAP)
# --------------------------------------------------------------------------- #
#### Configure uploader.
du.configure_upload(app,
r'C:\tmp\data_uploads',
use_upload_id = True)
# --------------------------------------------------------------------------- #
#### Generate navigation bar.
nav_bar = create_navbar()
# --------------------------------------------------------------------------- #
#### Generate layout.
app_contents = html.Div(
style = {'background': '#f0f0f0'},
children = [
data_store_1(),
nav_bar,
dcc.Location(id='url', refresh=False),
html.Div(id='page-content')
]
)
#### Index layout.
app.layout = app_contents
#### Validate complete layout.
app.validation_layout = html.Div([
app_contents,
home.home_page_layout,
data_upload.upload_page_layout,
visualisation.visualisation_page_layout,
about.about_page_layout
])
#### Index router callbacks.
@callback(Output('page-content', 'children'),
Input('url', 'pathname'))
def display_page(pathname):
if pathname == "/":
return home.home_page_layout
elif pathname == "/data_upload":
return data_upload.upload_page_layout,
elif pathname == "/visualisation":
return visualisation.visualisation_page_layout,
elif pathname == "/about":
return about.about_page_layout
# --------------------------------------------------------------------------- #
#### Specify callbacks.
all_callbacks(app)
# --------------------------------------------------------------------------- #
if __name__ == '__main__':
app.run_server(debug = True,
dev_tools_hot_reload = False)
# --------------------------------------------------------------------------- #
Question:
1 - Why doesn’t MWE structure generate this error: A nonexistent object was used in an
Output of a Dash callback
, when the output ID’s in callbacks are only referenced in specific page layouts?
If I comment out the app.validation_layout
lines, then there are some errors, but referencing callback inputs. Passing suppress_callback_exceptions=True
to the Dash instance, suppresses these errors.
However, suppressing exceptions doesn’t work in my actual app.
2 - Does the app.validation_layout
actually help with output errors?
Thanks for taking the time and apologies for the long post. I’ve spent a couple of weeks working on this and I’m loathe to give up. Hopefully one of you can save me from this rabbit hole haha!