Dynamic Output Components Created in a Callback

Objective of what I’m aiming for: a dropdown allows a user to indicate the type of plotly graph they’d like to use. This generates a form with dropdown fields for arguments x, y, color, etc., each of which should become connected to a dcc.Graph object.

Related threads: I am aware of this topic being raised here, however, all callbacks were created a priori in that example and they were created in the global scope. I have used the callback factory from that thread in a local callback scope.

Code: The following should demonstrate what I’m after, but currently does not seem to link the generated callback to the generated dropdowns. Could anyone suggest a fix to this code?

import dash
import dash_core_components as dcc
import dash_bootstrap_components as dbc
from dash.dependencies import Input, Output, State
import plotly.express as px
import numpy as np
import pandas as pd

# App & Data Prep
app = dash.Dash(__name__, suppress_callback_exceptions=True, external_stylesheets=[dbc.themes.LUX])
df = pd.DataFrame({
    "A": np.random.random(100),
    "B": pd.Categorical(np.random.choice(["Foo", "Bar", "Baz"], 100)),
    "C": np.random.random(100),
    "D": pd.Categorical(np.random.choice(["Do", "Re", "Mi"], 100)),
    "E": np.random.randint(0, 50, 100),
    "F": pd.Categorical(np.random.choice(["John", "James"], 100))
}).drop_duplicates()
numerical_cols = [col for col in df.columns if df[col].dtype.name not in {"object", "category"}]
category_cols = [col for col in df.columns if df[col].dtype.name in {"object", "category"}]

# Initial Layout
form_col = dbc.Col([], id="form-column", width=6)
graph_col = dbc.Col([dcc.Graph(id="target-graph", figure={})], id="graph-column", width=6)
graph_selector = dcc.Dropdown(id="graph-selector", options=[{"label": x, "value": x} for x in ["scatter", "box"]])
first_row = dbc.Row([dbc.Col([graph_selector], width=6)], className="mt-3")
second_row = dbc.Row([form_col, graph_col])
app.layout = dbc.Container([first_row, second_row], fluid=True)


# Callback factory
def generate_output_callback(*factory_args):
    print(f"generate_output_callback was invoked with {factory_args}")

    def output_callback(*callback_args):
        print(f"output_callback was invoked with arguments: {callback_args}")
        return dict()

    return output_callback


# Outer Callback should make the form and connect the form's elements to the graph's output
@app.callback(Output("form-column", "children"),
              Input("graph-selector", "value")
              )
def dynamic_form_generate(graph_type):
    if graph_type is None:
        return dash.no_update

    # Define dropdowns; and create the appropriate form for each
    if graph_type == "scatter":
        x_drpdwn = dcc.Dropdown(id="dropdown-x", options=[{"label": col, "value": col} for col in numerical_cols])
        y_drpdwn = dcc.Dropdown(id="dropdown-y", options=[{"label": col, "value": col} for col in numerical_cols])
        color_drpdwn = dcc.Dropdown(id="dropdown-color",
                                    options=[{"label": col, "value": col} for col in category_cols])
        size_drpdwn = dcc.Dropdown(id="dropdown-size",
                                   options=[{"label": col, "value": col} for col in category_cols])
        # Inputs for callback generation
        inputs = [Input(component.id, "value") for component in [x_drpdwn, y_drpdwn, color_drpdwn, size_drpdwn]]

        # Form
        x_row = dbc.FormGroup([dbc.Label("x", width=2), dbc.Col(x_drpdwn, width=10)], row=True)
        y_row = dbc.FormGroup([dbc.Label("y", width=2), dbc.Col(y_drpdwn, width=10)], row=True)
        color_row = dbc.FormGroup([dbc.Label("color", width=2), dbc.Col(color_drpdwn, width=10)], row=True)
        size_row = dbc.FormGroup([dbc.Label("size", width=2), dbc.Col(color_drpdwn, width=10)], row=True)
        form = dbc.Form([x_row, y_row, color_row, size_row], className="mt-3")

    elif graph_type == "box":
        x_drpdwn = dcc.Dropdown(id="dropdown-x", options=[{"label": col, "value": col} for col in category_cols])
        y_drpdwn = dcc.Dropdown(id="dropdown-y", options=[{"label": col, "value": col} for col in numerical_cols])
        color_drpdwn = dcc.Dropdown(id="dropdown-color",
                                    options=[{"label": col, "value": col} for col in category_cols])

        # Inputs for callback generation
        inputs = [Input(component.id, "value") for component in [x_drpdwn, y_drpdwn, color_drpdwn]]

        # Form
        x_row = dbc.FormGroup([dbc.Label("x", width=2), dbc.Col(x_drpdwn, width=10)], row=True)
        y_row = dbc.FormGroup([dbc.Label("y", width=2), dbc.Col(y_drpdwn, width=10)], row=True)
        color_row = dbc.FormGroup([dbc.Label("color", width=2), dbc.Col(color_drpdwn, width=10)], row=True)
        form = dbc.Form([x_row, y_row, color_row], className="mt-3")
    else:
        raise TypeError(f"Invalid graph type somehow specified: {graph_type}")

    # Create a callback to link the form dropdowns to the figure. Why doesn't this work?
    app.callback(Output("target-graph", "figure"),
                 inputs
                 )(generate_output_callback(inputs))

    return form


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

Hey mpasternak, Did you find any solution to your problem? I am also facing the similar problem

Turns out it is impossible to create new callbacks inside a callback as I have attempted in my original post.

All callbacks must be defined ahead of time.