Multiple callbacks for an output

From version 0.0.47, a new MultiplexerTransform is included in dash-extensions. It makes it possible to target an output by multiple callbacks (which is otherwise problematic in Dash) with nearly zero code changes,

import dash_html_components as html
from dash_extensions.enrich import Output, DashProxy, Input, MultiplexerTransform

app = DashProxy(prevent_initial_callbacks=True, transforms=[MultiplexerTransform()])
app.layout = html.Div([html.Button("left", id="left"), html.Button("right", id="right"), html.Div(id="log")])


@app.callback(Output("log", "children"), Input("left", "n_clicks"))
def left(_):
    return "left"


@app.callback(Output("log", "children"), Input("right", "n_clicks"))
def right(_):
    return "right"


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

For details on the implementation, see the dash-extensions docs or this thread. The core logic of the implementation was proposed by dwelch91 :slight_smile:

15 Likes

This is great! Thanks for sharing. Would it be possible to, eventually, combine the individual callbacks into a single callback instead? Or is that not the usecase this feature is intended for?

I am not sure I understand what you mean; you can always combine callbacks in Dash? (:

It seems like this Multiplexer allows you to ā€œsplitā€ the callbacks such that you can target the same output but via different inputs. Iā€™m unclear in what scenario youā€™d want to do something like this when the ā€œcombined callbacksā€ is already a thing with Dash.

Could you provide some more context behind why this is needed and how itā€™s supposed to be used?

It is an issue that is rather common to encounter is Dash**. The ā€œstandardā€ solution is to create one combined callback, which will typically require mixing the logic of otherwise unrelated functions along with not-so-nice dispatch logic (a lot of conditional statements based on inspection of which component triggered the callback).

As a concreate example, consider logging. Say that you have ā€œcreateā€, ā€œsaveā€, ā€œupdateā€, ā€œloadā€, etc. buttons that perform separate actions, but all needs to write some logging information (e.g. if an error occurs) to the same output. With MultiplexerTransform you can create an a callback for each button with their respective logic. Without it, you would have to create one callback to handle all actions, which will become rather messy (:

** Here are just a few threads on the topic,

3 Likes

Is this different from the GroupTransform from the previous version of the extension?

Yes. You donā€™t need to add groups anymore. And the callbacks are no longer grouped :slight_smile:

Thanks @Emil for your great works with dash-extension. I love your ServersideOutputTransform which helps me to speed up my app dramatically.

Iā€™m still searching for a way to fine-tune the speed of my app even further. Currently, my app has a giant callback with a long list of inputs to update a single output ( I didnā€™t use dash.callback_context.ctx.trigger yet, as proposed by others to check which input was changed before updating the output). Iā€™m wondering if Iā€™m using MultiplexerTransform instead, would it help to boost my appā€™s performance as compared to the giant callback with a dash.callback_context.ctx.triggered approach?

It is hard to say without seeing the code . Generally, if you just refactor into many ā€˜smallā€™ callbacks, the performance will probably be the same. However, if you can avoid doing expensive operations in some of the callbacks, performance will improve (:

Awesome! Thanks for the valuable work in packaging this together. Hopefully this will get integrated into core Dash library itself :slight_smile:

1 Like

Hi,
i have tried this extension to use multiple Outputs and it works well, but passing from

app = dash.Dash

to

app = DashProxy

cause the impossibility to share common data from my application pages using dcc.Location().

Interesting. Can you post an MWE?

Ok, butā€¦ what is a MWE? :sweat_smile:

Minimal Working Example, i.e. a small, self-contained code example demonstrating the issue :slight_smile:

Hi Emil!

Thanks for making an amazing extension for Dash users!

I would like to ask why does my code not work with MultiplexerTransform()

This is the example:


from dash import Dash, Input, Output, State, html, dcc, dash_table, callback
import os
import pandas as pd
import dash_bootstrap_components as dbc
from dash_extensions.enrich import DashProxy, MultiplexerTransform, Input, Output, State
from dash.exceptions import PreventUpdate
# does importing input output state from dash_extensions automatically replace the input output and state of Dash library?

PATH = os.getcwd()
PATH_DATA = PATH + "/datasets/"
df_contract = pd.read_excel(PATH_DATA + "Faker Contrat.xlsx")

app = DashProxy(
    prevent_initial_callbacks=True,
    suppress_callback_exceptions=True,
    external_stylesheets=[dbc.themes.BOOTSTRAP],
    meta_tags=[{"name": "viewport", "content": "width=device-width, initial-scale=1"}],
    transforms=[MultiplexerTransform()]
)

buttongroup = dbc.ButtonGroup(
    [
        dbc.Button('Save', color='primary', className="mr-1", id="save"),
        dbc.Button('Refresh', color='warning', className="mr-1", id="refresh"),
        dbc.Button('Apply', color='success', className="", id="apply")
    ],
    id="buttongroup",
    size='md',
    className="d-flex position-relative",
)

app.layout = dbc.Container(
    dbc.Card(
        [
            dbc.CardHeader(
                [
                    html.H3("Heading"),
                ]
            ),
            dbc.CardBody(
                [
                    html.H4("Insurers"),
                    dcc.Dropdown(
                        options=[
                            {'label': name, "value": name} for name in df_contract['Assureur'].drop_duplicates()
                        ],
                        value=[],
                        id="dropdown-1",
                        multi=True,
                    ),
                    dcc.Checklist(
                        options=[{"label": "All", "value": "All", "disabled": False}],
                        value=[],
                        id="checklist-1"
                    ),
                    html.Hr(),
                    html.H4("Life Insurance Plans"),
                    dcc.Dropdown(
                        options=[
                            {'label': name, "value": name} for name in df_contract['Nom Assurance Vie'].drop_duplicates()
                        ],
                        value=[],
                        id="dropdown-2",
                        multi=True,
                    ),
                    dcc.Checklist(
                        options=[{"label": "All", "value": "All", "disabled": False}],
                        value=[],
                        id="checklist-2"
                    ),
                    html.Hr(),
                    html.Div(id='table')
                ]
            ),
            dbc.CardFooter(
                [
                    buttongroup
                ]
            )
        ],
        body=True,
        className="h-100 flex-column",
    ),
    style={"height": "100vh"},
)

@callback(
    Output("dropdown-1", "value"),
    Input("checklist-1", "value")
)
def select_all(value):
    if value == ["All"]:
        return df_contract['Assureur'].drop_duplicates()

@callback(
    Output("dropdown-2", "options"),
    Input("dropdown-1", "value"),
)
def asd(value):
    if value is None:
        raise PreventUpdate
    else:
        df_temp = df_contract[df_contract['Assureur'].isin(value)]
        return [{'label': name, "value": name} for name in df_temp['Nom Assurance Vie']]

@callback(
    Output("table", "children"),
    Input("apply", "n_clicks"),
    State("dropdown-1", "value"),
    State("dropdown-2", "value")
)
def update_table(n, val1, val2):
    if n is None:
        raise PreventUpdate
    else:
        filter_insurer = df_contract["Assureur"].isin(val1)
        filter_plans = df_contract["Nom Assurance Vie"].isin(val2)

        df_0 = df_contract[filter_insurer]
        df_1 = df_0[filter_plans]

        return dash_table.DataTable(
            columns=[{"name": i, "id": i} for i in df_contract.columns],
            data=df_1.to_dict('records'),
            style_cell={"font-family": "sans-serif"},
            fixed_columns={'headers': True, 'data': 2},
            style_table={'minWidth': '100%'},
            style_as_list_view=True
        )


@callback(
    output=dict(
        dd1=Output("dropdown-1", "value"),
        dd2=Output("dropdown-2", "value")
    ),
    inputs=dict(
        refresh=Input("refresh", "n_clicks"),
    ),
)
def contract_refresh(refresh):
    if refresh is None:
        raise PreventUpdate
    if refresh > 0:
        return dict(
            dd1=[],
            dd2=[]
        )

if __name__ == "__main__":
    app.run_server(port=1111, debug=True)

everything works until I put @callback def contract_refresh (the very last one) and it shows this error:
Screenshot 2021-09-08 at 11.57.54

The sample data used looks like this:

ID Assureur Nom Assurance Vie Frais UC (%) Frais SCPI (%) Frais PEP (%) Frais sur versement (%) Frais dā€™administration (%) Frais fonds en euros (%) Frais arbitrage FE et UC (%) Frais arbitrege SCPI (%) Souscription minimum Versement minimum Droits dā€™entrĆ©e
UNPMSP UNEP Unep MultisƩlection Plus 1,00 1,14 1,00 5,00 0,00 1,00 0,00 0,50 1 200,00 1 200,00 11,00
UNPEVL UNEP Unep Evolution 0,90 1,00 0,00 3,00 0,00 0,98 0,50 0,50 50 000,00
AXAEXL AXA Axa Excelium 0,96 0,90 0,00 4,85 0,00 0,80 0,00 0,00 750,00 750,00 750,00
AXAMIL AXA Axa Millenium 0,96 1,00 0,00 2,00 0,00 0,90 0,40 0,50 - - -

The app looks like this:

Thank you so much in advance!

It is generally not recommended to do the Input, Output, State imports from dash. Do you get the same issue if you remove them?

EDIT: I just noticed the callback is using a (new?) syntax with dicts. Try adopding the ā€œnormalā€ syntax as in the other callbacks.

Hi, I removed importing line from dash for Input, Output, State and I also removed new syntax for callbacks and it still does not work.

Here is the code after modification


from dash import html, dcc, dash_table, callback
import os
import pandas as pd
import dash_bootstrap_components as dbc
from dash_extensions.enrich import DashProxy, MultiplexerTransform, Input, Output, State
from dash.exceptions import PreventUpdate
# does importing input output state from dash_extensions automatically replace the input output and state of Dash library?

PATH = os.getcwd()
PATH_DATA = PATH + "/datasets/"
df_contract = pd.read_excel(PATH_DATA + "Faker Contrat.xlsx")

app = DashProxy(
    prevent_initial_callbacks=True,
    suppress_callback_exceptions=True,
    external_stylesheets=[dbc.themes.BOOTSTRAP],
    meta_tags=[{"name": "viewport", "content": "width=device-width, initial-scale=1"}],
    transforms=[MultiplexerTransform()]
)

buttongroup = dbc.ButtonGroup(
    [
        dbc.Button('Save', color='primary', className="mr-1", id="save"),
        dbc.Button('Refresh', color='warning', className="mr-1", id="refresh"),
        dbc.Button('Apply', color='success', className="", id="apply")
    ],
    id="buttongroup",
    size='md',
    className="d-flex position-relative",
)

app.layout = dbc.Container(
    dbc.Card(
        [
            dbc.CardHeader(
                [
                    html.H3("Heading"),
                ]
            ),
            dbc.CardBody(
                [
                    html.H4("Insurers"),
                    dcc.Dropdown(
                        options=[
                            {'label': name, "value": name} for name in df_contract['Assureur'].drop_duplicates()
                        ],
                        value=[],
                        id="dropdown-1",
                        multi=True,
                    ),
                    dcc.Checklist(
                        options=[{"label": "All", "value": "All", "disabled": False}],
                        value=[],
                        id="checklist-1"
                    ),
                    html.Hr(),
                    html.H4("Life Insurance Plans"),
                    dcc.Dropdown(
                        options=[
                            {'label': name, "value": name} for name in df_contract['Nom Assurance Vie'].drop_duplicates()
                        ],
                        value=[],
                        id="dropdown-2",
                        multi=True,
                    ),
                    dcc.Checklist(
                        options=[{"label": "All", "value": "All", "disabled": False}],
                        value=[],
                        id="checklist-2"
                    ),
                    html.Hr(),
                    html.Div(id='table')
                ]
            ),
            dbc.CardFooter(
                [
                    buttongroup
                ]
            )
        ],
        body=True,
        className="h-100 flex-column",
    ),
    style={"height": "100vh"},
)

@callback(
    Output("dropdown-1", "value"),
    Input("checklist-1", "value")
)
def select_all(value):
    if value == ["All"]:
        return df_contract['Assureur'].drop_duplicates()

@callback(
    Output("dropdown-2", "options"),
    Input("dropdown-1", "value"),
)
def asd(value):
    if value is None:
        raise PreventUpdate
    else:
        df_temp = df_contract[df_contract['Assureur'].isin(value)]
        return [{'label': name, "value": name} for name in df_temp['Nom Assurance Vie']]

@callback(
    Output("table", "children"),
    Input("apply", "n_clicks"),
    State("dropdown-1", "value"),
    State("dropdown-2", "value")
)
def update_table(n, val1, val2):
    if n is None:
        raise PreventUpdate
    else:
        filter_insurer = df_contract["Assureur"].isin(val1)
        filter_plans = df_contract["Nom Assurance Vie"].isin(val2)

        df_0 = df_contract[filter_insurer]
        df_1 = df_0[filter_plans]

        return dash_table.DataTable(
            columns=[{"name": i, "id": i} for i in df_contract.columns],
            data=df_1.to_dict('records'),
            style_cell={"font-family": "sans-serif"},
            fixed_columns={'headers': True, 'data': 2},
            style_table={'minWidth': '100%'},
            style_as_list_view=True
        )

@callback(
    Output("dropdown-1", "value"),
    Output("dropdown-2", "value"),
    Input("refresh", "n_clicks")
)
def contract_refresh(refresh):
    if refresh is None:
        raise PreventUpdate
    if refresh > 0:
        return [], []

if __name__ == "__main__":
    app.run_server(port=1111, debug=True)

@Gordon_Shapiro your example doesnā€™t run. It needs external file(s), so I cannot reproduce the issue.

EDIT: I just noticed that you are using the (new) @callback decorator. It is not supported, so please use @app.callback instead

1 Like

@app.callback resolved the problem! Thank you so much for making this amazing stuff T_T

Hi Emil. Amazing work on these dash functionsā€¦
Iā€™d love to use Multiple Callback but as Iā€™m using multipage I had to replace all the @app.callback by @callback
Is there a way to either make Multiple callbacks work with @callback or make multipage dash work with @app.callback ?
Note for multipage Iā€™m using the newly released functionality: register each page with dash.register_page(ā€¦), and initialize the app with app = Dash(name, plugins=[dash_labs.plugins.pages])
Thank you