Black Lives Matter. Please consider donating to Black Girls Code today.
Dash HoloViews is now available! Check out the docs.

Can same component id reside in input/state and output simultaneously?

I have two functions:

@app.callback(
Output(‘click-register’, ‘children’),
[Input(‘planes-graph’, ‘clickData’)])
def click_register_function(clickData):

@app.callback(
Output(‘planes-graph’, ‘figure’),
[Input(‘planes-list’, ‘value’),
Input(‘fill-region-button’, ‘n_clicks’)],
[State(‘material-dropdown’, ‘value’),
State(‘click-register’, ‘children’)]
)
def fill_region(planes, n_clicks, selected_material, click_register):

where I used to have one:

@app.callback(
Output(‘planes-graph’, ‘figure’),
[Input(‘planes-list’, ‘value’),
Input(‘fill-region-button’, ‘n_clicks’)],
[State(‘material-dropdown’, ‘value’),
State(‘planes-graph’, ‘clickData’)]
)
def fill_region(planes, n_clicks, selected_material, clickData):

However, the 1 function solution didn’t seem to give me anything of value and nothing happened as far as I could tell. Using the 2 function solution I was able to see that something was happening, though the output wasn’t exactly what I wanted (output of the 2nd function changes the hoverData in figure; once fill-region-button is clicked, instead of altering hoverData, it all just disappears. So I want to know what, if any, flaws are associated with either solution and why I might be running into problems. If there is no reason either of these shouldn’t work, then perhaps a bug should be investigated. Thanks

In principle, yes, you can have component IDs shared across multiple callbacks as Input and State. If you can create a small, minimal, reproducable example that displays the issue without any extra code then we can dig in further.

Thank you so much for responding, here is the minimal code example I have. The hover data doesn’t disappear like it does in my full code but if we can fix the problem here I am confident I can transfer the success.

Procedure:

  1. Click Add Material Button

  2. Enter Name and Density (Temperature not Required)

  3. Select material from Dropdown

  4. Click Create Cell Button

  5. Enter list of radial planes e.g. “.5, .4, …”

  6. Notice Hover Data

  7. Click on a region

  8. Click Fill Region Button

  9. Notice no change in Hover Data

     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([
             # TODO: figure out why html.A components not working/updating and put all of them in button-activated Div
             dcc.Dropdown(id='material-dropdown'),
             html.Button('Add Material', id='add-material-button', n_clicks=0),
             html.Div(id='material-options-container'),
             html.A(id='material-message-update'),
    
             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-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):
    
         if n_clicks > 0:
             options = html.Div([dcc.Graph(id='planes-graph'),
                                 # TODO: decide whether different material dropdown should be added for geometry section
                                 dcc.Input(id='planes-list', placeholder='Enter list of radial planes (comma separated)',
                                           type="text"),
                                 html.Button('Fill Region', id='fill-region-button', n_clicks=0),
                                 html.Br(),
                                 ])
             return options
    
    
     @app.callback(
         Output('click-register', 'children'),
         [Input('planes-graph', 'clickData')])
     def click_register_function(clickData):
         region = 0
         click_x = 0
         click_y = 0
         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']
             message = 'Region: {}, x: {}, y: {}'.format(region, click_x, click_y)
             return message
    
    
     # Fill Region
     @app.callback(
         Output('planes-graph', 'figure'),       # TODO: Decide whether hover info change is enough to inform user
         [Input('planes-list', 'value'),
          Input('fill-region-button', 'n_clicks')],
         [State('material-dropdown', 'value'),
          State('click-register', 'children')]
     )
     def fill_region(planes, n_clicks, selected_material, click_register):
         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 click_register is not None:
             click_x = click_register[1]
             click_y = click_register[2]
    
             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
    
     if __name__ == '__main__':
         app.run_server()

Any help you give is appreciated. This is driving me crazy.

So what is weird is I am getting some weird results for a similar case, consider:

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

app.layout = html.Div([
html.A(id='selected-cells'),])


# Get Selected Cells
@app.callback(
    Output('selected-cells', 'children'),
    [Input('assembly-graph', 'clickData')], # I can supply this later if needed
    [State('selected-cells', 'children')]
)
def select_assembly_cells(clickData, cell_coords):
    click_x = click_y = 0
    if clickData is not None:
        if 'points' in clickData:
            point = clickData['points'][0]
            # if 'text' in point:
            #     cell_type = 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 cell_coords is None:
            cell_coords = []

        if cell_coords is not None:
            text = '[{}, {}], '.format(click_x, click_y)

            if text not in cell_coords:
                cell_coords.append(text)

            if text in cell_coords:
                cell_coords.remove(text)

    return cell_coords

It turns out that I can append/extend clickData to selected-cells but I cannot remove. In fact if the
if text in cell_coords:
cell_coords.remove(text)

block is present, then selected-cells wont even display for me.

I’m not entirely sure if I could reproduce the precise problem you’re seeing, but I can see a few issues.

I’d recommend testing your app with the developer tools open, as it can reveal bugs that you might not be aware of otherwise. For example, I think you need to return an empty list for the callback invoke_material_options in the case where n_clicks = 0, otherwise the interface gets an error trying to treat null as an object that contains dropdown options.

There was also a few Internal Server 500 errors resulting from Python Exceptions (which are shown in the terminal that you started the Dash app in).

One is that I think you might also need to return an empty dictionary for the fill_region callback when the planes parameter is None.

The other looks like your click_x and click_y values are not being extracted correctly in fill_region, as they’re pulling out single characters rather than numeric values. The click_register value that you’re extracting this data from is now just a string which is populated by click_register_function. It looks like you might want to be using clickData of planes-graph directly as a State rather than saving it as an intermediate value and extracting it back out of the DOM.

1 Like

@nedned Thank you for your response. I didn’t like using my click-register either, but I had left it as a diagnostic tool.

If you cant reproduce the precise problem, can I ask what you see if you run the code? Not the hoverData not disappear on you?:

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([
     # TODO: figure out why html.A components not working/updating and put all of them in button-activated Div
     dcc.Dropdown(id='material-dropdown'),
     html.Button('Add Material', id='add-material-button', n_clicks=0),
     html.Div(id='material-options-container'),
     html.A(id='material-message-update'),

     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:
     return []
 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-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):

 if n_clicks > 0:
     options = html.Div([dcc.Graph(id='planes-graph'),
                         # TODO: decide whether different material dropdown should be added for geometry section
                         dcc.Input(id='planes-list', placeholder='Enter list of radial planes (comma separated)',
                                   type="text"),
                         html.Button('Fill Region', id='fill-region-button', n_clicks=0),
                         html.Br(),
                         ])
     return options


# Fill Region
@app.callback(
 Output('planes-graph', 'figure'),
 [Input('planes-list', 'value'),
  Input('fill-region-button', 'n_clicks')],
 [State('material-dropdown', 'value'),
  State('planes-graph', 'clickData')]
)
def fill_region(planes, n_clicks, selected_material, clickData):
 if planes is None:
    return {}
 else:
     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

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

Also the hover data now disappears like it does in my full code for some reason.

Are you actually trying to use hoverData? I don’t see any use of it in your code, just clickData in the fill_region callback. Also, in that callback, perhaps you could use an Input rather than a State for the clickData, ie Input('planes-graph', 'clickData') and get rid of Input('fill-region-button', 'n_clicks'). I would think that using an Input would give you the same triggering on click behaviour?

I am not “using” the hover data so much as I am trying to change it when I forward the click data to the figure here:

And I could accomplish what you are saying with an input but I feel that for this application’s purpose, it makes more sense for the user to “fill” the region rather than have dash do it automatically (which I feel might leave the user temporarily confused when the regions are automatically ‘filling’ up)

Basically I am using the clickData to change the hoverData in the figure in the 'text' attribute of 'figure'. Should I change my callback to have the id set for the figure and the property as the hoverData? That actually sounds like it makes more sense. Still not sure why what I have wouldn’t work though

If you want something to happen when you hover, then you need to apply a callback to hoverData, so yeah i think you need to make that change.

Using an Input targeting clickData, will still only trigger when the user clicks on the chart, as the property will only change (triggering the callback) when the user clicks. So I think you’d get the behaviour you’re after with that approach.

So if the component id and property are hoverData and figure in the Output of the callback, I would just need to return the text array in the function that corresponds to that callback? That is, can I be sure that the text will assume itself correctly under the correct attribute, ‘text’?

Sorry, I’m not quite sure what you mean. hoverData will be an Input, but I don’t know which callback/Output it should be applied to since I’m not sure what you’re application should be doing.

Perhaps you could explain exactly what you want to happen when a user hovers over the chart?

If you notice here:

if planes is None:
    return {}
 else:
     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)

I am simply creating the hover text based on the planes-list input and nothing else. What this does is it creates a heatmap with several concentric circles that tells me what “region” the clickData/hoverData is in. The “region” refers to the area within the innermost circle, the areas between concentric circles, and the area outside the outermost circle (increasing in integer value as you go outward).

If you notice here:

 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

I am taking the x and y coordinates of the clickData to determine what region has been selected. From there, the function uses the string ‘value’ provided by the ‘material-dropdown’ and ‘n_clicks’ from the ‘fill-region-button’ to go through and replace the elements in the text list (which is the hoverData [text attribute of heatmap]) that match the selected region, with the string given by the ‘material-dropdown’. Thus the hoverData is not an input, but rather an output.

Actually, just tried to implement what we spoke of and, in fact, it is not possible to change the hoverData as an output in the callback (for my application) because the function subject to the callback would only have access to the data that hoverData provides (an x point, y point, and single text string), thus I could not filter through the entire text list to change all the elements I need.

In my code, everything works as its supposed to beside the mysterious disappearance of the hoverData. If I were to print cell_hover after clicking the ‘fill-region-button’, I get a non-empty list of the text that should be passed to the ‘text’ attribute of the heatmap figure. Instead of changing the hoverData to depict this change, the text in the hoverData simply disappears while x and y data remains. I don’t see how this shouldn’t work because the component property being changed is the entire figure thus any attribute under figure should change successfully, this is why I think it may be a bug of some sort.

@nedned I appreciate your time VERY much in helping me with this problem and I hope that I have identified potential issue

@nedned See here for very minimal example:

In this minimal example, instead of trying to change the hover data, I am changing the data attribute of the figure which is reflected in the color of the heatmap. The same principle is utilized as above and the same result (or lack of) is observed. This should be easier to debug. Let me know if this helps.