Dash multi-page app failing to detect callback output IDs in different page layouts

249 Views Asked by At

I'm trying to reconfigure my multi-tab dashboard into a multi-page app but have trouble with callback output errors.

Here is how I modularise to keep track of components of the app:

Folder structure

- app.py 
- assets
- data_processing_scripts
- app_scripts
    - app_pages       
       |-- __init__.py
       |-- home.py
       |-- data_upload.py
       |-- visualisation.py
       |-- about.py
    - app_tabs
        - data_upload_tabs
           |-- software_1_tables_upload.py
        - data_visualisation_tabs
           |-- software_1_data_visualisation.py
    - app_data_store       
       |-- data_store_software_1.py
    - callbacks
       |-- all_callbacks.py
       |-- software_1_data_callbacks.py
    - navigation_bar
       |-- nav_bar.py

I use the dash-uploader library to upload multiple tables as a zipped file in the data_upload page. This is where the initial callback keyed to the dash-uploader event, creates a series of outputs that serve as inputs for other callbacks dealing with data processing and eventually plot generation. These plots and tables should then be served to the app for display in various sub-tabs of the visualisation page.

The pages are just html.Div() wrappers that define the overal layout of the page. I import functions from modules in the app_tabs folder that return the meat of the layout for i.e. the upload or visualisation pages. These layouts reference various callback output IDs.

I followed the same logic for the callbacks. all_callbacks.py imports callbacks from software_1_data_callbacks.py. The pages and all_callbacks are imported into app.py which instantiates the app.

I use various dcc.Store objects that pass various tables as inputs for callbacks. Here I wrap them in a function , data_store_software_1.py but rather than placing them in a hidden div on the data_upload layout, I import them into app.py.

In this way I reference all necessary parts of the app in app.py. This modularisation works well with a single page app with tabs and nested sub-tabs.

Right, if you haven't already switched off after that intro, here is how I set-up app.py:

# =============================================================================

import dash_uploader as du
import dash_bootstrap_components as dbc
import dash
from dash import html

# =============================================================================

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.maxquant_data_store import data_store_software_1

# =============================================================================

#### Define app.
app = dash.Dash(__name__, 
                suppress_callback_exceptions = True,
                prevent_initial_callbacks = True,
                pages_folder = './app_scripts/app_pages',
                use_pages = True,
                external_stylesheets = dbc.themes.BOOTSTRAP)

#### Define server.
server = app.server

# --------------------------------------------------------------------------- #

#### 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.layout = html.Div(
    style = {'background': '#f0f0f0'},
    children = [
        data_store_software_1(),
        nav_bar, 
        dash.page_container
        ]
    )

# --------------------------------------------------------------------------- #

#### Specify callbacks.
all_callbacks(app)

# --------------------------------------------------------------------------- #

#### Instantiate app.

if __name__ == '__main__':
    app.run_server(debug = True, 
                   dev_tools_hot_reload = False)
    
# --------------------------------------------------------------------------- #

What works:

The app renders exactly as I expect. Pages route properly and layouts of individual pages and sub-tabs all display without error.

What doesn't work:

Callback output ID errors:

A nonexistent object was used in an `Output` of a Dash ...

Depending on the page I navigate to, the errors reference callback output IDs in the layout of another page and vice versa. I can see from the stack trace which ID's are in the layout of the page I click:

The string ids in the current layout are: [list of ID's]

Bad solution

I have to call data_store_software_1 in both data_upload and visualisation page layouts. In this case I also have to include dcc.Store(id = 'some_callback_out') objects for every callback output ID including plots (various plotly figures, plot pngs, pandas styled tables). The net effect is that the plots render very slowly in the visualisation page to the point of being unusable and at times raise browser errors due to reaching memory limits. Additionally, clicking away and back to the visualisation page, starts the plot rendering process again...slowly. In my standard 1 page approach the only things in dcc.Store() are just tables and string variables.

What I tried

1 - Minimal example from here - https://github.com/AnnMarieW/dash-multi-page-app-demos/tree/main/multi_page_store

I used this approach by passing the data_store_software_1 straight to the app layout in app.py. This leads to the aformentioned errors.

2 - Tried validation - https://dash.plotly.com/urls#dynamically-create-a-layout-for-multi-page-app-validation

In this case the layout in app.py looks like this:

app_contents = html.Div(
    style = {'background': '#f0f0f0'},
    children = [
        nav_bar, 
        dash.page_container]
    )

app.layout = app_contents

app.validation_layout = html.Div(children = [
    data_store_software_1(),
    app_contents]
    )

The error here is still:

A nonexistent object was used in an `Output` of a Dash ...

Except that it's only the very first callback output ID of the dash_uploader. If I then add this ID to data_store_software_1() it just specifies another ID etc, so again back to square 1.

3 - Probably the closest example of the problem I'm having:

https://community.plotly.com/t/a-nonexistent-object-was-used-in-an-output-of-a-dash-callback/60897/18

Here I couldn't work-out how I could split my callbacks as suggested. The use of stores for only specific layouts, also led to various output ID errors.

Question

Clearly I can't see the woods for the trees or perhaps it can't be done as I envisage.

How can I get round this issue?

Many thanks for your time and sorry for the long winded explanation. I also wanted to generate a minimally working example that replicates this error, using my overall approach. In the end I thought a descriptive approach might be better, as I didn't know how to re-create a self-contained dash-uploader instance in this context.

0

There are 0 best solutions below