Workarounds for Multiplexer plugin & MATCH selector

Hi all

I’m trying to write more efficient and scalable/reusable code using the Multiplexer plugin, and pattern-matching callbacks. As previously reported, the MATCH selector is not compatible with the Multiplexer feature. I’m wondering if there is another elegant & efficient method to reuse code.

My current situation is the following: I have a quite big multi-page dash App. I have a Plotly graph in which my data is visualized. The data for the graph is selected out of Table, and below the graph I have div where you can select layout options (=a layout editor for the graph). Currently, I have two functions ( data_selection() and layout_editor()), both have the graph as output, which works because of the Multiplexer plugin. However, I would like to reuse the layout-editor on other pages of my app as well.

Currently, every time I want to use this layout-editor again for a graph I have to copy the corresponding layout-editor_callbacks.py file, and edit all the id’s to match the new graph. However, when I change something to the layout-editor I have to go through each instance of this copied layout-editor and update the corresponding callbacks. Normally, such a scenario can be avoid using the MATCH selector (pattern-matching callbacks). However, this is not possible if more than one callback has the same output (i.e. the graph is the outputs of multiple callbacks).

I have been searching for a solution for quite some time now, but the only thing I have found was this post on stackoverflow where it is suggested to use the ALL selector (in combination with callback_context) rather than the MATCH selector, as the former is compatible with the Multiplexer feature. This still feels very inconvenient, as once you have more than a few instances this becomes a very big decision tree. (see below for a minimalistic example).

@callback(
    Output({'type':'graph_scatter', 'index':'page1'}, 'figure'),
    Output({'type':'graph_scatter', 'index':'page2'}, 'figure'),
    ....
    Output({'type':'graph_scatter', 'index':'page10'}, 'figure'),
    Input({"type":"fig_height","index":dash.ALL}, 'value'),
    Input({"type":"fig_width","index":dash.ALL}, 'value'),
    State({'type':'graph_scatter', 'index':dash.ALL}, 'figure')
    )
def update_figure_size(fig_height, fig_width, fig):
    ctx = callback_context.triggered_id
    if ctx == "page1":
        return(update_fig_size(),no_update, no_update, ...., no_update)
    elif ctx == "page2":
        return(no_update,update_fig_size(), no_update, ...., no_update)
    ....

Any workarounds or tips would be very helpful.

Thanks in advance!

Hello @F2P,

What you might be able to do is create an entire function that crafts this layout and the expected callbacks.

The downside is that it kinda becomes a static layout in the sense of dynamic loading on the backend. But can be updated via callbacks once loaded.

Basically, in your function you pass the id that you want to represent on the page and it does the rest.

———
You can put this function under a new folder called utils.

One option would be to create the graph layout and callbacks as a paramatrized DashBlueprint, possibly in a seperate file. You could then embed mutiple instances with different prefixes, thus avoiding callback id clashes.

Thanks for the comment. I have thought about this option as well. Right now the layout is already created dynamically, i.e. a function create_graph_editor(id) exists, where id is a string to create unique id’s for all the components. However is it possible to dynamically create @callbacks as well as you seem to suggest? If so, could you provide a minimal example of how those would work.

Thanks for the help.

It is not possible to create callbacks dynamically (specifically, it is not possible to create callbacks after the app has been created*). Hence, unless you can register all callbacks before app initialization, pattern matching callbacks is the only viable solution.

  • There might hacks that can achieve this, but they are not recommended, and the behaviour will likely be unpredictable, epecially of you use multiple workers

Hello @F2P,

Yes, you should be able to register the callbacks during the function. You cannot during runtime, but only during the initialization.

@Emil, that was what lead to my comment about losing dynamic loading of information.

Basically, instead of registering the page as layout, you’d register the page with layout(). This runs the function and passes all the variables to the app. Then, you have to use a callback to load the dynamic information upon page navigation, if necessary.

The suggestion of DashBlueprint made by Emil seems to work quite nicely for my purpose. As previously mentioned all the callbacks have to be registered when starting the app (no dynamic creation of callbacks).

Below I have provided a minimalistic example of how you can implement the DashBlueprint in a multipage app. I believe the trick is to explicit create your items before running the app. In helperfile.py I create first_item & second_item before the app is excuted. When needed, these items are loaded into the layout for the user (in this example, if the user is logged in, they appear in home.py).

Below you can find the code I used. For clarity I have simplified the app.py file.

app.py

from dash_extensions.enrich import DashProxy, MultiplexerTransform, DashBlueprint
from helperfile import bp

app = DashProxy(
    __name__, 
    plugins=[dl.plugins.pages], 
    transforms=[MultiplexerTransform()],
    blueprint=bp,
)
....
if __name__ == "__main__":
    app.run_server(debug=True)

helperfile.py

from dash_extensions.enrich import DashBlueprint, html, Output, Input

bp = DashBlueprint()

def create_layout(id, title):
    bp.layout = html.Div([
        html.H1(title),
        html.Button(
            'Click me!', 
            id=id), 
        html.Div(id=f'log_{id}')])

    @bp.callback(Output(f'log_{id}', 'children'), Input(id, 'n_clicks'))
    def on_click(n_clicks):
        return f"Hello world {n_clicks}!"
    return(bp.layout)

first_item = create_layout('new_item1', "My first item")
second_item = create_layout('new_item2', "My second item")

home.py

from dash import html, callback, Output, Input, State, dcc, register_page

register_page(__name__, path="/home")

layout = html.Div(children = [], id = "page_home_auth")

def created_logged_in_layout():
    first_item,
    second_item 

]))

# check login
@callback(
    Output("page_home_auth", "children"),
    Input("login_status", "data"))
def authenticate(logged_in):
    if logged_in:
        return created_logged_in_layout()

    return dcc.Location(pathname="/", id="someid")

Thanks @Emil and @jinnyzor for their input.

EDIT: Just want to mention that using DashBlueprint is not a necessity to having this workaround to proper excute. I removed the incorporation of DashBlueprint as I experienced some issues with the multi-pages module.

1 Like