Dash: Determining Which Input Has Been Fired

Created on 13 Jul 2018  路  15Comments  路  Source: plotly/dash

We need a way to determine which input has changed. We've provided a temporary hack with n_clicks_timestamp but we need something that is more general.

I'm not worried about the implementation, it fits into the dash-renderer architecture nicely.

What's not as clear to me is what the app.callback decorated functions should look like. How does this interface scale when we want to add more things like:

  • Previous state
  • Multiple outputs
  • Keyword-style arguments instead of list-style arguments
  • Which input changed
  • Timestamps when inputs changed
  • Nested properties (e.g. subscribing to figure.layout.title)
  • Error handling (e.g. updating an error div container when an exception is thrown)

So, let's use this thread to just propose different interfaces to the callbacks. Of course, doing this in a way that would be backwards compatible is preferred.


cc @plotly/dash

Most helpful comment

Got a prototype working...

import dash
import dash_html_components as html

from dash.dependencies import Output, Input
from dash.exceptions import PreventUpdate

app = dash.Dash(__name__)

BUTTONS = ['btn-{}'.format(x) for x in range(1, 6)]

app.layout = html.Div([
    html.Div([
        html.Button(x, id=x) for x in BUTTONS
    ]),
    html.Div(id='output'),
])


@app.callback(Output('output', 'children'),
              [Input(x, 'n_clicks') for x in BUTTONS])
def on_click(*args):
    if not dash.callback.triggered:
        raise PreventUpdate
    trigger = dash.callback.triggered[0]
    input_value = dash.callback.inputs.get(trigger)
    return 'Just clicked {} for the {} time!'.format(trigger, input_value)


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

All 15 comments

Just spitballing here, not actually sure how much I'm into this. One approach could be to use a request object that is passed into the callback functions, as is common with many web frameworks, and then retrieve the component registrations from the request using element_id.prop_name identifiers. The request object could track which element fired and the individual component objects could track things like has_changed and prev_value. eg

app.layout = html.Div([
    html.Div(id='target'),
    dcc.Input(id='my-input', type='text', value=''),
    html.Button(id='submit1', n_clicks=0, children='Submit 1')
    html.Button(id='submit2', n_clicks=0, children='Submit 2')
])

@app.callback([Output('target', 'children')],
              [Input('submit', 'n_clicks'), Input('submit2', 'n_clicks')],
              [State('my-input', 'value')])
def callback(request):
    my_input = request.state['my-input.value']

    if my_input.has_changed:
        result = f"Input {request.trigger.id} triggered callback; {my_input.id} changed value from {my_input.prev_value} to {my_input.value}"
    else:
        result = f"Input {request.trigger.id} triggered callback; {my_input.id} did not change value."

    return result

This also solves the problem of managing unwieldy lists of Input/State that you need to align with the callback function arguments, as I describe as being an issue in #159.

However this would likely mean either a non-backwards compatible change to callback function signatures, or we have two callback functions, the previous simple list of argument values alongside the the new request-based one.

However this would likely mean either a non-backwards compatible change to callback function signatures, or we have two callback functions, the previous simple list of argument values alongside the the new request-based one.

One option at our disposal is checking the number or even the type of arguments in the def my_callback function from our decorator and then passing a new set of arguments through. We could also check the types and number of arguments passed into our app.callback function.

That is, roughly:

def callback(output, inputs, states):
    def scoped_wrapper(func):
        def wrapper(*args, **kwargs):
            if len(args) == 1 and (len(inputs) + len(states) > 1)
                # e.g. callback signature type 1
                request = {
                    'inputs': inputs,
                    'states': states
                }
                func(request)
            else:
                # e.g. existing callback signature
                return func(*(inputs + states))

In your example, we'd some way to differentiate between a callback with a single input and a callback with a single input that uses the request object

Why not make 2 options available for the user to choose from?

With

@app.callback(output, inputs, states, as_request=True)

assume signature request where as_request=False is default. This way if there are more arguments with as_request=True exception could be raised to inform the user that he mistakenly used wrong signature.

It may be also good idea to allow configuring Dash object with default Dash(..., callbacks_with_request=False) but allowing to set as_request=app.callbacks_with_request allowing user to define his choice upfront.

One option at our disposal is checking the number or even the type of arguments in the def my_callback function from our decorator and then passing a new set of arguments through. We could also check the types and number of arguments passed into our app.callback function.

Ah, good point @chriddyp. Polymorphism through decorators! If we went down the path of a request-like context object, this could well be a good approach to supporting it alongside the original callback signature.

Why not make 2 options available for the user to choose from?

Note that in general, I'm looking for solutions that are unified and ideally backwards compatible. As in the zen of python, there should be one way to do things.

You could add another optional parameter to the callback decorator: PreviousState() which would feed the previous state to the function, not sure what that would look like, but once you have previous state then you can work out which one or many things have changed

Got a prototype working...

import dash
import dash_html_components as html

from dash.dependencies import Output, Input
from dash.exceptions import PreventUpdate

app = dash.Dash(__name__)

BUTTONS = ['btn-{}'.format(x) for x in range(1, 6)]

app.layout = html.Div([
    html.Div([
        html.Button(x, id=x) for x in BUTTONS
    ]),
    html.Div(id='output'),
])


@app.callback(Output('output', 'children'),
              [Input(x, 'n_clicks') for x in BUTTONS])
def on_click(*args):
    if not dash.callback.triggered:
        raise PreventUpdate
    trigger = dash.callback.triggered[0]
    input_value = dash.callback.inputs.get(trigger)
    return 'Just clicked {} for the {} time!'.format(trigger, input_value)


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

Hi @T4rk1n,
Look like a very good example. I have tried it but i got this error message: line 22, in on_click
if not dash.callback.triggered:
AttributeError: module 'dash' has no attribute 'callback'.
Do you know how to fix it? I am using version '0.37.0' of dash.

@hoangmt this solution has not been published to PyPI yet, it's a WIP at the two pull requests linked just above.

@alexcjohnson my bad. Can't wait to see how it works. :D

You mean the change from dash.callback to dash.callback_context? Yes, the example here is out of date. But GitHub issues and PR comments are not documentation, they鈥檙e a working conversation, so I wouldn鈥檛 want to be going back and sanitizing them after the fact.

This was helpful! Looks like it's been added to the "FAQs" here: https://dash.plot.ly/faqs

It seems to be missing from the FAQs now. However, I do find this information here (https://dash.plotly.com/advanced-callbacks) under "Determining which Input has fired with dash.callback_context"

Was this page helpful?
0 / 5 - 0 ratings

Related issues

germayneng picture germayneng  路  4Comments

rpkyle picture rpkyle  路  3Comments

T4rk1n picture T4rk1n  路  3Comments

AngusCui picture AngusCui  路  3Comments

mpkuse picture mpkuse  路  4Comments