Input two or more button - How to tell which button is pressed?


#1

Imagine following code, there are two buttons bound to the same callback. The target is to identify which button is pressed in the callback. What is the best way?
The application scenario is like one button for page up, the other button for page down. Press button-1 the figure shows data that is newer while press button-2 the figure shows data that is older.

# -*- coding: utf-8 -*-
import dash
import dash_core_components as dcc
import dash_html_components as html
import pandas as pd
import plotly.graph_objs as go
from dash.dependencies import Input, Output, Event, State
app = dash.Dash()
def getLayout():
    return html.Div([
        html.H1(id='Title1',children='Multiple Button Test',style={'text-align':'center'}),
        html.Div(id='Title2', children='''
            Test multi button in a single page.
        ''',style={'margin-bottom':'50px','text-align':'center'}),
        html.Div(dcc.Markdown('''''',id='div1')),
        html.Button(id='btn1',children='button1'),
        html.Button(id='btn2',children='button2'),
    ])
app.layout = getLayout
@app.callback(Output('div1', 'children'),
              inputs=[Input('btn1', 'n_clicks'),
              Input('btn2', 'n_clicks')])
def update_div_1(n1,n2):
    print('$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$',n1,n2)
    #
    # How to tell which button is pressed in callback context
    #
    return '''button1 clicks *'''+str(n1)+'''* times\n'''+'''button2 clicks *'''+str(n2)+'''* times'''
if __name__ == '__main__':
    app.run_server(debug=True, port=10092, host='0.0.0.0')

#2

It’s currently not possible to tell which Input was fired. This may change in the future by introducing something like a PreviousState option in the app.callback.

For now, you’ll have to figure out another way to represent this UI. Perhaps a slider that represents time?


#3

Thanks @chriddyp
I’m considering your suggestion.


#4

I have a very similar problem… I have to graphs with some data and text area + img which are supposed to be updated based on hover data from the currently active graph. As it’s impossible to create two different callbacks with the same output I’m forced to implement something like this:
@app.callback(
Output(‘image1’, ‘src’),
[Input(‘plot1’, ‘hoverData’), Input(‘plot2’, ‘hoverData’)])
def display_product_image(hoverData, hoverData2):
print(hoverData)
print(hoverData2)
return "omg…"
As both hoverData and hoverData2 are filled I cannot say what’s the source of the update.

I think it’s not a matter of my bad ui design, it’s rather a limitation of Dash (for comparison it can be easily done in Shiny).
Any idea how to solve it?


#5

Thanks for posting @wolftorn! I think that I’m finally convinced that they are valid use-cases for this.

I’ve created an experimental branch in https://github.com/plotly/dash/pull/140 and https://github.com/plotly/dash-renderer/pull/25 that adds support for this by adding a PrevInput parameter in the callbacks.

I’ll keep those PRs updated with progress. It may take a while for this to get released as it is a substantial addition to Dash’s (minimal) API.


Which component triggered the callback
#6

@kfyao, you were very close to figuring this out! You can store the previous click state in a global variable (see warning below) and then compare the previous with the current state.

from collections import OrderedDict

import dash
import dash_core_components as dcc
import dash_html_components as html
import pandas as pd
import plotly.graph_objs as go
from dash.dependencies import Input, Output, Event, State
app = dash.Dash()

N_CLICKS = OrderedDict([('btn1',0), ('btn2',0)])

def getLayout():
    return html.Div([
        html.H1(id='Title1',children='Multiple Button Test',style={'text-align':'center'}),
        html.Div(id='Title2', children='''
            Test multi button in a single page.
        ''',style={'margin-bottom':'50px','text-align':'center'}),
        html.Div(dcc.Markdown('''''',id='div1')),
        html.Button(id='btn1',children='button1'),
        html.Button(id='btn2',children='button2'),
    ])
app.layout = getLayout
@app.callback(Output('div1', 'children'),
              inputs=[Input('btn1', 'n_clicks'),
              Input('btn2', 'n_clicks')])
def update_div_1(*new_clicks):
    global N_CLICKS
    btn_clicked = ''
    n_clicks_clicked = 0
    for (button, n_click_old), n_click_new in zip(N_CLICKS.items(), new_clicks):
        if n_click_new > n_click_old:
            btn_clicked = button
            n_clicks_clicked = n_click_new
    
    N_CLICKS[state_clicked] = n_clicks_clicked
    return btn_clicked 

if __name__ == '__main__':
    app.run_server(debug=True, port=10092, host='0.0.0.0')

This is just an example, for actual production implementation remember to share data between callbacks safely and DO NOT USE A GLOBAL VARIABLE More info


#7

Remember, using global variables is not safe. Do not mutate global variables. Your apps will break if multiple people are using them at the same time or if they are run on multiple workers.

If you or your company needs official support for these features (https://github.com/plotly/dash/pull/1405 and https://github.com/plotly/dash-renderer/pull/25) and can help sponsor the work, please reach out: https://plot.ly/products/consulting-and-oem/


#8

Just wondering if there is any progress on getting this implemented? I see that there has been no change on the PRs. This is the one feature that seems to be limiting my Dash apps at the moment ! Thanks


#9

There have not been any updates on this yet. You could create a custom build based off of those PRs. I can’t say for sure when we’ll have the time to wrap this up, however we can dedicate more resources to this feature if a company or organization is able to sponsor it.


#10

Tabs are basically this, they just look different and might have to be styled a lot…


#11

I’ve run in to the same problem. I have a dropdown which selects from a list of values. I would like to have “Previous” and “Next” buttons to make walking though the list easier.

What is the reason for only allowing one callback per output? Having multiple callback functions seems like the easiest solution.


#12

Allowing multiple callbacks to share the same output causes too much ambiguity (what happens when there are overlapping inputs?) and violates the zen of python rule “there should be one way to do things”.


#13

What if you could allow multiple callbacks per Output but not allow overlapping Inputs across these shared callbacks? I can’t think of any practical reason to have two callbacks with the same Output and the same Input(s)? However, there is a use case for having the State of an element in one callback and the Input in another callback, both with the same Output.


#14

I am too having lots of headache with this limitation. Example:
a table with actions on the table - copy, delete, subscribe, unsubscribe.

the simplest would be to register a callback for each button, perform necessary action and then output rows into table.

but you cant do it because after each button was clicked once you dont know which one was clicked anymore unless you create some really weird state control either via hidden divs (and more callbacks) or managing that state per use session (which is complicated, unreliable or requires some sort of additional services like redis or similar).

all is needed to specify a caller component (i.e. which input) triggered callback
OR
being able to setup multiple callbacks for the same output

or am i missing something?

any advice will be greatly appreciated!


#15

UPDATE
i decided that i am spending too much time on workaround and went to the source :slight_smile:
it does appear that adding anything that would indicate source of the change is a big change. so i opted out for introducing n_clicks_previous property in the Button component.

for this I modified the generator scripts at dash-html-components/scripts/generate-components.js with 3 lines of code:
add new property n_clicks_previous in the declaration,
set it to 0 in the default props
and set it to n_clicks after event gets fired in the onClick handler:

onClick={() => {
        if (props.setProps) props.setProps({n_clicks: props.n_clicks + 1});
        if (props.fireEvent) props.fireEvent({event: 'click'});
        if (props.setProps) props.setProps({n_clicks_previous: props.n_clicks + 1});
}}

then run rest of the instructions to build and install locally.
that did the trick and I can now determine which of the buttons clicked based on whether their previous value is the same as current. interestingly, i have to use n_clicks_previous = n_clicks + 1 due to some reason which i dont entirely comprehend. using n_clicks_previous = n_clicks gives difference of 2 for clicked button and 1 for those that are not clicked.

it works for me for now! :smile:


#16

This is clever way to make it work for buttons or things with n_clicks.

I’ll add my vote too that it would be nice to have incorporated into Dash – something easy like this for buttons, or if there is a general and whatever better other way, to know which of the multiple inputs have changed to cause a particular callback. I want to a parameter which is a list of which inputs changed, upon call to callback, so the callback can be aware of what reason or event its being called for exactly.

Right now have to use hidden divs and timestamps. Its kinda works ok but a little messy with all the extra code to keep track of and imprecise.


#17

Are you able to create a branch of dash_html_components and make a pull request? Maybe we can get this merged with the master branch for everyone to use. This adds a lot of functionality without changing the core dash library. Any thoughts @chriddyp ?


#18

Great idea @mikesmith1611 , I like this as an interim solution :+1:. I’d accept a PR that added this functionality.


#20

I can’t seem to get this to work. I’m new to npm, and javascript so probably doing something wrong. The button seems to have the property n_clicks_previous but it doesn’t update on click :frowning: any advice? Also nothing is changing in the lib/ folder when I run npm run install-local, I’m not sure that this is why I’m having issues??


#21

Mike,

change needs to be in 3 places of the generate-components.js script:

line 60:

    'n_clicks': PropTypes.integer,
    'n_clicks_previous': PropTypes.integer,

line 104:

                onClick={() => {
                    if (props.setProps) props.setProps({n_clicks: props.n_clicks + 1});
                    if (props.fireEvent) props.fireEvent({event: 'click'});
		    if (props.setProps) props.setProps({n_clicks_previous: props.n_clicks + 1});
                }}

line 124:

    n_clicks: 0,
    n_clicks_previous: 0

3rd one is actually not that important.

after that run:
npm install
see that is completes without errors. I would build it on linux box as some commands do not work on windows
this will create a dash_html_components-0.8.0-py2.7.egg in the /dash-html-components/dist folder
you will then need to install it locally using
python -m easy_install dash_html_components-0.8.0-py2.7.egg.

i can share the egg file if you want (i.e. it is already precompiled and all you need to do is just do the python easy_install step)

Because the change is done at the generate-components.js level, it means that all dash-html components that have n_click property will have n_clicks_previous. that includes links, html.H1/2/3/4/5/6 and many many more. comes very handy!