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