Callbacks clearing all (unconnected) core-components values


#1

I have been working on a menu where you can select a target audience using a number of drop-downs, checklists and sliders. Some of these components use callbacks to update the selectable values in other components, while others are independent from it all.

The problem however is that when a callback does trigger all previous made choices (selections in drop-downs and checklists) get cleared. It seems to be a bug since only using non-callback components it all works as intended and the values remain. Though I must admit I could always be the case of overlooking a very simple mistake on my end.

I left out some of the lists I was using to fill the selections trying to shorten the text for this post, but I can state the values are loaded correctly in the components, its jus that they get cleared when another component triggers a callback. Additionally the layout is returned to a simple page selector:
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 == '/':
        return home()
    elif pathname == '/test':
        return test()
    elif pathname == '/audience':
        return audience_selection()
    else:
        return '404'

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

Now, my apologies for the following lengthy, possibly unreadable, script:

import dash_core_components as dcc
import dash_html_components as html
from dash.dependencies import Input, State, Output
from app import app

app.config.supress_callback_exceptions = True
COLOURS = {'One':'#172A39', 'Two':'#EFEFEF'}

def audience_selection():
    """Target audience selection layout"""

    layout = html.Div([
        html.Div([
            html.H2('Create your target audience', className='eight columns', style={'color':COLOURS['Two']}),

            html.Button('Load', id='load_group', className='two columns',
                style={'backgroundColor':COLOURS['One'], 'color':COLOURS['Two'], 'margin-top':'10'}),

            html.Button('Save', id='save_group', className='two columns',
                style={'backgroundColor':COLOURS['One'], 'color':COLOURS['Two'], 'margin-top':'10'}),

            html.Div([], id='load_container', className='twelve columns'),
            html.Div(className='twelve columns', style={'margin-top':'8', 'border-bottom':'2px solid white'}),
        ], id='header', className='twelve columns', style={'height':'100%', 'margin-top':'0'}),

        html.Div([
            html.H4('- Device', className='twelve columns', style={'color':COLOURS['Two']}),

            html.Button('Desktop', id='desktop_input', className='four columns', n_clicks=0,
                style={'backgroundColor':COLOURS['One'], 'color':COLOURS['Two']}),

            html.Button('Tablet', id='tablet_input', className='four columns', n_clicks=0,
                style={'backgroundColor':COLOURS['One'], 'color':COLOURS['Two']}),

            html.Button('SmartPhone', id='phone_input', className='four columns', n_clicks=0,
                style={'backgroundColor':COLOURS['One'], 'color':COLOURS['Two']}),
        ], id='devices', className='twelve columns', style={'height':'100%', 'margin-top':'5'}),

        html.Div([
            html.H4('- Gender', className='twelve columns', style={'color':COLOURS['Two']}),

            dcc.Dropdown(id='gender_input',
                placeholder='Select gender(s)...',
                options=[
                    {'label':'Male', 'value':'m'},
                    {'label':'Female', 'value':'f'},
                    {'label':'Other', 'value':'o'}],
                multi=True, value=''
            ),
        ], id='gender', className='twelve columns', style={'height':'100%', 'margin-top':'5'}),

        html.Div([
             html.H4('- Location', className='twelve columns', style={'color':COLOURS['Two']}),

            dcc.Dropdown(id='location_input_provence',
                placeholder='Select provence(s)...',
                options=[
                    {'label':'Groningen', 'value':'gr'},
                    {'label':'Friesland', 'value':'fr'},
                    {'label':'Drente', 'value':'dr'}],
                multi=True, value=''
            ),

            dcc.Dropdown(id='location_input_town',
                placeholder='Select town(s)...',
                options=[
                     {'label':'One', 'value':'1'},
                     {'label':'Two', 'value':'2'},
                     {'label':'Three', 'value':'3'}],
                 multi=True, value=''
            ),
        ], id='location', className='twelve columns', style={'height':'100%', 'margin-top':'5'}),

        html.Div([
            html.H4('- Age', className='twelve columns', style={'color':COLOURS['Two']}),

            html.H5('Age ranging from: 18 - 80', id='age_output',
                className='twelve columns', style={'text-align': 'center', 'color':COLOURS['Two']}),

            dcc.RangeSlider(id='age_input', className='twelve columns',
                count=1, min=18, max=80, step=1, value=[24,32]),
        ], id='age', className='twelve columns', style={'height':'100%', 'margin-top':'5'}),

        html.Div([
            html.H4('- Products', className='twelve columns', style={'color':COLOURS['Two']}),

            dcc.Dropdown(id='product_input', className='twelve columns',
                placeholder='Select product(s)...',
                options=gen_options(products),
                multi=True, value=''
            ),

            html.Div([], id='product_output', className='twelve columns', style={'margin-top':'5'}),

        ], id='products', className='twelve columns', style={'height':'100%', 'margin-top':'5'}),

        html.Div([
            html.H4('- Platforms', className='twelve columns', style={'color':COLOURS['Two']}),

            html.Div([
                dcc.Checklist( id='device_input', labelStyle={'display':'inline-block', 'color':COLOURS['Two']},
                    options=gen_options(platforms), values=[],
                ),
            ], className='twelve columns', style={'color':COLOURS['Two']}),
        ], id='platforms', className='twelve columns', style={'height':'100%', 'margin-top':'5'}),

        html.Div([
            html.H4('- Interests', className='twelve columns', style={'color':COLOURS['Two']}),

            dcc.Dropdown(id='interests_input',
                placeholder='Select interest(s)...',
                options=gen_options(interests),
                multi=True, value=''
            ),
        ], id='interests', className='twelve columns', style={'height':'100%', 'margin-top':'5'}),

        html.Div([
            html.Div('[numberUsers] users part of this selection which is [percentageUsers]%, and have an average age of [AverageAge]', className='twelve columns', style={'color':COLOURS['Two']}),
        ], id='footer', className='twelve columns', style={'height':'100%', 'margin-top':'25'}),

    ], className='background', style={'backgroundColor':COLOURS['One'], 'padding':'5', 'width':'600', 'float':'right', 'overflow-y':'auto'})

    return layout

@app.callback(
    Output('load_container', 'children'),
    [Input('load_group', 'n_clicks')])
def load_audience(n_clicks):
    return html.Div([
            html.H5('Select your saved target audience', className='eleven columns',
                style={'color':COLOURS['Two'], 'float':'right'}),

            html.Div([
                dcc.Dropdown(id='loading_input', placeholder='Select which target audience to load...',
                options=[
                    {'label':'One', 'value':'1'},
                    {'label':'Two', 'value':'2'},
                    {'label':'Three', 'value':'3'}],
                value='1'),
            ], className='nine columns'),

            html.Button('Load', id='load_selected', className='two columns',
                style={'backgroundColor':COLOURS['One'], 'color':COLOURS['Two'], 'float':'right'}),
        ], className='twelve columns')

for name in ['desktop_input', 'tablet_input', 'phone_input']:
    @app.callback(
        Output('{}'.format(name), 'style'),
        [Input('{}'.format(name), 'n_clicks')])
    def update_device_input(n_clicks):
        if n_clicks % 2 == 0:
            return {'backgroundColor':COLOURS['One'], 'color':COLOURS['Two']}
        else:
            return {'backgroundColor':COLOURS['Two'], 'color':COLOURS['One']}

@app.callback(
    Output('location_input_town', 'options'),
    [Input('location_input_provence', 'value')])
def update_town_selection(selection):
    towns = []
    if len(selection) > 0:
        for select in selection:
            if select == "gr":
                for a in towns_in_gr:
                    towns.append({'label':a, 'value':a})
            elif select == "fr":
                for b in towns_in_fr:
                    towns.append({'label':b, 'value':b})
            elif select == "dr":
                for c in towns_in_dr:
                    towns.append({'label':c, 'value':c})
    else:
        for a in towns_in_gr:
            towns.append({'label':a, 'value':a})
        for b in towns_in_fr:
            towns.append({'label':b, 'value':b})
        for c in towns_in_dr:
            towns.append({'label':c, 'value':c})
    return towns

@app.callback(
    Output('age_output', 'children'),
    [Input('age_input', 'value')])
def update_age_feedback(values):
    return "Age ranging from: {} - {}".format(values[0], values[1])

@app.callback( 
    Output('product_output', 'children'),
    [Input('product_input', 'value')])
def update_subs_selection(selection):
    accounts = []
    if len(selection) < 1:
        return html.Div('Selecting one or more products will show the available subscriptions here',
            style={'color':COLOURS['Two']})

    for select in selection:
        for product in products:
            if select == product:
                if account_types[product] == 'subs_long':
                    for name in account_long:
                        accounts.append('{} {}'.format(select, name))
                elif account_types[product] == 'subs_short':
                    for name in accounts_short:
                        accounts.append('{} {}'.format(select, name))
                break

    return dcc.Checklist( id='account_output', labelStyle={'display':'inline-block', 'color':COLOURS['Two']},
            options=gen_options(accounts), values=[],
    )

def gen_platform_input():
    return html.Div([html.Button('{}'.format(name), id='{}'.format(name), n_clicks=0, 
            style={'backgroundColor':COLOURS['One'], 'color':COLOURS['Two'], 'borderColor':COLOURS['Two'],
            'margin-right':'5', 'display':'inline-block'}) 
        for name in platforms])
    
for name in platforms:
    @app.callback(
        Output('{}'.format(name), 'style'),
        [Input('{}'.format(name), 'n_clicks')])
    def update_plaform_input(n_clicks):
        if n_clicks % 2 == 0:
            return {'backgroundColor':COLOURS['One'], 'color':COLOURS['Two'], 'borderColor':COLOURS['Two'], 'margin-right':'5', 'display':'inline-block'}
        else:
            return {'backgroundColor':COLOURS['Two'], 'color':COLOURS['One'], 'borderColor':COLOURS['Two'], 'margin-right':'5', 'display':'inline-block'}

def gen_options(list_of_options):
    out = []
    for item in list_of_options:
        out.append({'label':'{}'.format(item), 'value':'{}'.format(item)})
    return out

Checkbox/radiobutton changing when I change slider values
#3

That is a bit odd isn’t it. I think you’re right, it looks like any dcc input components that aren’t involved in a callback are having their current contents reset. When another component that is registered with a callback updates. This has nothing to do with where the callbacks are targeting, it’s simply a matter of if the component is registered to a callback or not.

For example, if you change the gender dropdown (which is initially not involved in a callback) to be a State of some callback, then its contents is no longer reset.

@chriddyp is this expected? It certainly feels like buggy behaviour to me.


#4

Correct me if I am wrong, but triggering an update via callback seems to be done by sending a request to the server which then returns with the updated site. If that is the case the non-callback components do not seem to be included in the newly send site layout (resulting in default state = non selected).

A simple temporary solution would be to to add some zero size html elements to use as a callback output for all these components (return None or something), just so they are added to the list of callback components. Or perhaps there is a local/client side version of these callbacks, since all these non-callback components will not be using anything the server has to provide.

If their is however an actual solution I would love to know.


#5

Almost. A callback only returns the data needed to update the property of the element being targeted by the callback. you can see this clearly by opening the developer tools (F12 in Chrome and Firefox) and going to the network tab then causing a callback to trigger. You will see a new request that is made to the Dash server (likely _dash-update-component). Looking at the response tab for this request will show the JSON response.

So it’s not that the entire page is being returned and overwriting those components, or anything to do with the callback response I think. It seems like it’s something about the logic of the client side component system that is clearing the non-registered components. I’m not familiar enough with Dash’s clientside to know what’s going on.

Yeah, just adding these components as Inputs to a dummy callback that returns None or throws an exception (which means the callback will just abort) will work. Or adding them as States to existing callbacks and not doing anything with the values.

Although if the components aren’t associated with any callbacks, are they necessary for the interface?


#6

Ah, I see… sending the components does make more sense, and it turns out I have not been thinking it all through. The reason for having those components is so the user can create a selection and save it, which would mean adding it to a callback as state… I suppose the method of creating something small and testing it is not always beneficial.

Although the behaviour is still a bit weird, I should never cause issue since all components on the screen will be part of a callback.

Thanks for helping though


#7

No worries :slight_smile:

Yeah I agree that it it’s not overly helpful that behaviour. At least some kind of warning somewhere would be good. I might create an Issue about this on Dash’s github.


#8

I think this was never resolved?

I seem to just have spent 20 minutes debugging symptoms of this exact issue (still nothing in the error messages :s)

dash==0.23.1
dash-core-components==0.27.0rc2
dash-html-components==0.12.0rc2
dash-renderer==0.13.0

(not on latest since I’m waiting for https://github.com/plotly/dash/issues/334 to be merged and pushed!)