[Solved] Multiple models plus multiple toolbars of multiple widgets: managing callbacks

I have an app that is almost entirely functional. By changing a single global variable, the app can either run a simulation using math model #1 and toolbar #1 (consisting of 6 pairs of Labels and Inputs), or using model #2 and toolbar #2 (consisting of 5 pairs of different Labels/Inputs). I use a class for each model to hold attributes, generate and store the toolbars required, and generate a figure for the Graph. My plan was to use a RadioItems to toggle between the two models.

Several problems were immediately apparent. According to the documentation, changing a global variable such as “active_model” would be a Bad Thing, but using the RadioItems value plus a dictionary lookup to find the model was an easy enough workaround. That was chained to a callback to update the toolbar, which worked. The snag is that, whenever the model/toolbar changes, AND whenever the Input widgets contained in the toolbar change their value, there must be a callback to generate a figure from that model and update the Graph. There can’t be multiple Outputs to the same target, so there must be a catch-all routine where on any change to the program’s state the correct model is called with the correct variables to produce the correct figure.

If I’m reading the resources and forums correctly, the workaround would seem to be:

  • export the keyword for the current model to another html element, as a proxy for a global variable
  • dig through the toolbar Div’s children list-of-dictionaries to find the current toolbar’s Input values, and json-dump them to another element
  • create separate figure-update callbacks for each model type, and decorate multiple copies of an update_figure function with them
  • create a unique Graph object for each model, and swap them as well, because two callbacks can’t output to the same element (i.e. can’t have two Outputs target the same Graph figure).

This seems like a heck of a workaround for changing one global variable. Am I missing something? Is there an easier way? This seems doable with only two models, but a dozen models and a dozen toolbars with scores of Inputs changing a dozen figures for a dozen graphs would be highly repetitive and tedious.

The core code of the simulation in the current, “hard code the desired model as active_model” state:

import dash
import dash_core_components as dcc
import dash_html_components as html
from dash.dependencies import Input, Output
import plotly.graph_objs as go
import numpy as np

# A class for each model handles widgets, toolbars, and generating Graph
# figures.
from model_definitions import dnmr_two_singlets_kwargs, dnmr_AB_kwargs
from models_dash import BaseDashModel

app = dash.Dash()
app.css.append_css(
    {'external_url': 'https://codepen.io/chriddyp/pen/bWLwgP.css'})

dnmr_two_singlets = BaseDashModel(**dnmr_two_singlets_kwargs)
dnmr_AB = BaseDashModel(**dnmr_AB_kwargs)
# The app can be run in single-model mode by hard-coding the model below
active_model = dnmr_AB  # choose one of two models above

app.layout = html.Div([

    # top toolbar: list of Label/Input paired widgets
    html.Div(id='top-toolbar', children=active_model.toolbar),

    # The plot
    dcc.Graph(id='test-dnmr-plot')])  # figure added by callback


@app.callback(
    Output(component_id='test-dnmr-plot', component_property='figure'),
    [Input(component_id=key, component_property='value')
     for key in active_model.entry_names])
def update_graph(*input_values):
    variables = (float(i) for i in input_values)

    return active_model.update_graph(*variables)


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

Screenshots: the first two show the two different toolbar configurations for the two models. The third shows partial workaround, with radio buttons and json dumps employed.

The key thing is getting the right abstraction in the first place to deal with the constraints that Dash places upon you. I would say the abstraction you’ve made that’s sent you down this path with possibly no solution is try and use the one callback for both models.

Another way of laying out your app is use Dash’s URL routing mechanism to select between alternative layouts each corresponding to the two models and then registering every callback needed for both models. So your router could look like this:

route_map = {
    '/model1': model1.layout,
    '/model2': model2.layout,
}

@app.callback(Output('content-container', 'children'), [Input('url', 'pathname')])
def display_page(pathname):
    default_layout = html.P("No page '{}'".format(pathname))
    layout = route_map.get(pathname, default_layout)
    return layout

As for how you register the callbacks and organise the code of your models, in the above snippet, model1 and model2 could be either separate modules which import the Dash instance from the same app.py file (eg they would both be doing from app import app), which is how @chriddyp organised the Dash tutorial or you could provide your own class-based abstraction which creates new model instances parameterised by the Dash app instance, and which include both layout and callbacks, as I suggest in this github issue. Or of course, come up with your own preferred abstraction.

2 Likes

Oh yeah, I should also add that this means you need to set app.config.suppress_callback_exceptions = True as you’ll be registering callbacks for layouts that aren’t yet loaded.

Also, one potential gotcha is that you’ll need to ensure that you keep the IDs used for your Inputs and States distinct across the two different models, as repeated usage of IDs will class with the exiting one.

1 Like

Thanks for the pointers, which let me get to a working prototype:

2 Likes