Graphs Duplicated When creating callbacks in a loop with closures

Hi Dashers

I have an odd issue where I create callbacks dynamically in a loop, similar to the “Store Clicks Example” in the documentation. What I have is a bunch of graph container divs, which are populated from a callback. The final graph that is created is always duplicated to all the graph containers, instead of each container rendering its own graph. Its hard to explain so I have stripped down the code to a bare minimum and pasted it here. After clicking submit I expect to see three different graphs, one in each container, however all three containers contain the same graph.

Any help will be much appreciated.

# -*- coding: utf-8 -*-
from collections import OrderedDict
import json
import dash

import dash_table
import dash_html_components as html
import dash_core_components as dcc
from dash.dependencies import Input, Output, State, ALL
import plotly.graph_objects as go


app = dash.Dash(__name__)
server = app.server # For running under Gunicorn


MULO_GEOS = (
    ('WALMART', 'Walmart'),
    ('TARGET', 'Target'),
    ('KROGER', 'Kroger'),
)


submit_button = html.Button(
        id='submit-button',
        children='Submit',
        n_clicks=0,
)




model_output = [
    html.Div('Model Output Placeholder', id='model-output')
]


output_table = html.Table(
    html.Tbody([
        html.Tr([
            html.Th('Total Universe'),
            html.Th('Channels'),
            html.Th('Units/Period'),
            html.Th('Accounts'),
            html.Th('Units/Period')
        ]),
        html.Tr([
            html.Td('0.00', id='total-universe'),
            html.Td('TOTAL MULO'),
            html.Td('0.00', id='total-mulo'),
            html.Td('Walmart'),
            html.Td('0.00', id='walmart-average')
        ]),
        html.Tr([
            html.Td(),
            html.Td(),
            html.Td(),
            html.Td('Target'),
            html.Td('0.00', id='target-average')
        ]),
        html.Tr([
            html.Td(),
            html.Td(),
            html.Td(),
            html.Td('Kroger'),
            html.Td('0.00', id='kroger-average')
        ]),
    ])
)


def make_graph_div(geo):
    '''
    Return a graph container div
    for the given geo.
    '''
    g, name = geo
    id_ = name.lower()
    id_ = f'{id_}-graph-container'
    div = html.Div(
        id_,
        id=id_
    )
    return div


def make_graph_divs(geos):
    divs = []
    for geo in geos:
        div = make_graph_div(geo)
        divs.append(div)
    return divs


app.layout = html.Div(
    [
        submit_button,
        html.Div(output_table), # Probably this is where we will put the output table
    ] + make_graph_divs(MULO_GEOS) + [
        # Hidden div inside the app that stores the intermediate value
        # See example 1 from https://dash.plotly.com/sharing-data-between-callbacks
        # We will use this to store the product_dict which can then be used
        # by all the graphs...
        html.Div(
            id='product-dict', style={'display': 'none'}
        )
    ]
)


def build_graph(predictions, title):
    fig = go.Figure()
    x = list(range(len(predictions)))
    fig.add_trace(go.Scatter(x=x, y=predictions))
    fig.update_layout(
        title=title,
        xaxis_title='Month',
        yaxis_title='Units per Four Week Period'
    )
    graph = dcc.Graph(
        id='example-graph',
        figure=fig
    )
    return graph


@app.callback(
    Output('product-dict', 'children'),
    Input('submit-button', 'n_clicks'),
    State({'type': 'attribute', 'name': ALL}, 'id'),
    State({'type': 'attribute', 'name': ALL}, 'value'),
    State({'type': 'diet-suitability', 'name': ALL}, 'id'),
    State({'type': 'diet-suitability', 'name': ALL}, 'value'),
    State({'type': 'certification', 'name': ALL}, 'id'),
    State({'type': 'certification', 'name': ALL}, 'value'),
    prevent_initial_call=True)
def convert_inputs(n_clicks, attribute_ids, attribute_values,
                          diet_suitability_ids,
                          diet_suitability_values,
                          certification_ids,
                          certification_values):
    print('Started Conversion.')
    product_dict = dict()
    print('Done Conversion.')
    return json.dumps(product_dict)


for i, (geo, short_name) in enumerate(MULO_GEOS):
    output_id = short_name.lower()
    output_id = f'{output_id}-graph-container'
    average_id = short_name.lower() + '-average'

    print(f'Creating callback for {output_id}')

    @app.callback(
        Output(output_id, 'children'),
        Output(average_id, 'children'),
        Input('product-dict', 'children'),
        prevent_initial_call=True)
    def generate_graph(product_dict):
        print(f'Started {short_name}.')
        product_dict = json.loads(product_dict)
        g = geo.lower()
        #predictions = predict_series(product_dict, g)
        predictions = [i] * 12
        avg = sum(predictions) / 12
        print(geo, avg)
        graph = build_graph(predictions, geo)
        print(f'Done {short_name}')
        return graph, avg




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

The problem is with how the callback in the loop is defined. This in turn is due to how function closures work (in Python, but similarly in many other languages including JS). The callbacks you have defined close over the variables i, geo, short_name, but only preserve the reference to the variable, not its value at the time the callback function is defined (by the def statement inside the loop). Each callback refers to those variables, and the values of those variables are those they received in the final loop iteration. So you get the “same graph repeated” effect you noted in your comment.

The solution to this is to close over the values of the variables, and you do it by defining a function which takes the variables as arguments and returns a callback function using those arguments. In your case:

def make_generate_graph(i, geo, short_name):
    def generate_graph(product_dict):
         # your callback code here ...

   return generate_graph
        
for i, (geo, short_name) in enumerate(MULO_GEOS):
    app.callback(...)(make_generate_graph(i, geo, short_name))

Note that app.callback is not invoked as a decorator, but instead directly as a function call. That is how you can pass it the result of make_generate_graph. I’ve done this in my own code and I know it works, but have not run this with your code.