✊🏿 Black Lives Matter. Please consider donating to Black Girls Code today.
🧬 Learn how to build RNA-Seq data apps with Python & Dash. Register for the May 20 Webinar!

Having trouble with pattern matching callbacks in Dash

Hey all, I am having some trouble with pattern matching callbacks in Dash. First, let me explain what I am trying to do, and then I’ll show you some code.

I am creating a simple dashboard for a research study. On the left side of the page should be a list of study participants. When you click on the participant, it will expand a list of dates that the participant did some activities. Then, when you click on a date, it will expand and show the activities that the participant did on that day. This expanding list is already functional. No help is needed on the expanding list.

When the user clicks on an activity, I want to show some content related to that activity - typically data, plots, etc. Here is a hand-drawn image showing my example layout:

As you can see, when the user clicks on a button/link in the left-hand expanding menu, I want to show some content on the right-hand side. When I generate the expanding menu, I give each button a unique id. That code looks something like this:

this_activity_layout = html.Div(
    dbc.Button(f"{activity.activity_name}", id={
        "type" : "activity_button",
        "participant_id" : activity.participant_id,
        "unique_button_id" : str(uuid.uuid4())
    }, color="link"),
    style={'text-indent':'6em'}
)

The final layout of the app comes together looking something like this:

app.layout = html.Div(
    [
        dbc.Row(
            [
                dbc.Col(
                    #All the layout stuff for the left-hand menu is in here.
                ),
                dbc.Col(html.Div(id="container")) #This empty column is the right-hand side of the page
            ]
        )
    ]
)

Then, I want to listen for those button clicks and update the right-hand side of the page accordingly. For now, just for the sake of simplicity, all I want to do is display some text. (Of course I want to do something more than that in the future, but I am having trouble just getting text to display on the right hand side at the moment).

Here is my callback function:

@app.callback(
    Output("container", "children"),
    Input({"type": "activity_button"}, "n_clicks")
)
def on_button_click(n):
    ctx = dash.callback_context

    if not ctx.triggered:
        button_id = 'No clicks yet'
    else:
        button_id = ctx.triggered[0]['prop_id'].split('.')[0]
    
    return html.Div(button_id)

As you can probably guess, nothing is happening when the buttons are clicked. No text is displayed on the right hand side of the page.

Can y’all help me figure out where I am going wrong? Thanks!

You forgot to add the other keys of the activity button ids in the input of the callback, they should be added with ALL as value for all buttons to trigger the callback.

https://dash.plotly.com/pattern-matching-callbacks

I had those in there originally and it still didn’t work. I have added them back in, and it still doesn’t work. Here is the changed section of code:

@app.callback(
    Output("container", "children"),
    Input({"type": "activity_button", "participant_id" : ALL, "unique_button_id" : ALL}, "n_clicks")
)

Have you tried without the “participant_id” key? I haven’t use multiple ALL values before. I am not sure if it is supported or not.

I have tried reducing the number of keys:

this_activity_layout = html.Div(
    dbc.Button(f"{activity.activity_name}", id={
        "type" : "activity_button",
        "uuid" : str(uuid.uuid4())
    }, color="link"),
    style={'text-indent':'6em'}
)

And here is the callback:

@app.callback(
    Output("container", "children"),
    Input({"type": "activity_button", "uuid" : ALL}, "n_clicks")
)

But still nothing.

Also, for the sake of showing what else I have tried, I have tried abandoning the idea of pattern matching callbacks and using a list of Input objects, like so:

all_activity_inputs = []
for activity in all_activities:
    activity_uuid_str = str(uuid.uuid4())
    this_activity_layout = html.Div(
        dbc.Button(f"{activity.activity_name}", id=activity_uuid_str, color="link"),
        style={'text-indent':'6em'}
    )
    
    new_activity_input = Input(component_id=activity_uuid_str, component_property="n_clicks")
    all_activity_inputs.append(new_activity_input)

And then the callback looks like this:

@app.callback(
    Output("container", "children"),
    all_activity_inputs
)
def on_button_click(n):
    #code in here

But this also doesn’t work. In fact, when I try this it gives me an error:

When I edit that code to be like the following:

@app.callback(
    Output("container", "children"),
    all_activity_inputs
)
def on_button_click(*n):

It then technically works, but the time to execute the function is horrendous. Each time I click one of the buttons it takes about 3 to 5 seconds just for simple text to appear on the other side of the layout showing the ID of the button that was pressed.

Any ideas on getting the pattern matching callback to work? I can’t believe people haven’t done something like this before. It seems like a pretty common use-case!

Okay, I have gotten the pattern matching callback to work. It turns out it was an issue with the keyword “ALL”, which apparently I hadn’t imported properly. So now my code just looks like this:

@app.callback(
    Output("container", "children"),
    Input({"type": "activity_button", "uuid" : dash.dependencies.ALL}, "n_clicks")
)

While this technically works, the performance is actually even worse than the solution in my previous post that worked. After clicking a button, it takes up to 10-seconds for it to simply execute the callback and display some text on the other side of the page.

Is this the kind of performance I can expect from Dash? If so, I’m ready to throw in the towel and use a different tool to make this app. All of our codebase is in Python, which is why I wanted to choose Dash for this app, but this kind of performance is terrible. I’m open to suggestions.

The performance of a Dash app depends mainly on the performance on the underlying React components and Python code, apart from the overhead introduced by Dash itself. It can be limited by,

  • Not transferring too much data between server and client
  • Not having too many object on the page/in callbacks

In your case you have a lot of objects (buttons), so the performance you see is in line with my expectations. In general, I would recommend building components (which your tree-selection-thing seems to be) using React directly, and using Dash to compose these component into apps.

That being said, i guess there might be a simple fix for your issue. While i haven’t seen your code, i guess it would be possible to generate the buttons dynamically? Then there would be only ~ 6 inputs (as per you drawing, 6 games) for the callback instead of 1587, and i would expect the performance to be as you intend.