🚀 Gen 5 of the leading AI app deployment platform launches October 6. Click for the livestream.

Show and Tell: Python wrapper code for easier multiple outputs

In a project I’m working on, I was running into the “cannot have multiple callbacks update an output” issue crop up numerous times. I tried a few approaches to create a workaround for this so that I could more easily and cleanly code up what I needed. I thought I’d share what I came up with in case it helps anyone else.

Scenario: You have 2 or more callbacks that need to update a single property on a component. Currently, Dash does not allow you to (directly) do this, a component + prop may only appear in one and only one Output().

Solution: dcc.Store() and some dynamic Python code makes this nearly as easy as if it was natively supported. I am still hoping that Dash gains this ability itself at some point, but I was able to get quite far with this approach.

First step, include this code in your project:

from time import sleep, time
from typing import List, Optional

from dash import callback_context
from dash.dependencies import ALL, Input, Output
from dash.exceptions import PreventUpdate

import dash_core_components as dcc

MULTIPLEXER_OUTPUTS = {}

def get_triggered() -> Optional[str]:
    ctx = callback_context
    if not ctx.triggered:
        return
    return ctx.triggered[0]['prop_id'].split('.')[0]

def create_multiplexer(output_component: str, output_field: str, count: int) -> List:
    store_component = f'{output_component}-{output_field}-store'

    @app.callback(
        Output(output_component, output_field),  # Output is an arbitrary dash component
        Input({'id': store_component, 'idx': ALL}, 'data'),  # Input is always a dcc.Store()
        prevent_initial_call=True
    )
    def multiplexer(_):
        triggered = get_triggered()
        if triggered is None:
            raise PreventUpdate

        inputs = callback_context.inputs
        for k, v in inputs.items():
            id_ = k.split('.')[0]
            if id_ == triggered:
                return v

        raise PreventUpdate

    return [dcc.Store({'id': store_component, 'idx': idx}, data=None) for idx in range(count)]


def MultiplexerOutput(output_component: str, output_field: str):
    store_component = f'{output_component}-{output_field}-store'
    MULTIPLEXER_OUTPUTS[store_component] = idx = MULTIPLEXER_OUTPUTS.setdefault(store_component, -1) + 1
    return Output({'id': store_component, 'idx': idx}, 'data')

Second step, determine the number of outputs that you need to feed into the single output. Pass that number in a call to create_multiplexer() along with the target component and prop. This function returns a list of dcc.Store components, and they must be incorporated into your layout, so generally you’d call it in a list of children in a Div using a spread operator *. For example, to allow 3 callbacks to disable a button with id the-button-id, you could have this in your layout:

layout = html.Div([
    *create_multiplexer('the-button-id', 'disable', 3),
    ...

Next step is to setup your 3 callbacks to control this component prop. Here’s what one of those 3 might basically look like:

app.callback(
    MultiplexerOutput('the-button-id', 'disable'),
    Output(...),
    Input(...),
    ...
)
def foo(...):
   ...
    return True, ...

At that point, the MultiplexerOuput works just like any other Output except that it can appear in 3 (for this example) app callbacks.

Notes:

  • The MULTIPLEXER_OUTPUTS as a global seems to work fine as every session/client has the same component setup.
  • The target component can be a dcc.Store itself, so you can create shared data that can be updated in several places. You can also chain multiplexers together.
  • I attempted to have an automatic way of setting/capturing the output count, but ran into some issues. It would be nice if, when you add an additional MultiplexerOutput that a backing store was automatically created as well. So, you have to remember to bump that number up/down as your design changes. It is pretty obvious if you don’t have that number set high enough, you will get errors about Stores that don’t exist.

Anyway, that’s it. If anyone has any questions, etc. please let me know.
-Don

6 Likes

That is a super creative work around! And unless you are targeting an output an excessive amount of times, I guess it could be adopted to perform almost as ‘native’. I think it could fit nicely within the Transform framework of dash extensions, which would make it much easier to use. I’ll take a stab at an implementation when I find the time :upside_down_face:

EDIT: I think this integration would also solve your ‘counting issue’ :slight_smile:

Based on your idea, I have just made an initial implementation in dash-extensions. I have solved the counting issue, simplified the syntax and improved the performance by moving the multiplexer logic clientside. If you want to try it out,

pip install dash-extensions==0.0.47rc1

The only thing you need to do is to add the new MultiplexerTransform to specify that you wan’t to use the new multiplexer feature. Here is a small app with 10 buttons that all write to the same log,

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

n = 10
# Create example app with n buttons.
buttons = [html.Button(f"Button {i}", id=f"btn_{i}") for i in range(n)]
app = DashProxy(prevent_initial_callbacks=True, transforms=[MultiplexerTransform()])
app.layout = html.Div(buttons + [html.Div(id="log")])
# Target the same output n times.
for button in buttons:
    app.callback(Output("log", "children"), Input(button.id, "n_clicks"))(lambda _, x=button.id: f"Your clicked {x}!")

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

I haven’t tested much yet, but I seems to work as intended! I’am bit excited as this issue has been a key drawback for Dash, and your work around is the best one I have seen so far :smiley:

@AnnMarieW - Check this out!

Please let me know if you have any ideas for improvements and/or find any bugs. The code is here,

1 Like

@Emil and @dwelch91

WOW - This is awesome! Nice work :clap:

I’ll start using it and let you know if I find any issues or have questions. This is a great addition to Dash.

Thanks to both of you! :star_struck: :star_struck:

1 Like

@Emil

I’m trying to update dash-extension using

pip install dash-extensions=0.0.47rc1

but nothing happens :thinking:

Should be pip install dash-extensions==0.0.47rc1 (notice == vs =)

Yes, I did but is showing this that I don’t understand:

Try updating pip, sometimes that is necessary.

hmm… I’m facing the same issue. I’m using the latest pip - 21.0.1
The most recent version of dash-extensions it can find is 0.0.46

Argh, sorry. I didn’t notice that the upload failed. It should be there now,

That worked - Thanks :confetti_ball:

1 Like

Hey now it works.

Congratulations @dwelch91 and @Emil you did an amazing job. It is very interesting to have this issue solved so easy to implement. :grinning: :tada: :tada: :tada:

Hey @chriddyp , where are you ?? we miss you :joy: :joy: Check this extraordinary tool.

3 Likes

Very clever solution! :smiley:

Hi @Emil and @dwelch91, thank you for the great work you are doing here. I am working on a project where most of my experiment is on Jupyter notebook. I was wondering if is there a Jupyter version of this extension? I have used the dash-extension on a small data set and it worked but implementing same in Jupyter notebook on the server where I have the dataset for the project is giving error.

Hi @dwelch91, I tried using your suggestion in jupyterdash but the app is not displaying any output but I have no errors. Please what could be wrong?

Please see my code snippet below:

app.layout = …
.
.
.
html.Hr(),
html.Div([
dcc.Tabs(id=‘tabs-example’, value=‘tab-1’, children=[
dcc.Tab(label=‘abseee’,
value=‘cus-sys’,
style=tab_style,
selected_style=tab_selected_style
),
dcc.Tab(label=‘Data-seelele’,
value=‘upload’,
style=tab_style,
selected_style=tab_selected_style
),
dcc.Tab(label=‘Mismputation’,
value=‘mvi’,
style=tab_style,
selected_style=tab_selected_style
),
dcc.Tab(label=‘Trainingxxx’,
value=‘training’,
style=tab_style,
selected_style=tab_selected_style
),
dcc.Tab(label=‘Resbbbbults’,
value=‘result’,
style=tab_style,
selected_style=tab_selected_style
),
], style=tabs_styles),
html.Div([*create_multiplexer(‘tabs-example-content’, ‘children’, 2)])
])
],
fluid=True,
)
.
.
.
@app.callback(
MultiplexerOutput(‘tabs-example-content’, ‘children’),
#Output(‘tabs-example-content’, ‘children’),
Input(‘upload-data’, ‘contents’),
State(‘upload-data’, ‘filename’),
State(‘upload-data’, ‘last_modified’))
def update_output(list_of_contents, list_of_names, list_of_dates):
if list_of_contents is not None:
children = [
parse_contents(c, n, d) for c, n, d in
zip(list_of_contents, list_of_names, list_of_dates)]
return children

@app.callback(
MultiplexerOutput(‘tabs-example-content’, ‘children’),
#Output(‘tabs-example-content’, ‘children’),
Input(‘tabs-example’, ‘value’))
def render_content(tab):
if tab == ‘cus-sys’:
return tab1_content
elif tab == ‘upload’:
return tab2_content
elif tab == ‘mvi’:
return tab1_content

elif tab == 'training':
    return html.Div([
        html.H3('Tab content 2')
    ])
elif tab == 'result':
    return html.Div([
        html.H3('Tab content 2')
    ])

I haven’t made a Jupyter version as I don’t use Jupyter myself, so the incompabilty is not unexpected. If you find a good solution, I would be happy to look at a pull request :slight_smile: