How to use dynamically generated content as input to display plots in Plotly Dash?

Say I have 3 datasets with different column names. I want to make a plotly dash app where based on which dataset the user selects, it should display the column names in text areas (horizontal strips, sort of like buttons, but not clickable). So far, I’m able to do this, and here’s the code for that:

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

data1 = {'Col1':['A', 'B', 'C', 'D', 'E'],
        'Col2':list(np.random.randn(5))}
data2 = {'Col1':['F', 'G', 'H', 'I', 'J'],
        'Col2':list(np.random.randn(5))}
data3 = {'Col1':['K', 'L', 'M', 'N', 'O'],
        'Col2':list(np.random.randn(5))}

df1 = pd.DataFrame(data1, columns=['Col1', 'Col2'])
df2 = pd.DataFrame(data2, columns=['Col1', 'Col2'])
df3 = pd.DataFrame(data3, columns=['Col1', 'Col2'])


app = dash.Dash()

app.layout = html.Div([
        html.Div([dcc.Dropdown(
                 id='dropdown',
                 options=[{'label': 'Option 1', 'value': 1},
                          {'label': 'Option 2', 'value': 2},
                          {'label': 'Option 3', 'value': 3}],
                 placeholder='Select the dataset...',
                 )]),

        html.Div(id='output')
    ])


@app.callback(
    dash.dependencies.Output('output', 'children'),
    [dash.dependencies.Input('dropdown', 'value')])
def update_output(selected):
    if selected==1:
        df = df1
    elif selected==2:
        df = df2
    elif selected==3:
        df = df3

    columns = df.Col1.values

    divs=[]
    for i in range(len(columns)):
        divs.append(
            dcc.Graph(
                figure=go.Figure(
                    data=[go.Bar(
                                x=[10],
                                y=[columns[i]],
                                orientation = 'h',
                                text=['Column number {}: {}'.format(i+1, columns[i])],
                                textposition = 'inside',
                                marker = dict(color = 'rgb(158,202,225)'),
                            )
                        ],
                    layout=go.Layout(
                        xaxis=dict(showticklabels=False, fixedrange=True),
                        yaxis=dict(showticklabels=False, fixedrange=True),
                    )
                ),
                hoverData='',
                style={'width': 400, 'height':250, 'padding': 1},
                id='dynamic-text-area-'+format(i)
            ),
        )

        divs.append(html.Div(style={'height': '1'}))

    return divs

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

enter image description here

Now, I want to add another functionality that whenever the user hovers over any of these column names and a pie chart should pop up showing some info (I’ll use some simple dummy info in my code below for demonstration purposes). So I added a div in the app layout called pie-chart-div and a callback inside the loop with the dynamically created text area figure IDs as input to output a pie chart, like so:

for i in range(len(columns)):
    '''
    ###
    The rest of the stuff written above
    ###
    '''
    @app.callback(dash.dependencies.Output('pie-chart-div', 'children'),
              [dash.dependencies.Input('dropdown', 'value'),
               dash.dependencies.Input('dynamic-text-area-{}'.format(i), 'hoverData')])
        def update_graph(df, hoverData):
            values = [df[df.Col1==hoverData].Col2.iloc[0], sum(df.Col2)]
            labels = ['Selected column','All other columns']
            trace = go.Pie(values=values, labels = labels)
            fig = dcc.Graph(
                id='graph',
                figure={
                    'data': [trace],
                    'layout': {
                        'height': 400,
                        'width': 400,
                        'showlegend': False
                    }
                }
            )
            return fig

The problem is, this gives an error saying that it cannot the figure ID in the app layout. And that’s because dash cannot handle dynamic figure creation, as mentioned here. Here’s the full error:

  File "C:\Users\h473\Desktop\example_app.py", line 82, in update_output
    dash.dependencies.Input('dynamic-text-area-{}'.format(i), 'hoverData')])
  File "C:\Users\h473\AppData\Local\Programs\Python\Python35\lib\site-packages\dash\dash.py", line 827, in callback
    self._validate_callback(output, inputs, state, events)
  File "C:\Users\h473\AppData\Local\Programs\Python\Python35\lib\site-packages\dash\dash.py", line 631, in _validate_callback
    ).replace('    ', ''))
dash.exceptions.NonExistantIdException:
Attempting to assign a callback to the
component with the id "dynamic-text-area-0" but no
components with id "dynamic-text-area-0" exist in the
app's layout.


Here is a list of IDs in layout:
['dropdown', 'output', 'pie-chart-div']


If you are assigning callbacks to components
that are generated by other callbacks
(and therefore not in the initial layout), then
you can suppress this exception by setting
`app.config['suppress_callback_exceptions']=True`.

So, I tried putting in the line:

app.config['suppress_callback_exceptions']=True

but this gives another error saying that pie-chart-div has already been used and cannot be reused:

  File "C:\Users\h473\AppData\Local\Programs\Python\Python35\lib\site-packages\flask\app.py", line 2309, in __call__
    return self.wsgi_app(environ, start_response)
  File "C:\Users\h473\AppData\Local\Programs\Python\Python35\lib\site-packages\flask\app.py", line 2295, in wsgi_app
    response = self.handle_exception(e)
  File "C:\Users\h473\AppData\Local\Programs\Python\Python35\lib\site-packages\flask\app.py", line 1741, in handle_exception
    reraise(exc_type, exc_value, tb)
  File "C:\Users\h473\AppData\Local\Programs\Python\Python35\lib\site-packages\flask\_compat.py", line 35, in reraise
    raise value
  File "C:\Users\h473\AppData\Local\Programs\Python\Python35\lib\site-packages\flask\app.py", line 2292, in wsgi_app
    response = self.full_dispatch_request()
  File "C:\Users\h473\AppData\Local\Programs\Python\Python35\lib\site-packages\flask\app.py", line 1815, in full_dispatch_request
    rv = self.handle_user_exception(e)
  File "C:\Users\h473\AppData\Local\Programs\Python\Python35\lib\site-packages\flask\app.py", line 1718, in handle_user_exception
    reraise(exc_type, exc_value, tb)
  File "C:\Users\h473\AppData\Local\Programs\Python\Python35\lib\site-packages\flask\_compat.py", line 35, in reraise
    raise value
  File "C:\Users\h473\AppData\Local\Programs\Python\Python35\lib\site-packages\flask\app.py", line 1813, in full_dispatch_request
    rv = self.dispatch_request()
  File "C:\Users\h473\AppData\Local\Programs\Python\Python35\lib\site-packages\flask\app.py", line 1799, in dispatch_request
    return self.view_functions[rule.endpoint](**req.view_args)
  File "C:\Users\h473\AppData\Local\Programs\Python\Python35\lib\site-packages\dash\dash.py", line 911, in dispatch
    return self.callback_map[target_id]['callback'](*args)
  File "C:\Users\h473\AppData\Local\Programs\Python\Python35\lib\site-packages\dash\dash.py", line 851, in add_context
    output_value = func(*args, **kwargs)
  File "C:\Users\h473\Desktop\NPS SHAP Dashboard\ex.py", line 902, in update_output
    dash.dependencies.Input('dynamic-text-area-{}'.format(i), 'hoverData')])
  File "C:\Users\h473\AppData\Local\Programs\Python\Python35\lib\site-packages\dash\dash.py", line 827, in callback
    self._validate_callback(output, inputs, state, events)
  File "C:\Users\h473\AppData\Local\Programs\Python\Python35\lib\site-packages\dash\dash.py", line 708, in _validate_callback
    output.component_property).replace('    ', ''))
dash.exceptions.CantHaveMultipleOutputs:
You have already assigned a callback to the output
with ID "pie-chart-div" and property "children". An output can only have
a single callback function. Try combining your inputs and
callback functions together into one function. 

So, how do I fix this and make it work?

Here’s my full code:

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

data1 = {'Col1':['A', 'B', 'C', 'D', 'E'],
        'Col2':list(np.random.randn(5))}
data2 = {'Col1':['F', 'G', 'H', 'I', 'J'],
        'Col2':list(np.random.randn(5))}
data3 = {'Col1':['K', 'L', 'M', 'N', 'O'],
        'Col2':list(np.random.randn(5))}

df1 = pd.DataFrame(data1, columns=['Col1', 'Col2'])
df2 = pd.DataFrame(data2, columns=['Col1', 'Col2'])
df3 = pd.DataFrame(data3, columns=['Col1', 'Col2'])


app = dash.Dash()

app.layout = html.Div([
        html.Div([dcc.Dropdown(
                 id='dropdown',
                 options=[{'label': 'Dataset 1', 'value': 1},
                          {'label': 'Dataset 2', 'value': 2},
                          {'label': 'Dataset 3', 'value': 3}],
                 placeholder='Select the dataset...',
                 )]),

        html.Div(id='output'),

        html.Div(id='pie-chart-div')
    ])

@app.callback(
    dash.dependencies.Output('output', 'children'),
    [dash.dependencies.Input('dropdown', 'value')])
def update_output(selected):
    if selected==1:
        df = df1
    elif selected==2:
        df = df2
    elif selected==3:
        df = df3

    columns = df.Col1.values

    divs=[]
    for i in range(len(columns)):
        divs.append(
            dcc.Graph(
                figure=go.Figure(
                    data=[go.Bar(
                                x=[10],
                                y=[columns[i]],
                                orientation = 'h',
                                text=['Column number {}: {}'.format(i+1, columns[i])],
                                textposition = 'inside',
                                marker = dict(color = 'rgb(158,202,225)'),
                            )
                        ],
                    layout=go.Layout(
                        xaxis=dict(showticklabels=False, fixedrange=True),
                        yaxis=dict(showticklabels=False, fixedrange=True),
                    )
                ),
                hoverData='',
                style={'width': 400, 'height':250, 'padding': 1},
                id='dynamic-text-area-{}'.format(i)
            ),
        )

        divs.append(html.Div(style={'height': '1'}))




        @app.callback(dash.dependencies.Output('pie-chart-div', 'children'),
              [dash.dependencies.Input('dropdown', 'value'),
               dash.dependencies.Input('dynamic-text-area-{}'.format(i), 'hoverData')])
        def update_graph(df, hoverData):
            values = [df[df.Col1==hoverData].Col2.iloc[0], sum(df.Col2)]
            labels = ['Selected column','All other columns']
            trace = go.Pie(values=values, labels = labels)
            fig = dcc.Graph(
                id='graph',
                figure={
                    'data': [trace],
                    'layout': {
                        'height': 400,
                        'width': 400,
                        'showlegend': False
                    }
                }
            )
            return fig



    return divs



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