Quantcast
Channel: 🎛️ Dash - Plotly Community Forum
Viewing all articles
Browse latest Browse all 6271

Show and Tell: Python wrapper code for easier multiple outputs

$
0
0

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

2 posts - 2 participants

Read full topic


Viewing all articles
Browse latest Browse all 6271

Trending Articles