Clear explanation of dynamic callback generation

Hello,

I am working on an app that creates a new plot from a callback. I would like to dynamically assign a callback to that plot as well.

I’ve read and tried what is described here but I can’t get it to work.

In this example I am declaring the callbacks statically for the first two plots:

app = Dash()
.
.
.
@app.callback([Output('tabs', 'children')],[Input('button', 'n_clicks')])
def create_graph(n_clicks):
    if(n_clicks is None):
        #do nothing
    else:
        #create new dcc.Tab containing new dcc.Graph with id="plot"+str(n_clicks)
    return #updated tabs children


@app.callback([Output('plot1', 'figure')],
              [Input('plot1', 'relayoutData')])
def callback_function(input):
    return do_something(input)

@app.callback([Output('plot2', 'figure')],
              [Input('plot2', 'relayoutData')])
def callback_function(input):
    return do_something(input)

Now this obviously works fine for the first two plots, but when I try to create those callbacks dynamically the callbacks are not triggered, as if they didn’t existed:

app = Dash()
.
.
.
@app.callback([Output('tabs', 'children')],[Input('button', 'n_clicks')])
def create_graph(n_clicks):
    if(n_clicks is None):
        #do nothing
    else:
        #create new dcc.Tab containing new dcc.Graph with id="plot"+str(n_clicks)
        @app.callback([Output("plot"+str(n_clicks), 'figure')],
                      [Input("plot"+str(n_clicks), 'relayoutData')])
        def callback_function(input):
            return do_something(input)
    
        return #updated tabs children

But when I try to mix both codes (declare them statically and dynamically), just for the sake of understanding what is going on then it tells me that I am creating a duplicate callback output:

Merged code:

app = Dash()
.
.
.
@app.callback([Output('tabs', 'children')],[Input('button', 'n_clicks')])
def create_graph(n_clicks):
    if(n_clicks is None):
        #do nothing
    else:
        #create new dcc.Tab containing new dcc.Graph with id="plot"+str(n_clicks)
        @app.callback([Output("plot"+str(n_clicks), 'figure')],
                      [Input("plot"+str(n_clicks), 'relayoutData')])
        def callback_function(input):
            return do_something(input)
    return #updated tabs children


@app.callback([Output('plot1', 'figure')],
              [Input('plot1', 'relayoutData')])
def callback_function(input):
    return do_something(input)

@app.callback([Output('plot2', 'figure')],
              [Input('plot2', 'relayoutData')])
def callback_function(input):
    return do_something(input)

Exception:

dash.exceptions.DuplicateCallbackOutput:
Multi output …plot1.figure… contains an Output object
that was already assigned.
Duplicates:
{‘plot1.figure’}

So what I get from this is that when I try to create them dynamically they do get created but are unresponsive.

How can I create them dynamically?

see Dynamic Controls and Dynamic Output Components

Thanks for the fast reply on a sunday!

see Dynamic Controls and Dynamic Output Components

I’ve implemented what is proposed in that post and the one I linked to before:

app = Dash()
.
.
.
@app.callback([Output('tabs', 'children')],[Input('button', 'n_clicks')])
def create_graph(n_clicks):
    if(n_clicks is None):
        #do nothing
    else:
        #create new dcc.Tab containing new dcc.Graph with id="plot"+str(n_clicks)
    return #updated tabs children

def create_callback(retfunc):
    """creates a callback function"""
    def callback(*input_values):
        if input_values is not None and input_values!='None':
            try:
                retval = retfunc(*input_values)
            except Exception as e:
                exc_type, exc_obj, exc_tb = sys.exc_info()
                fname = os.path.split(exc_tb.tb_frame.f_code.co_filename)[1]
                print('Callback Exception:',e,exc_type, fname, exc_tb.tb_lineno)
                print('parameters:',*input_values)
            return retval
        else:
            return []
    return callback

dyn_func = create_callback(save_zoom)

[app.callback([Output('plot'+str(i), 'figure')],
              [Input('plot'+str(i), 'relayoutData')],
              [State('plot'+str(i), 'figure'),
               State('tabs', 'value')]) (dyn_func) for i in range(2)]

But to me, this is essentially the same as creating the callbacks statically. Although I am technically creating them dynamically I still need to know beforehand the exact quantity of elements/callbacks that will be generated (Or at least the maximum amount in this case). Forcing me to constrain the user to a limited number of options.

I know for know it’s not possible to create callbacks at runtime, so I guess I’ll have to wait for an update : )

In Dash, all of the callback functions and decorators need to be defined up-front (before the app starts). This means that you must generate callbacks for every unique set of input components that could be present on the page.

This, by the way, is confusing to me. How am I getting a dash.exceptions.DuplicateCallbackOutput (when the app has already started), as explained in the original post if it’s not possible to create callbacks after the app has started? @chriddyp

Thanks, looking forward to any updates on this!

2 Likes

Here is a my bicycle for table, where i have ‘load more’ and ‘add’ options:

def setup_callbacks_for_rows_adding(self):
    inputs = []
    if self.can_add_rows:
        inputs.append(Input(self.add_button_id(), 'n_clicks'))
    if self.display_rows_count != None:
        inputs.append(Input(self.load_more_button_id(), 'n_clicks'))

    @app.callback(
        [
            Output({'type': 'table-data-cells-body', 'component_id': self.id, 'udid': self.id}, 'children'),
            Output(self.adding_rows_id(), 'children'),
        ],
        inputs,
        [State(self.adding_rows_id(), 'children')]
    )

    def on_add_or_load_row_button_clicked(*args):
        ctx = dash.callback_context
        if not ctx.triggered:
            raise PreventUpdate('Cancel the callback')

        trigger_id_raw = dash.callback_context.triggered[0]['prop_id']
        id_end_location = trigger_id_raw.find('.')
        trigger_id = trigger_id_raw[:id_end_location]

        adding_rows = args[-1]

        if trigger_id == str(self.add_button_id()):

            input_result = [util.generate_udid(),]
            if self.__has_checkmarks():
                input_result.append(True)

            parse_res = self.parse_data_from_adding_rows(adding_rows=adding_rows)
            input_result += parse_res

            self.add_data_row(data_list=input_result)

            adding_cells = self.adding_cells()
        else:
            self.display_rows_count = None
            adding_cells = adding_rows

        data_rows = self.data_cells()

        return data_rows, adding_cells