Reputation: 4537
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)
Upvotes: 9
Views: 5297
Reputation: 6014
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.
Upvotes: 3
Reputation: 177
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.
Upvotes: 8
Reputation: 6586
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.
Upvotes: 9