question-mark
Stuck on an issue?

Lightrun Answers was designed to reduce the constant googling that comes with debugging 3rd party libraries. It collects links to all the places you might be looking at while hunting down a tough bug.

And, if you’re still stuck at the end, we’re happy to hop on a call to see how we can help out.

determine which input is triggering in callback

See original GitHub issue

Since only a single Output is allowed in a callback, and all Inputs must feed into it, how can we determine which input is being triggered during a callback? I have three time-series line plots that I want to cross-filter, and update, any time a range is selected in any of the other plots. For example, the range of Plot A should update when either the range of Plot B or Plot C is updated. But the callback collects the ranges from all plots simultaneously. Is there a way to get the id of which input was triggered during a callback?

Here is my code but it only updates with data from plotB, when it is triggered. When I update Plot C the datac variable is indeed updated but the data from plotB is also passed in again, so the callback function has no knowledge of which plot is actually triggering new input.

@app.callback(
    Output('plotA', 'figure'),
    [Input('plotB', 'relayoutData'),
     Input('plotC', 'relayoutData')])
def display_selected_data(data, datac):

    print('data', data)
    print('datac', datac)

    startx = 'xaxis.range[0]' in data if data else None
    endx = 'xaxis.range[1]' in data if data else None

   # define the new xrange
    if startx and endx:
        xrange = [data['xaxis.range[0]'], data['xaxis.range[1]']]
    elif startx and not endx:
        xrange = [data['xaxis.range[0]'], thedates.max()]
    elif not startx and endx:
        xrange = [thedates.min(), data['xaxis.range[1]']]
    else:
        xrange = None

    traces = [go.Scatter(
        x=thedates,
        y=mdf['uniqvisits'])]
    return {
        'data': traces,
        'layout': get_layout('Unique Visits', 'Date', 'Unique Visits', xrange=xrange)
    }  

Issue Analytics

  • State:closed
  • Created 6 years ago
  • Reactions:7
  • Comments:13 (2 by maintainers)

github_iconTop GitHub Comments

7reactions
chriddypcommented, Jul 5, 2017

Great question @havok2063 !

Since only a single Output is allowed in a callback, and all Inputs must feed into it, how can we determine which input is being triggered during a callback?

This isn’t possible in Dash.

One core concept with Dash is that the app is described entirely by its current state and not by the order of events. This concept makes its easier to reason about the app’s logic and it forces the user interface to be consistent with exactly what you see on the screen (and not by the steps that were taken to get there).

I think that in most cases, the Dash app developer shouldn’t need to know the order of events. If they do, it usually means that the component needs to be patched in a way to make it more stateful or the UI needs to be redesigned. I’ll leave this issue open to invite examples of good UIs that are impossible to create without supporting “order-of-events”.


I have three time-series line plots that I want to cross-filter, and update, any time a range is selected in any of the other plots.

In this case, our app logic shouldn’t depend on the most recent action, it should depend on the current state of the other graphs. For example, if Graph A depends on Graph B and Graph C, the data should be filtered by the intersect of the regions selected or zoomed in B and C, not just the most recently zoomed.


Here is how I recommend doing crossfiltering for now. A few things to note: 1 - We’re using selectedData instead of zoom data with relayoutData so that the viewer can see the whole picture and can easily edit their selection. 2 - We’re styling the selected graph’s points themselves to better indicate which points were selected. 3 - We’re displaying the most recently “selectedData” by drawing a rectangle shape (documentation on shapes). This requires a recent version of dash-core-components: pip install dash-core-components==0.5.3. 4 - To keep the code DRY 🌴 we’re generating the callback functions and calling the decorators directly. For more on generating functions and generators in Python, I recommend this SO answer/essay on decorators. 5 - You’re free to customize the style of the selected points or of the non-selected points. In this case, we’re dimming the non-selected points by setting opacity=0.1. 6 - The numbers displayed on top of the points are just the IDs of the points to help you better understand how filtering is working. In practice, we would probably hide these or display a more descriptive ID.

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

import numpy as np
import pandas as pd

app = dash.Dash()

df = pd.DataFrame({
    'Column {}'.format(i): np.random.rand(50) + i*10
for i in range(6)})

app.layout = html.Div([
    html.Div(dcc.Graph(id='g1', selectedData={'points': [], 'range': None}), className="four columns"),
    html.Div(dcc.Graph(id='g2', selectedData={'points': [], 'range': None}), className="four columns"),
    html.Div(dcc.Graph(id='g3', selectedData={'points': [], 'range': None}), className="four columns"),
], className="row")

def highlight(x, y):
    def callback(*selectedDatas):

        index = df.index;
        for i, hover_data in enumerate(selectedDatas):
            selected_index = [
                p['customdata'] for p in selectedDatas[i]['points']
                if p['curveNumber'] == 0 # the first trace that includes all the data
            ]
            if len(selected_index) > 0:
                index = np.intersect1d(index, selected_index)

        dff = df.iloc[index, :]

        color = 'rgb(125, 58, 235)'

        trace_template = {
            'marker': {
                'color': color,
                'size': 12,
                'line': {'width': 0.5, 'color': 'white'}
            }
        }
        figure = {
            'data': [
                dict({
                    'x': df[x], 'y': df[y], 'text': df.index, 'customdata': df.index,
                    'mode': 'markers', 'opacity': 0.1
                }, **trace_template),
                dict({
                    'x': dff[x], 'y': dff[y], 'text': dff.index,
                    'mode': 'markers+text', 'textposition': 'top',
                }, **trace_template),
            ],
            'layout': {
                'margin': {'l': 20, 'r': 0, 'b': 20, 't': 5},
                'dragmode': 'select',
                'hovermode': 'closest',
                'showlegend': False
            }
        }

        shape = {
            'type': 'rect',
            'line': {
                'width': 1,
                'dash': 'dot',
                'color': 'darkgrey'
            }
        }
        if selectedDatas[0]['range']:
            figure['layout']['shapes'] = [dict({
                'x0': selectedDatas[0]['range']['x'][0],
                'x1': selectedDatas[0]['range']['x'][1],
                'y0': selectedDatas[0]['range']['y'][0],
                'y1': selectedDatas[0]['range']['y'][1]
            }, **shape)]
        else:
            figure['layout']['shapes'] = [dict({
                'type': 'rect',
                'x0': np.min(df[x]),
                'x1': np.max(df[x]),
                'y0': np.min(df[y]),
                'y1': np.max(df[y])
            }, **shape)]

        return figure

    return callback

app.css.append_css({"external_url": "https://codepen.io/chriddyp/pen/bWLwgP.css"})

app.callback(
    Output('g1', 'figure'),
    [Input('g1', 'selectedData'), Input('g2', 'selectedData'), Input('g3', 'selectedData')]
)(highlight('Column 0', 'Column 1'))

app.callback(
    Output('g2', 'figure'),
    [Input('g2', 'selectedData'), Input('g1', 'selectedData'), Input('g3', 'selectedData')]
)(highlight('Column 2', 'Column 3'))

app.callback(
    Output('g3', 'figure'),
    [Input('g3', 'selectedData'), Input('g1', 'selectedData'), Input('g2', 'selectedData')]
)(highlight('Column 4', 'Column 5'))

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

crossfiltering-recipe

1reaction
alishobeiricommented, Jul 5, 2017

@havok2063 States are arguments passed to the function of the callback that won’t trigger the callback itself. So changing the code you provided:

@app.callback(
    Output('plotA', 'figure'),
    [Input('plotB', 'relayoutData')],
    [State('plotC', 'relayoutData')])
def display_selected_data(data, datac):

In the above code only changes on the relayoutData of plotB would trigger the callback, changes to the layout of plotC would not trigger the callback itself. plotC’s relayoutData will be passed into def display_selected_data as datac and can be accessed when the callback itself is triggered but layout changes on plotC will not directly trigger the callback.

In terms of what you’re trying to do, it might be bit tough to figure out directly which input triggered the callback, but one thing you could do is to have two hidden html.P element that you tie to separate callbacks and when either plotB changes or plotC changes you set the value of the html.P element associated with either one to a known value - for example, you could set the element to hold the value of 1 if the plot has changed and a 0 otherwise. You could then pass the paragraph elements as inputs:

@app.callback(
    Output('plotA', 'figure'),
    [Input('pHolderPlotB', 'children')
     Input('pHolderPlotC', 'children'],
    [State('plotB', 'relayoutData'), State('plotC', 'relayoutData')])
def display_selected_data(bTrigger, cTrigger, data, datac):
    if(int(bTrigger) == 1):
       # do this 
   elif(int(cTrigger) == 1):
       # do that

And to reset the values of pHolderPlotB and pHolderPlotC after you have redrawn plotA you could create a third callback that will reset the values of both pHolderPlotB and pHolderPlotC back to 0 when plotA has changed.

There might be easier ways to do this but this is one that came to mind.

Let me know if my answer was hard to follow or if I could clarify something.

Read more comments on GitHub >

github_iconTop Results From Across the Web

Know which Input triggered a callback? - Dash Python
Hello guys, I'm wondering if would be possible to know which input triggered a callback which listens to different elements (i.e. button ...
Read more >
Plotly-Dash: How to determine trigger input in client-side ...
Dash's documentation describes how to determine which input triggered a callback in the case of server-side callbacks: Advanced Callbacks.
Read more >
Create Callbacks for Apps Created Programmatically
These input arguments are often named src and event . The first argument is the UI component that triggered the callback. The second...
Read more >
Long Callbacks in Dash Web Apps - Towards Data Science
dash.callback_context , which is useful for identifying the component which triggered the callback in case there is more than one input, is not ......
Read more >
Actions | Input System | 1.3.0 - Unity - Manual
A named Action that triggers callbacks in response to input. Actions use InputBinding to refer to the inputs they collect. For more information ......
Read more >

github_iconTop Related Medium Post

No results found

github_iconTop Related StackOverflow Question

No results found

github_iconTroubleshoot Live Code

Lightrun enables developers to add logs, metrics and snapshots to live code - no restarts or redeploys required.
Start Free

github_iconTop Related Reddit Thread

No results found

github_iconTop Related Hackernoon Post

No results found

github_iconTop Related Tweet

No results found

github_iconTop Related Dev.to Post

No results found

github_iconTop Related Hashnode Post

No results found