Callback does not trigger in a multi-page Dash App

Hello Dash users,

Trying to build a multi-page app based on the Vanguard report style (https://github.com/plotly/dash-vanguard-report/blob/master/app.py). It looks very suitable to my needs! However, in the example each page is static and is completely declared in the separate layouts. In my case, I need to build graphs which are dynamically populated by callbacks. Everything works fine except one problem: when I’m in a single subpage, which for example looks like this:

overview = html.Div([  # page 1

    html.Div([

        # Header
        get_logo(),
        get_header(),
        html.Br([]),
        get_input(),
        get_result_bar(),
        get_menu(),

        # Row 2

        html.Div([
            html.Div([
                dcc.Graph(id='sentiment-overview'),
                # html.Div(id='topic-content')
                # html.Div(DataTable(rows=[{}]), style={'display': 'none'})
            ], className='twelve columns padded')
        ],
            className='row ')

    ], className="subpage")

], className="page")

and the get_menu() looks like this:

def get_menu():
    menu = html.Div([

        dcc.Link('Overview   ', href='/overview', className="tab first"),

        dcc.Link('Sentiment timeline   ', href='/timeline', className="tab"),

        dcc.Link('Sentiment dynamic   ', href='/dynamic', className="tab")

    ], className="row ")
    return menu

the following callback needs to be fired to be able to build a graph from a intermediate hidden div with data:

@app.callback(
    dash.dependencies.Output('sentiment-overview', 'figure'),
    [dash.dependencies.Input('url', 'pathname'), Input('intermediate-value-df', 'children')])
def sentiment_overview(pathname, jsonified_data):
    print("Pathname in overview graph:", pathname)
    if (pathname == '/overview' or pathname == "/") and jsonified_data:
        figure = {
            'data': [
                go.Bar(x=positive['Day'], y=positive['count'], name='positive',
                       marker=dict(color='#1ABC9C')),
                go.Bar(x=negative['Day'], y=negative['count'], name='negative',
                       marker=dict(color='#E74C3C'))
            ],
            'layout': go.Layout(
                title='Number of pos/neg tweets per day',
                showlegend=True
            )
        }
        return figure

In the second page, which looks similarly:

sentimentTimeline = html.Div([  # page 2

    html.Div([

        # Header
        get_logo(),
        get_header(),
        html.Br([]),
        get_input(),
        get_result_bar(),
        get_menu(),

        # Row 2

        html.Div([

            html.Div([
                dcc.Graph(id='sentiment-timeline'),

            ], className="twelve columns padded")

        ], className="row "),

    ], className="subpage")

], className="page")

the needed callback is placed right after the first one:

@app.callback(
    dash.dependencies.Output('sentiment-timeline', 'figure'),
    [dash.dependencies.Input('url', 'pathname'),
     Input('intermediate-value-df', 'children')])
def sentiment_timeline(pathname, jsonified_data):
    print("Pathname in timeline graph:", pathname)
    if (pathname == '/timeline') and jsonified_data:
        sentiment_df = pd.read_json(jsonified_data)
        # print(sentiment_df.head(1))
        positive = sentiment_df.loc[sentiment_df['DateValue'] > 0.05]
        negative = sentiment_df.loc[sentiment_df['DateValue'] < -0.05]
        figure = {'data': [
            go.Scatter(
                x=positive.Date,
                y=signal.savgol_filter(positive.DateValue, 21, 3),
                customdata=positive.Tweet,
                name="Positive tweets",
                line=dict(color='#1ABC9C'),
                opacity=0.8),

            go.Scatter(
                x=negative.Date,
                y=signal.savgol_filter(negative.DateValue, 21, 3),
                customdata=negative.Tweet,
                name="Negative tweets",
                line=dict(color='#E74C3C'),
                opacity=0.8)],

            'layout': go.Layout(
                title='Sentiment over time: 1 - positive, -1 - negative',
                xaxis=dict(
                    rangeselector=dict(
                        buttons=list([
                            dict(count=1,
                                 label='1d',
                                 step='day',
                                 stepmode='todate'),
                            dict(count=1,
                                 label='1h',
                                 step='hour',
                                 stepmode='backward'),
                            dict(count=6,
                                 label='6h',
                                 step='hour',
                                 stepmode='backward')
                        ])
                    ),
                    rangeslider=dict(
                        visible=True
                    ),
                    type='date'
                )
            )}
        return figure

These two callbacks seem to block each other in some way because whenever I am in the first page, I need to click the menu item twice to be able to properly see the loaded graph. The same goes for the second page.

Also note that with the debug print statements seem to show that when I’m on page 2, the first callback gets triggered until I click on the link for the second page for the second time, only then the right callback gets triggered.

My app Layout is:

app.layout = html.Div([
    dcc.Location(id='url'),
    html.Div(id='handle-value', style={'display': 'none'}),
    html.Div(id='intermediate-value', style={'display': 'none'}),
    html.Div(id='intermediate-value-df', style={'display': 'none'}),
    html.Div(id='page-content')
])

and the display function is:

# Update page
@app.callback(dash.dependencies.Output('page-content', 'children'),
              [dash.dependencies.Input('url', 'pathname'), dash.dependencies.Input('intermediate-value', 'children')])
def display_page(pathname, value):
    if (pathname == '/overview' or pathname == "/") and value:
        print("in the overview display page")
        return overview
    elif pathname == '/timeline' and value:
        print("In the timeline display page")
        return sentimentTimeline
    else:
        return empty_template

I attached a small gif to demonstrate the problem (don’t mind the messages in between or bad graph, it’s just for debug :wink: ). It’s a long one so I am happy to show more of the code if necessary!

Thanks a lot in advance!!
dash|600x400

3 Likes

Hi,

When transfering data with hidden Div’s, make sure you’re callbacks never return None, but always have a proper return function, returning dummies and placeholders like [{}] instead of None. Especially when you have conditionals in your callback functions, this can easily be overlooked.

Otherwise callbacks don’t work for some reason. (took me a while to trace down a bug related to this last week)

I ran into a similar problem when building a multi-tab Dash App (so this may be related to your experience). The static app rendered just fine but none of the callbacks were operational (or just extremely slow).