Search code examples
pythoncallbackhrefplotly-dash

Dash app - Dynamically Create a Layout for Multi-Page App Validation [Error: A nonexistent object was used in an `State` of a Dash callback.]


I have a Dash application where I'm trying to load a few additional input cells which depend on the input of an earlier cell

I.e.

if input (query-input-1-state) = MOVE --> additional set0 of inputs load (input-X-state,input-Y-state)
if input (query-input-1-state) = PARABOLIC --> additional set1 of inputs load for further submission (input-XX-state,input-YY-state)

The code is this - the layouts file (I have followed the dash multipage app template) file contains the main layout and the sub-layouts for additional inputs (layout_query_parabolic and layout_query_move).

layouts.py
layout_menu = html.Div([
    dcc.Link('Run Query', href='/apps/query'),html.Br(),
    dcc.Link('Optimise', href='/apps/optimise'),html.Br(),
])

#Set 0 of input boxes
layout_query_move = html.Div([
                        dcc.Input(id='input-X-state', type='number', value=2),html.Br(),
                        dcc.Input(id='input-Y-state', type='number', value=3),html.Br(),
                        ])

#Set 1 of input boxes
layout_query_parabolic = html.Div([
                        dcc.Input(id='input-XX-state', type='number', value=6),html.Br(),
                        dcc.Input(id='input-YY-state', type='number', value=7),html.Br(),
                        dcc.Input(id='input-Z-state', type='number', value=8),html.Br(),
                        ])

layout_query_menu = html.Div([
    dcc.Link('Go to Main', href='/apps/'),html.Br(),
    html.H3('Enter settings for Move'),
    dbc.Label("Ticker:        ", size="md"),dcc.Input(id='query-input-0-state', type='text', value='QQQ'),
    dbc.Label("Event:         ", size="md"),dcc.Input(id='query-input-1-state', type='text', value='MOVE'),
    html.Div(id='full-input-boxes'),
    html.Button(id='submit-button-state2', n_clicks=0, children='Show all inputs'),
    html.Button(id='submit-button-state', n_clicks=0, children='Go!'),
    dcc.Graph(id='graph-with-slider'),
])

The callbacks file declares 2 callbacks - one for the main page - and the other to show additional inputs which depends on the input into query-input-1-state.

callbacks.py

#Validation layout to 'declare' all the input values
app.validation_layout = html.Div([ 
    layout_query_move,
    layout_query_parabolic,
    layout_menu,
    layout_query_menu,
    layout_optimise,
    dcc.Input(id='input-X-state', type='number', value=2), #set 0 of inputs
    dcc.Input(id='input-Y-state', type='number', value=3), #set 0 of inputs

    dcc.Input(id='input-XX-state', type='number', value=6), #set 1 of inputs
    dcc.Input(id='input-YY-state', type='number', value=7), #set 1 of inputs
    dcc.Input(id='input-Z-state',  type='number', value=8), #set 1 of inputs
])

flask.has_request_context() == False

@app.callback(
    Output('graph-with-slider', 'figure'),
    Input('submit-button-state', 'n_clicks'),
    State('query-input-0-state', 'value'),
    State('query-input-1-state', 'value'),
    State('input-X-state', 'value'),
    State('input-Y-state', 'value'),
    State('input-XX-state', 'value'),
    State('input-YY-state', 'value'),
    State('input-Z-state', 'value'),
             )
def display_value0(n_clicks,v0,v1, v2,v3,v4,v5,v6):
    d = {'x': [v2, v2], 'y': [v2, v2]}
    df = pd.DataFrame(data=d)
    filtered_df = df
    fig = px.scatter(filtered_df, x="x", y="y")
    fig.update_layout(transition_duration=500)
    return fig


@app.callback(
    Output('full-input-boxes', 'children'),
    Input('submit-button-state2', 'n_clicks'),
    State('query-input-1-state', 'value'),
)
def ask_for_more_inputs(n_clicks,event_id): #,asset_str,event_str
    if not n_clicks: raise dash.exceptions.PreventUpdate
    if event_id == 'MOVE': return layout_query_move
    if event_id == 'PARABOLIC': return layout_query_parabolic

The app entry page is this (declares the app layout):

index.py #App Entry page

app.layout = html.Div([
    dcc.Location(id='url', refresh=False),
    html.Div(id='page-content'),
])

@app.callback(
        Output('page-content', 'children'),
        Input('url', 'pathname')
             )
def display_page(pathname):
    if pathname == '/apps/':
         return layout_menu
    elif pathname == '/apps/query':
         return layout_query_menu
    elif pathname == '/apps/optimise':
         return layout_optimise
    elif pathname == '/apps/move':
         return layout_query_move

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

I however get an nonexistent object error with:

A nonexistent object was used in an `State` of a Dash callback. The id of this object is 
`input-X-state` and the property is `value`. 

I have declared the validation layout and have tried turning off the errors through going directly into flask under the hood, however these two measures don't seem to have helped. I have also tried moving around the declaration for the validation but that doesn't make any difference. The callback which takes input-X-state doesn't recognise both sets of inputs (X,Y) and (XX,YY,Z) and the chart never updates.

How can I fix this?


Solution

  • OK, so I am assuming you are referring to the following Dash docs for URL Routing and Multiple Apps; and have (as I personally would too) decided to go with the second option for their recommended file structure layouts which they refer to as 'flat', and looks like this:

    .
    |-- assets
    |   `-- custom.css
    |-- app.py
    |-- callbacks.py
    |-- index.py
    `-- layout.py
    
    1 directory, 5 files
    

    where it is the index.py file which is the actual "entry" to the app file (i.e., the file which controls dynamically the URL and thus which 'layout' (or essentially, app) is being displayed, via a callback function utilizing the dcc.Location object built specifically for this purpose (multi-URL Dash apps)).

    I'll be honest it's not entirely clear to me what exactly you are trying to do, but, that may not be necessary. It looks pretty complicated/involved/esoteric. BUT, what I've done is re-arranged your files so that they run without error, and hopefully this will help guide you on the right track.

    The reason you were getting that error, is because you were loading a layout which did not have the mentioned Inputs declared.

    Side note: I am not positive what you even want here (/need) is a multi-app/URL Dash app, or, that this is the best approach (again because I'm not 100% on what your overall program is intended to solve) but, there is more than one way to skin a cat, as they say. This approach could work out perfectly, for your given needs; especially if you understand what it is I've changed and how this Dash paradigm with altering the page based on the URL works.

    Feel free to lmk if any questions further, but..

    Here's the contents of the files:

    1. app.py

    import dash
    
    app = dash.Dash(__name__, suppress_callback_exceptions=True)
    server = app.server
    
    1. layouts.py

    from dash import dcc
    from dash import html
    
    import dash_bootstrap_components as dbc
    
    layout_menu = html.Div(
        [
            dcc.Link("Run Query", href="/apps/query"),
            html.Br(),
            dcc.Link("Optimise", href="/apps/optimise"),
            html.Br(),
        ]
    )
    
    # Set 0 of input boxes
    layout_query_move = html.Div(
        [
            dcc.Input(id="input-X-state", type="number", value=2),
            html.Br(),
            dcc.Input(id="input-Y-state", type="number", value=3),
            html.Br(),
        ]
    )
    
    # Set 1 of input boxes
    layout_query_parabolic = html.Div(
        [
            dcc.Input(id="input-XX-state", type="number", value=6),
            html.Br(),
            dcc.Input(id="input-YY-state", type="number", value=7),
            html.Br(),
            dcc.Input(id="input-Z-state", type="number", value=8),
            html.Br(),
        ]
    )
    
    layout_query_menu = html.Div(
        [
            dcc.Link("Go to Main", href="/apps/"),
            html.Br(),
            html.H3("Enter settings for Move"),
            dbc.Label("Ticker:        ", size="md"),
            dcc.Input(id="query-input-0-state", type="text", value="QQQ"),
            dbc.Label("Event:         ", size="md"),
            dcc.Input(id="query-input-1-state", type="text", value="MOVE"),
            html.Div(id="full-input-boxes"),
            html.Button(
                id="submit-button-state2", n_clicks=0, children="Show all inputs"
            ),
            html.Button(id="submit-button-state", n_clicks=0, children="Go!"),
            dcc.Graph(id="graph-with-slider"),
            dcc.Input(
                id="input-X-state", type="number", value=2
            ),  # set 0 of inputs
            dcc.Input(
                id="input-Y-state", type="number", value=3
            ),  # set 0 of inputs
            dcc.Input(
                id="input-XX-state", type="number", value=6
            ),  # set 1 of inputs
            dcc.Input(
                id="input-YY-state", type="number", value=7
            ),  # set 1 of inputs
            dcc.Input(
                id="input-Z-state", type="number", value=8
            ),  # set 1 of inputs
        ]
    )
    
    # Validation layout to 'declare' all the input values
    layout_optimise = html.Div(
        [
            dcc.Input(
                id="input-X-state", type="number", value=2
            ),  # set 0 of inputs
            dcc.Input(
                id="input-Y-state", type="number", value=3
            ),  # set 0 of inputs
            dcc.Input(
                id="input-XX-state", type="number", value=6
            ),  # set 1 of inputs
            dcc.Input(
                id="input-YY-state", type="number", value=7
            ),  # set 1 of inputs
            dcc.Input(
                id="input-Z-state", type="number", value=8
            ),  # set 1 of inputs
        ]
    )
    
    
    1. callbacks.py

    import dash
    import layouts
    import pandas as pd
    import plotly.express as px
    
    from app import app
    from dash import dcc
    from dash import html
    from dash.dependencies import Input
    from dash.dependencies import Output
    from dash.dependencies import State
    
    
    @app.callback(
        Output("graph-with-slider", "figure"),
        Input("submit-button-state", "n_clicks"),
        State("query-input-0-state", "value"),
        State("query-input-1-state", "value"),
        State("input-X-state", "value"),
        State("input-Y-state", "value"),
        State("input-XX-state", "value"),
        State("input-YY-state", "value"),
        State("input-Z-state", "value"),
    )
    def display_value0(n_clicks, v0, v1, v2, v3, v4, v5, v6):
        d = {"x": [v2, v2], "y": [v2, v2]}
        df = pd.DataFrame(data=d)
        filtered_df = df
        fig = px.scatter(filtered_df, x="x", y="y")
        fig.update_layout(transition_duration=500)
        return fig
    
    
    @app.callback(
        Output("full-input-boxes", "children"),
        Input("submit-button-state2", "n_clicks"),
        State("query-input-1-state", "value"),
    )
    def ask_for_more_inputs(n_clicks, event_id):  # ,asset_str,event_str
        if not n_clicks:
            raise dash.exceptions.PreventUpdate
        if event_id == "MOVE":
            return layouts.layout_query_move
        if event_id == "PARABOLIC":
            return layouts.layout_query_parabolic
    
    
    1. index.py

    from dash import dcc
    from dash import html
    from dash.dependencies import Input, Output
    
    from app import app
    from layouts import (
        layout_menu,
        layout_query_menu,
        layout_optimise,
        layout_query_move,
    )
    import callbacks
    
    app.layout = html.Div(
        [dcc.Location(id="url", refresh=False), html.Div(id="page-content")]
    )
    
    
    @app.callback(Output("page-content", "children"), Input("url", "pathname"))
    def display_page(pathname):
        if pathname == "/apps/":
            return layout_menu
        elif pathname == "/apps/query":
            return layout_query_menu
        elif pathname == "/apps/optimise":
            return layout_menu
        elif pathname == "/apps/move":
            return layout_query_move
        else:
            return "404"
    
    
    if __name__ == "__main__":
        app.run_server(debug=True, dev_tools_hot_reload=True)
    
    

    Obviously you're going to need to finish/fix them to do what it is you want, exactly. One thing to notice, for example: if you're going to reference a layout in the callbacks file, it needs to be imported (i.e., import layouts → then refer to it as layouts.__name_of_layout__, or, import it directly from layouts import _____). And for any single callback that may be triggered, if/when it is triggered, all of its inputs (including states) must all be present in whatever the current "layout" being served is! Hope that makes sense. 🙂

    EDIT: Alas, newer version of Dash are no longer hindered by the requirement just bolded and italicized thanks to a new dash.Dash() app attribute "validation_layout", which allows you to essentially assign all of your pages/layouts to the "validation_layout" (thus containing all components ids you might use) and not have to worry about callbacks being tied down to specific layouts