Callback Decorator in Loop

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)