Search code examples
pythoncallbackplotlydata-analysisplotly-dash

Adding callbacks (interactive inputs) to page container in plotly dash multi pages app


Context:

I am working on an app with multi pages feature in Plotly Dash that uses this schema:
|app.py
| pages|
| games_*dashboard.py
|
|channels_dashboard.py
|
| *videos_dashboard.py

The project is talking about youtube gaming analysis (videos, channels, comments) data and each file of the pages folder file contains layout in a layout variable and some callbacks that are also in a variable here's some example code:

 layout = html.Div(children= [

    html.Center(children= html.H1(children= ["Analysis for specfic", html.Span(
        " Games ", style= {"color": THEME_COLORS[1]}), "Videos"])),

    dcc.Graph(id= "video_stats_per_game",
              figure= video_stats_per_game.update_layout(width= 1240, height= 500),
              style= {'position': 'absolute', 'border': f'2px solid {THEME_COLORS[0]}','left': '10px',
                      'display': 'inline-block'}),

    html.Div(["Choose a game:",
        dcc.Dropdown(GAMES, "Minecraft" ,id= "game_dropdown")],
                     style= {'top': '600px', 'position': 'absolute', 'display': 'inline-block',
                            'width': '1230px'}), #Bla bla bla 

and here's the callback varible for the same file:

callback_1 = [Output('duration_vs_view', 'figure'), # this `duration_vs_view` is a plot name.
             [Input('game_dropdown', 'value')]]

def duration_vs_view(value): # Some unreadable code ...
 


callback_2 = [Output('stats_growth', 'figure'),
              [Input('game_dropdown', 'value')]]

def stats_growth(value): # bla bla bla


callbacks = callback_1 + callback_2 + callback_3

The problem:

Now what I want to do is implement the callbacks into my app.py page_container and here's some code from app.py :

# --------------------Creating the main app----------------------
app = Dash(__name__, use_pages=True,
           external_stylesheets= EXERNAL_STYLESHEETS)

app.layout = html.Div([
    html.Center(html.H1([html.Span("Youtube",
                                  style= {"color": THEME_COLORS[1]}), ' gaming analysis'])),
    
    html.Button('Channels dashboard', id='channels_dashboard_button'),
    
    html.Button('Games dashboard', id='games_dashboard_button'),
    
    html.Center(html.Button('Videos dashboard', id='videos_dashboard_button',)),
    
    html.Div(id='page-content', children=dash.page_container)])
# ---------------------------------------------------------------


@app.callback(Output('page-content', 'children'),
              [Input('channels_dashboard_button', 'n_clicks'),
               Input('games_dashboard_button', 'n_clicks'),
               Input('videos_dashboard_button', 'n_clicks')])

def display_page(channels_clicks, games_clicks, videos_clicks):
    
    if channels_clicks:
        return (pages.channels_dashboard.layout,
               pages.channels_dashboard.callbacks)
    
    
    elif games_clicks:
        return (pages.games_dashboard.layout,
               pages.games_dashboard.callbacks) # ...

    else:
            return ''

The Error:

The plotly dash app returns a callback error when I click on any button that leads me to a dashbord with callbcks and here's the error message:

dash.exceptions.InvalidCallbackReturnValue: The callback for `<Output `page-content.children`>`
                returned a value having type `tuple`
                which is not JSON serializable.


The value in question is either the only value returned,
or is in the top level of the returned list,

Expectation:

I expected that I will get the dashboard just as fine as I was running them without the muli pages functionality (I used before the normal call back way for single variables)

At last but not least:

please if you can help me by telling me a totally another way please do that like to use links to each dashboard or something and if you have any idea about how can I fix this please also do that if the problem needs more code or screenshots to put also tell me,Thanks in advance


Solution

  • As mentioned in the comments, the InvalidCallbackReturnValue exception you got is because return statements like this:

    return (pages.channels_dashboard.layout, pages.channels_dashboard.callbacks)
    

    For your Output: Output('page-content', 'children') a valid return value could be:

    return pages.channels_dashboard.layout
    

    But you can't setup the page callbacks this way.


    I think your display_page callback, the passing of layout and callback variables are all unnecessary. I think it's working against the dash pages feature.

    You can implement the callbacks in the pages (.py files) themselves.

    Here's an example of a simple page with a callback:

    # File: pages/page1.py
    
    from dash import html, callback, Input, Output, register_page
    
    register_page(__name__)
    
    layout = html.Div([
        html.Button(id="page1-input", children="Click me"),
        html.Div(id="page1-output")
    ])
    
    
    @callback(
        Output("page1-output", "children"),
        Input("page1-input", "n_clicks")
    )
    def update_city_selected(n_clicks):
        return n_clicks
    

    Instead of using inputs like Input('channels_dashboard_button', 'n_clicks') to go to a certain page you can just use links, no callback required:

    dcc.Link("Link", href="/page1")
    

    If you need the link to look/act like a button you can wrap a button in a link.

    Example app.py:

    from dash import Dash, html, dcc
    import dash
    
    app = Dash(__name__, use_pages=True)
    
    app.layout = html.Div([
        html.Div(
            [
                html.Div(dcc.Link(f"{page['name']} - {page['path']}", href=page["relative_path"]))
                for page in dash.page_registry.values()
            ]
        ),
        dash.page_container
    ])
    
    if __name__ == '__main__':
        app.run_server(debug=True)