Sorting callback is extremely slow

A have a dash app with a container div that stores many DBC buttons. The buttons can be clicked to change their color. There is a final button outside of this container div that allows a user to sort the buttons within the container div. The output of the callback associated with this button is the children property of the container div. The problem is that this in turn triggers all callbacks related to the buttons, because for some reason the n_clicks parameter of these buttons is observed as input by dash. These button callbacks are not slow at all. Additionally, the PreventUpdate exceptions work in that the remainder of the callback is not triggered. However, for some reason, dash still sends out empty post requests (status 204) for all buttons to update their color. These requests make sorting callback extremely slow when the number of buttons is large.

Minimal working example is posted below. It is not problematic in this example because the number of buttons is small. However, the actual example has hundreds of buttons which makes it extremely slow (>5 minutes) and thus unusable.

import dash
from dash import html
from dash.exceptions import PreventUpdate
from dash.dependencies import Input, Output, State
import dash_bootstrap_components as dbc

app = dash.Dash(__name__)

app.layout = html.Div([
    dbc.Button('Sort', id='sort-btn'),
    html.Div([
        dbc.ButtonGroup([dbc.Button('Button 1-0', id='btn-1-0'), dbc.Button('Button 1-1', id='btn-1-1')], id='btn-grp-1'),
        dbc.ButtonGroup([dbc.Button('Button 0-0', id='btn-0-0'), dbc.Button('Button 0-1', id='btn-0-1')], id='btn-grp-0'),
    ], id='main')
])

for group in range(2):
    for index in range(2):
        @app.callback(
            Output(f'btn-{group}-{index}', 'color'),
            Input(f'btn-{group}-{index}', 'n_clicks'),
        )
        def change_color(n_clicks: int):
            if not n_clicks:
                raise PreventUpdate
            
            return 'warning'

@app.callback(
    Output('main', 'children'),
    Input('sort-btn', 'n_clicks'),
    State('main', 'children'),
)
def sort_button_groups(n_clicks: int, current_children):
    if not n_clicks:
        raise PreventUpdate
    
    output_list = []
    for i in range(2):
        btn_group = [child for child in current_children if child.get('props', {}).get('id') == f'btn-grp-{i}'][0]
        output_list.append(btn_group)
    return output_list

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

Try adding a prevent_initial_call = True under your callbacks:

for group in range(2):
    for index in range(2):
        @app.callback(
            Output(f'btn-{group}-{index}', 'color'),
            Input(f'btn-{group}-{index}', 'n_clicks'),
            prevent_initial_call = True
        )
        def change_color(n_clicks: int):
            print("warning2")
            if not n_clicks:
                raise PreventUpdate
            return 'warning'

Hello @rob_cl,

Welcome to the community!

Yes, this will trigger the callbacks as they are loaded, and it actually an issue one faces any time you dynamically add buttons into the children of a div.

Trying to use prevent_initial_call only stops it from triggering when the dash layout is loading, not when new elements are added.

Thank you for your reply!
If I understand correctly, there is no way to prevent this from happening. However, I do find it strange that this resorting is extremely slow while the initial page load is not slow at all. Why is it slower when dynamically generating components in a callback?

Each callback is being triggered is my guess.

Why are you trying to sort the buttons?

Maybe there is a different way to perform this task.

I am developing a production planning application. Each button represents a production order that can be released to the shop floor. The user can click the buttons to select them.
The sorting allows users to sort by planned start date or planned finished date, depending on the release logic they want to use.

Is it possible to use something like AG grid for this instead?

You have a lot more flexibility and you can have buttons inside the grid that trigger things. Or just selection boxes too.

2 Likes

I am completely new to dash, so I have not stumbled upon this library before. I will check it out and post here if I found a suitable solution. Thanks a lot for your help!

1 Like

For anyone interested: my usecase can indeed by solved easily using AG Grid. Thank you very much @jinnyzor for pointing this out!

Instead of creating button groups for each production order, I simply used a data grid with production order information. The users can (de-)select any production order they want by simply clicking on the corresponding row in the grid. Any additional logic that should be executed when a user selects a row can be implemented in a callback (I used multiple row selection on click, see Multiple Row Selection On Click | Dash for Python Documentation | Plotly). The sorting is extremely fast as no buttons are triggered. An additional benefit is that sorting is already integrated in AG Grid, so I do not have to write custom callbacks for those operations anymore.

3 Likes