Black Lives Matter. Please consider donating to Black Girls Code today.

Callback Decorator in Loop

I can’t seem to get the callback decorator to work in a loop for my problem:


(Making this post in case the issue is more general (title))

I looked into this post for my problem:

However, it doesn’t seem to work in my case. I posted my code in a stack overflow post: https://stackoverflow.com/questions/49419718/plotly-dash-callback-decorator-in-loop-not-possible

But basics resembles:

app = dash.Dash()
app.config['suppress_callback_exceptions'] = True

app.layout = html.Div([
html.Button('Create Cell', id='cell-geometry-button', n_clicks=0),
html.Div(id='cell-geometry-config-container'),
html.A(id='click-register'),
])

num_clicks = 0

# Initiate cell geometry config with button
@app.callback(
    Output('cell-geometry-config-container', 'children'),
    [Input('cell-geometry-button', 'n_clicks')],)
def invoke_cell_geometry_options(n_clicks):
    geometry_ui_list = []
    global num_clicks
    num_clicks = n_clicks
    for i in range(n_clicks):
        graph_id = 'cell-graph-{}'.format(i)
        planes_list_id = 'planes-list-{}'.format(i)
        button_id = 'fill-region-button-{}'.format(i)
        click_register_id = 'click-register-{}'.format(i)

        geometry_ui_list.extend([dcc.Graph(id=graph_id),
                                 dcc.Input(id=planes_list_id, placeholder='Enter list of radial planes (comma separated)',
                                           type="text"),
                                 html.Button('Fill Region', id=button_id, n_clicks=0),
                                 html.A(id=click_register_id),
                                 html.Br(),
                        ])

    options = html.Div(geometry_ui_list)
    return options

@app.callback(
    Output('cell-geometry-config-container', 'children'),
    [Input('cell-geometry-button', 'n_clicks')],)
def invoke_cell_geometry_options(n_clicks):
    geometry_ui_list = []
    global num_clicks
    num_clicks = n_clicks
    for i in range(n_clicks):
        graph_id = 'cell-graph-{}'.format(i)
        planes_list_id = 'planes-list-{}'.format(i)
        button_id = 'fill-region-button-{}'.format(i)
        click_register_id = 'click-register-{}'.format(i)

        geometry_ui_list.extend([dcc.Graph(id=graph_id),
                                 dcc.Input(id=planes_list_id, placeholder='Enter list of radial planes (comma separated)',
                                           type="text"),
                                 html.Button('Fill Region', id=button_id, n_clicks=0),
                                 html.A(id=click_register_id),
                                 html.Br(),
                        ])

    options = html.Div(geometry_ui_list)
    return options

for val in range(num_clicks):
    @app.callback(
        Output('click-register-{}'.format(val), 'children'),
        [Input('cell-graph-{}'.format(val), 'clickData')])
    def click_register_function(clickData):
         ...
         return [region, click_x, click_y]


    # Fill Region
    @app.callback(
        Output('cell-graph-{}'.format(val), 'figure'),
        [Input('planes-list-{}'.format(val), 'value'),
         Input('fill-region-button-{}'.format(val), 'n_clicks')],
        [State('material-dropdown', 'value'),
         State('click-register-{}'.format(val), 'children')]
    )
    def fill_region(planes, n_clicks, selected_material, click_register):
        ...
        return figure

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

Is there something I am missing?

I think this was posted in another topic, but I’ll link the reference here as well: For some more info on dynamic callbacks, see Dynamic Controls and Dynamic Output Components

Also, it’s not safe to use global variables. Explanation here: https://dash.plot.ly/sharing-data-between-callbacks as well as in some other threads in the forum like Global variables sharing with mutex

Thank you for the response, that first link you sent looks like it has the answer I’ve been searching for. Will post an update if I figure it out and it looks like I wouldn’t even need the global variable in that case for this part of my code anyway (there is another part but I will post my question in the other thread).

I’m afraid that my case is too different from the example in Dynamic Controls and Dynamic Output Components for me to understand an adaptation.

  1. I think its safe to say that my output container is also my control container
  2. My generate_output_callback function doesn’t take any arguments
  3. I am taking a single component property (n_clicks) instead of 2

Below is my attempt of implementation. I noticed that if I change n_clicks in
html.Button('Create Cell', id='cell-geometry-button', n_clicks=0),

then it works for as many plots as n_clicks equals. What am I doing wrong still?

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

app = dash.Dash()
app.config['suppress_callback_exceptions'] = True

#######################################################################################################################

app.layout = html.Div([
    html.Div([
        dcc.Dropdown(id='material-dropdown'),
        html.Button('Add Material', id='add-material-button', n_clicks=0),
        html.Div(id='material-options-container'),

        html.Button('Create Cell', id='cell-geometry-button', n_clicks=0),
        html.Div(id='cell-geometry-config-container'),
        html.A(id='click-register'),
    ]),

])

#######################################################################################################################
# Materials Interface
# Keep track of material names
materials_list = []


# Invoke material options
@app.callback(
    Output('material-options-container', 'children'),
    [Input('add-material-button', 'n_clicks')],)
def invoke_material_options(n_clicks):
    if n_clicks > 0:
        options = html.Div([dcc.Input(id='material-name', placeholder='Enter Material Name'),
                            dcc.Input(id='material-density', placeholder='Enter Material Density', type='number'),
                            dcc.Input(id='material-temperature', placeholder='Enter Material Temperature', type='number'),
                            html.Button('Submit Material', id='submit-material-button', n_clicks=0),
                            html.Br()
                            ])
        return options


# Submit material to model
@app.callback(
    # Output('material-message-update', 'children'),
    Output('material-dropdown', 'options'),
    [Input('submit-material-button', 'n_clicks')],
    [State('material-name', 'value'),
     State('material-density', 'value'),
     State('material-temperature', 'value'),
     State('material-dropdown', 'options')])
def submit_material(n_clicks, material_name, material_density, material_temperature, material_options):
    if n_clicks > 0:
        if material_options is not None:
            material_options.append({'label': material_name, 'value': len(material_options)+1})
            materials_list.append(material_name)
        if material_options is None:
            material_options = [{'label': material_name, 'value': 0}]
            materials_list.append(material_name)
        n_clicks = 0
        return material_options

#######################################################################################################################
# Geometry Interface


# Initiate cell geometry config with button
@app.callback(
    Output('cell-geometry-config-container', 'children'),
    [Input('cell-geometry-button', 'n_clicks')],)
def invoke_cell_geometry_options(n_clicks):
    # TODO: Below works but must find way to implement fill_region function on arbitrary number of graphs
    geometry_ui_list = []
    for i in range(n_clicks):
        graph_id = 'cell-graph-{}'.format(i)
        planes_list_id = 'planes-list-{}'.format(i)
        button_id = 'fill-region-button-{}'.format(i)

        geometry_ui_list.extend([dcc.Graph(id=graph_id),
                                 dcc.Input(id=planes_list_id, value='.4, .43', placeholder='Enter list of radial planes (comma separated)',
                                           type="text"),
                                 html.Button('Fill Region', id=button_id, n_clicks=0),
                                 html.Br(),
                        ])

    options = html.Div(geometry_ui_list)
    return options


def generate_output_callbacks():
    def fill_region(planes, n_clicks, selected_material, clickData):
        planes = [float(plane) for plane in planes.split(',')]
        planes.sort()

        edge = planes[-1]
        x = np.linspace(-edge, edge, 250)
        y = np.linspace(-edge, edge, 250)

        regions = []
        cell_hover = []
        # Normal Display
        for i in x:
            row = []
            text_row = []
            for j in y:

                if np.sqrt(i ** 2 + j ** 2) < planes[0]:
                    row.append(7)  # <- Arbitrary number to adjust color
                    text_row.append('Region 1')

                if np.sqrt(i ** 2 + j ** 2) > planes[-1]:
                    row.append(5)  # <- Arbitrary number to adjust color
                    text_row.append('Region {}'.format(len(planes) + 1))

                for k in range(len(planes) - 1):
                    if planes[k] < np.sqrt(i ** 2 + j ** 2) < planes[k + 1]:
                        row.append(k * 3)  # <- Arbitrary number to adjust color
                        text_row.append('Region {}'.format(k + 2))
            regions.append(row)
            cell_hover.append(text_row)

        ######################################################
        # Initialize region
        if clickData is not None:
            if 'points' in clickData:
                point = clickData['points'][0]
                if 'text' in point:
                    region = int(re.search(r'\d+', point['text']).group())
                if 'x' in point:
                    click_x = point['x']
                if 'y' in point:
                    click_y = point['y']

            if n_clicks > 0:
                new_hover = []

                # Change graph on Click # TODO: Figure out why new text wont show up
                if 0 < np.sqrt(click_x ** 2 + click_y ** 2) < planes[0]:
                    for row_ in cell_hover:
                        for text in row_:
                            new_hover.append(
                                text.replace('Region 1', '{} Region'.format(materials_list[selected_material])))

                if np.sqrt(click_x ** 2 + click_y ** 2) > planes[-1]:
                    for row_ in cell_hover:
                        for text in row_:
                            new_hover.append(text.replace('Region {}'.format(len(planes) + 1),
                                                          '{} Region'.format(materials_list[selected_material])))

                for k in range(len(planes) - 1):
                    if planes[k] < np.sqrt(click_x ** 2 + click_y ** 2) < planes[k + 1]:
                        for row_ in cell_hover:
                            for text in row_:
                                new_hover.append(text.replace('Region {}'.format(k + 2),
                                                              '{} Region'.format(materials_list[selected_material])))

                cell_hover = new_hover
                n_clicks = 0

        ######################################################

        heatmap = go.Heatmap(z=regions, x=x, y=y, hoverinfo='x+y+text', text=cell_hover, opacity=0.5, showscale=False)

        data = [heatmap]
        shapes = []

        for plane in planes:
            shape = {
                'type': 'circle',
                'x0': -plane,
                'y0': -plane,
                'x1': plane,
                'y1': plane,
                'line': {
                    'width': 4,
                },
                'opacity': 1
            }

            shapes.append(shape)

        layout = dict(title='Cell Region Depiction',
                      height=1000,
                      width=1000,
                      shapes=shapes)

        figure = dict(data=data, layout=layout)
        return figure

    return fill_region


for val in range(app.layout['cell-geometry-button'].n_clicks):
    app.callback(
        Output('cell-graph-{}'.format(val), 'figure'),
        [Input('planes-list-{}'.format(val), 'value'),
         Input('fill-region-button-{}'.format(val), 'n_clicks')],
        [State('material-dropdown', 'value'),
         State('cell-graph-{}'.format(val), 'clickData')]
    )(generate_output_callbacks())

##################################################


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

This isn’t possible in Dash because you can’t have dynamic callbacks. Everything needs to be upfront.
app.layout['cell-geometry-button'].n_clicks will not change throughout the lifecycle of the app and even if it did, you can’t register app.callback after the app has started.

So what would the workaround look like in my case?
So basically there is no way to render as many plot instances as there are clicks of the button (and still have access to their callback for output)? So you would specify them up-front and the user is limited to as many as you have specified?

Also, I’m not sure I understand why app.layout['cell-geometry-button'].n_clicks will not change? Is this different than if I did a callback and retrieved the value from some-element?:

@app.callback(
        Output('some-element, 'children'),
        [Input('cell-geometry-button', 'n_clicks'),

The workaround is to define possible combination of things upfront, what I explained in Dynamic Controls and Dynamic Output Components. If defining everything upfront isn’t possible, then it’s not possible to build that type of UI is not possible in pure Dash right now. Alternatively, you can build the UI in React and then make it a Dash plugin (https://plot.ly/dash/plugins). I’d like to add support for dynamic components and callbacks to Dash we’ll likely need a commercial sponsor to help fund that work (it’s a significant amount of work).

Yes, those two things are very different. Dash, like all web apps, works by having a stateless backend and keeping the per-user state is stored in the frontend (the browser). This allows Dash to handle serving multiple users concurrently and to run on multiple processes without having to keep every user’s session in backend memory or keep data synchronized across multiple processes. Callbacks work by asking the front-end (where all the data is stored) to pass the value down to the backend on demand: whenever the input changes. Data isn’t automatically synced up between the backend and frontend (that would be too much data to pass over the network), it’s only passed through callbacks.

Thank you so much for your help, looks like I will have to work with some limitations until you guys get that funding (I’m rooting for you).

Let me tell you, next, how much I love Plotly, both your products and service. I recommend you guys to anyone remotely interested in computing.
Apologies if I appear negligent in some things as everything I know is self-taught so there are some things that I still don’t understand.

Thanks again

1 Like