9

I'm trying to make an app in the Python Dash framework which lets a user select a name from a list and use that name to populate two other input fields. There are six places where a user can select a name from (the same) list, and so a total of 12 callbacks that need to be performed. My question is, how can I use a single function definition to supply multiple callbacks?

As I've seen other places (here for example), people reuse the same function name when doing multiple callbacks, e.g.

@app.callback(
    Output('rp-mon1-health', 'value'),
    [Input('rp-mon1-name', 'value')]
)
def update_health(monster):
    if monster != '':
        relevant = [m for m in monster_data if m['name'] == monster]
        return relevant[0]['health']
    else:
        return 11

@app.callback(
    Output('rp-mon3-health', 'value'),
    [Input('rp-mon3-name', 'value')]
)
def update_health(monster):
    if monster != '':
        relevant = [m for m in monster_data if m['name'] == monster]
        return relevant[0]['health']
    else:
        return 11

@app.callback(
    Output('rp-mon1-health', 'value'),
    [Input('rp-mon1-name', 'value')]
)
def update_health(monster):
    if monster != '':
        relevant = [m for m in monster_data if m['name'] == monster]
        return relevant[0]['health']
    else:
        return 11

This is a ton of identical repetition and is bad if there's a fix I need to implement later. Ideally I'd be able to do something like:

@app.callback(
    Output('rp-mon1-health', 'value'),
    [Input('rp-mon1-name', 'value')]
)
@app.callback(
    Output('rp-mon2-health', 'value'),
    [Input('rp-mon2-name', 'value')]
)
@app.callback(
    Output('rp-mon3-health', 'value'),
    [Input('rp-mon3-name', 'value')]
)
def update_health(monster):
    if monster != '':
        relevant = [m for m in monster_data if m['name'] == monster]
        return relevant[0]['health']
    else:
        return 11

However, the above ends up no call back on the first two, only on the last. My code as is, is below.

import json

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

monster_data = json.loads('''[{
    "name": "Ares Mothership",
    "health": 14,
    "transition": 2
  },{
    "name": "Cthugrosh",
    "health": 7,
    "transition": 3
  }]''')
monster_names = [{'label': m['name'], 'value': m['name']} for m in monster_data]
monster_names.append({'label': 'None', 'value': ''})

app = dash.Dash(__name__)


def gen_monster(player, i):
    name = 'Monster #%d:  ' % i
    id_gen = '%s-mon%d' % (player, i)
    output = html.Div([
        html.Label('%s Name   ' % name),
        html.Br(),
        dcc.Dropdown(
            options=monster_names,
            value='',
            id='%s-name' % id_gen
        ),
        html.Br(),
        html.Label('Health'),
        html.Br(),
        dcc.Input(value=11, type='number', id='%s-health' % id_gen),
        html.Br(),
        html.Label('Hyper Transition'),
        html.Br(),
        dcc.Input(value=6, type='number', id='%s-state' % id_gen),
    ], style={'border': 'dotted 1px black'})
    return output


app.layout = html.Div(children=[
    html.H1(children='Monsterpocalypse Streaming Stats Manager'),

    html.Div([
        html.Div([
            html.Label('Left Player Name: '),
            dcc.Input(value='Mark', type='text', id='lp-name'),
            gen_monster('lp', 1),
            html.Br(),
            gen_monster('lp', 2),
            html.Br(),
            gen_monster('lp', 3)
        ], style={'width': '300px'}),

        html.Br(),

        html.Div([
            html.Label('Right Player Name: '),
            dcc.Input(value='Benjamin', type='text'),
            gen_monster('rp', 1),
            html.Br(),
            gen_monster('rp', 2),
            html.Br(),
            gen_monster('rp', 3)
        ], style={'width': '300px'})
    ], style={'columnCount': 2}),

    html.Div(id='dummy1'),
    html.Div(id='dummy2')
])

@app.callback(
    Output('rp-mon1-health', 'value'),
    [Input('rp-mon1-name', 'value')]
)
def update_health(monster):
    if monster != '':
        relevant = [m for m in monster_data if m['name'] == monster]
        return relevant[0]['health']
    else:
        return 11


@app.callback(
    Output('rp-mon1-state', 'value'),
    [Input('rp-mon1-name', 'value')]
)
def update_health(monster):
    if monster != '':
        relevant = [m for m in monster_data if m['name'] == monster]
        return relevant[0]['transition']
    else:
        return 6


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

3 Answers 3

9

You could do something like this:

def update_health(monster):
    if monster != '':
        relevant = [m for m in monster_data if m['name'] == monster]
        return relevant[0]['health']
    else:
        return 11


@app.callback(
    Output('rp-mon1-health', 'value'),
    [Input('rp-mon1-name', 'value')]
)
def monster_1_callback(*args, **kwargs):
    return update_health(*args, **kwargs)

@app.callback(
    Output('rp-mon2-health', 'value'),
    [Input('rp-mon2-name', 'value')]
)
def monster_2_callback(*args, **kwargs):
    return update_health(*args, **kwargs)


@app.callback(
    Output('rp-mon3-health', 'value'),
    [Input('rp-mon3-name', 'value')]
)
def monster_3_callback(*args, **kwargs):
    return update_health(*args, **kwargs)

Now the function that contains the logic is only written once, and the other functions are simple passthroughs that you shouldn't ever need to update.

Sign up to request clarification or add additional context in comments.

Comments

8

I had the exact same issue. Loads of callbacks that differed only with the Input and Output ids. The following worked for me (I'll provide an example from my code, but the idea is the same)

def rangeslider_tocalendar(output, input):
    @app.callback([Output(output, 'start_date'),
                   Output(output, 'end_date')],
                  [Input(input, 'value')])
    def repeated_callback(range_slider):
        cal_start = datetime.date.fromordinal(range_slider[0])
        cal_end = datetime.date.fromordinal(range_slider[1])
        return cal_start, cal_end

rangeslider_tocalendar('date-range', 'range-slider')

I wrapped the repeating callbacks in a function rangeslider_tocalendar(). Then I just called the wrapper function and pass in the input and output ids. Kept spaghetti off my plate.

Comments

3

A logically equivalent approach, but witch less repeated code, would be to assign the callbacks in a loop,

def update_health(monster):
    if not monster:
        return 11
    relevant = [m for m in monster_data if m['name'] == monster]
    return relevant[0]['health']

for i in range(1, 13):
    app.callback(Output(f'rp-mon{i}-health', 'value'),
                 [Input(f'rp-mon{i}-name', 'value')])(update_health)

A more canonical option would be to use the pattern-matching callback feature of Dash,

@app.callback(Output(dict(id=MATCH, type='rp-mon-health'), 'value'),
              [Input(dict(id=MATCH, type='rp-mon-name'), 'value')])
def monster_callback(monster):
    if not monster:
        return 11
    relevant = [m for m in monster_data if m['name'] == monster]
    return relevant[0]['health']

Besides a clear and compact syntax, this approach has the advantage of scaling to a dynamic number of components. That is, if you add/remove input/output component pairs at runtime (say, you wanted to add another option due to some other user selections), the pattern-matching approach will still work. The fixed callback assignment(s) obviously won't.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.