Dynamic Controls and Dynamic Output Components

Update! The limitations discussed in this topic are no longer, please see the new pattern-matching callbacks feature released in Dash 1.11.0: šŸ“£ Dash v1.11.0 Release - Introducing Pattern-Matching Callbacks - #3

Iā€™m creating this thread to discuss how dynamic UIs work in Dash. That is, how to generate dynamic input components that update dynamic output components. For example, selecting an item in a dropdown might generate 2 or 3 other dropdowns or sliders and the combination of those controls might update a graph.

In Dash, all of the callback functions and decorators need to be defined up-front (before the app starts). This means that you must generate callbacks for every unique set of input components that could be present on the page.

In Dash, the callback functionā€™s decoratorā€™s arenā€™t dynamic. Each set of input components can only update a single output component. So, for each unique set of input components that are generated, you must generate a unique output component as well.

I recommend creating 2 containers: one to hold the dynamic controls and the other to generate the dynamic output container.

Hereā€™s a simple example. In this example:

  • 2 static dropdowns generate unique input components with unique IDs in the control-container Div
  • These dropdowns also create a unique output component with a unique ID thatā€™s based off of their values.
  • Callbacks are generated for every unique input set and are bound to the corresponding dynamic output component.
import dash
from dash.dependencies import Input, Output
import dash_core_components as dcc
import dash_html_components as html
import itertools

app = dash.Dash()
app.layout = html.Div([
    dcc.Dropdown(
        id='datasource-1',
        options=[
            {'label': i, 'value': i} for i in ['A', 'B', 'C']
        ],
    ),
    dcc.Dropdown(
        id='datasource-2',
        options=[
            {'label': i, 'value': i} for i in ['X', 'Y', 'Z']
        ]
    ),
    html.Hr(),
    html.Div('Dynamic Controls'),
    html.Div(
        id='controls-container'
    ),
    html.Hr(),
    html.Div('Output'),
    html.Div(
        id='output-container'
    )
])

def generate_control_id(value):
    return 'Control {}'.format(value)

DYNAMIC_CONTROLS = {
    'A': dcc.Dropdown(
        id=generate_control_id('A'),
        options=[{'label': 'Dropdown A: {}'.format(i), 'value': i} for i in ['A', 'B', 'C']]
    ),
    'B': dcc.Dropdown(
        id=generate_control_id('B'),
        options=[{'label': 'Dropdown B: {}'.format(i), 'value': i} for i in ['A', 'B', 'C']]
    ),
    'C': dcc.Dropdown(
        id=generate_control_id('C'),
        options=[{'label': 'Dropdown C: {}'.format(i), 'value': i} for i in ['A', 'B', 'C']]
    ),

    'X': dcc.Dropdown(
        id=generate_control_id('X'),
        options=[{'label': 'Dropdown X: {}'.format(i), 'value': i} for i in ['A', 'B', 'C']]
    ),
    'Y': dcc.Dropdown(
        id=generate_control_id('Y'),
        options=[{'label': 'Dropdown Y: {}'.format(i), 'value': i} for i in ['A', 'B', 'C']]
    ),
    'Z': dcc.Dropdown(
        id=generate_control_id('Z'),
        options=[{'label': 'Dropdown Z: {}'.format(i), 'value': i} for i in ['A', 'B', 'C']]
    )
}

@app.callback(
    Output('controls-container', 'children'),
    [Input('datasource-1', 'value'),
     Input('datasource-2', 'value')])
def display_controls(datasource_1_value, datasource_2_value):
    # generate 2 dynamic controls based off of the datasource selections
    return html.Div([
        DYNAMIC_CONTROLS[datasource_1_value],
        DYNAMIC_CONTROLS[datasource_2_value],
    ])

def generate_output_id(value1, value2):
    return '{} {} container'.format(value1, value2)

@app.callback(
    Output('output-container', 'children'),
    [Input('datasource-1', 'value'),
     Input('datasource-2', 'value')])
def display_controls(datasource_1_value, datasource_2_value):
    # create a unique output container for each pair of dyanmic controls
    return html.Div(id=generate_output_id(
        datasource_1_value,
        datasource_2_value
    ))

def generate_output_callback(datasource_1_value, datasource_2_value):
    def output_callback(control_1_value, control_2_value):
        # This function can display different outputs depending on
        # the values of the dynamic controls
        return '''
            You have selected {} and {} which were
            generated from {} (datasource 1) and and {} (datasource 2)
        '''.format(
            control_1_value,
            control_2_value,
            datasource_1_value,
            datasource_2_value
        )
    return output_callback

app.config.supress_callback_exceptions = True

# create a callback for all possible combinations of dynamic controls
# each unique dynamic control pairing is linked to a dynamic output component
for value1, value2 in itertools.product(
        [o['value'] for o in app.layout['datasource-1'].options],
        [o['value'] for o in app.layout['datasource-2'].options]):
    app.callback(
        Output(generate_output_id(value1, value2), 'children'),
        [Input(generate_control_id(value1), 'value'),
         Input(generate_control_id(value2), 'value')])(
        generate_output_callback(value1, value2)
    )

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

7 Likes

I was fiddling with this to try and troubleshoot why Iā€™m running into issues with this attempt. Is there any reason you couldnā€™t do this instead of the static definition of DYNAMIC_CONTROLS?

def dynamic_control_maker(val):
    return dcc.Dropdown(
        id=generate_control_id(val),
        options=[{'label': 'Dropdown {}: {}'.format(val, i), 'value': i} for i in ['A', 'B', 'C']])

@app.callback(
    Output('controls-container', 'children'),
    [Input('datasource-1', 'value'),
     Input('datasource-2', 'value')])
def display_controls(datasource_1_value, datasource_2_value):
    # generate 2 dynamic controls based off of the datasource selections
    return html.Div([
        dynamic_control_maker(datasource_1_value),
        dynamic_control_maker(datasource_2_value)
    ])

I think itā€™s working identically to the original example. This was great to find, but Iā€™ll need to putz further as this shows what Iā€™m trying to do should work just fine. Thanks for putting it together!

Hi chriddyp,

Iā€™ve tried to use your example to build a dashboard that I need.
But Iā€™ve got an issue, because when i run the code Iā€™ve got no error message, the dropdown show the right list, but no result is shown after.

I dont understand what I did wrong (I think there a problem with the loop, but not sure)

Here my code :

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

path = ā€˜C:/ā€™
file = ā€˜stat2018.xlsxā€™
xl = pd.ExcelFile(path +file)
dfplayers = xl.parse(ā€˜Sheet1ā€™)

fullbacks = dfplayers[(dfplayers.primary_position == ā€˜Left Backā€™) ]
fullbacksname = sorted(fullbacks.name)
strickers = dfplayers[(dfplayers.primary_position == ā€˜Strikerā€™)]
strickersname = sorted(strickers.name)
forwards = dfplayers[(dfplayers.primary_position == ā€˜Right Attacking Midfielderā€™) ]
forwardsname = sorted(forwards.name)
centralmidf = dfplayers[(dfplayers.primary_position == ā€˜Right Centre-Midfielderā€™) ]
centralmidname = sorted(centralmidf.name)
centrebacks = dfplayers[(dfplayers.primary_position == ā€˜Left Centre Backā€™) ]
centrebacksname = sorted(centrebacks.name)

app = dash.Dash()

app.layout = html.Div(children=[
html.H1(children=ā€˜Statistiques Joueursā€™),

html.Label(''),
        html.Div([
        dcc.RadioItems(
            id='position',
            options=[{'label': i, 'value': i} 
            for i in ['ArriĆØre Lateral', 'ArriĆØre Central', 'Milieu Central', 'Milieu Lateral','Attaquant' ]],
            value='ArriĆØre Lateral',
            labelStyle={'display': 'inline-block'}
        ), 
        
          html.Hr(),
 html.Div('Joueurs'),
 html.Div(
    id='control_position'
 ),
                 ]),

html.Div([

html.Div(id = ā€˜player-infosā€™)
]),

 ])

app.config.supress_callback_exceptions = True
def generate_control_id(value):
return ā€˜Control {}ā€™.format(value)

DYNAMIC_CONTROLS = {
ā€˜ArriĆØre Lateralā€™: dcc.Dropdown(
id=generate_control_id(ā€˜ArriĆØre Lateralā€™),
options=[{ā€˜labelā€™: i, ā€˜valueā€™: i} for i in fullbacksname],
value = ā€˜Mendyā€™,

),
'ArriĆØre Central': dcc.Dropdown(
    id=generate_control_id('ArriĆØre Central'),
    options=[{'label': i, 'value': i} for i in centrebacksname],
    value = 'Ramos',
    
),
            
'Milieu Central': dcc.Dropdown(
    id=generate_control_id('Milieu Central'),
    options=[{'label': i, 'value': i} for i in centralmidname],
    value = 'Rabiot',
    
),
        
'Milieu Lateral': dcc.Dropdown(
    id=generate_control_id('Milieu Lateral'),
    options=[{'label': i, 'value': i} for i in forwardsname],
    value = 'Messi',
    
),
'Attaquant': dcc.Dropdown(
    id=generate_control_id('Attaquant'),
    options=[{'label': i, 'value': i} for i in strickersname],
    value='Benzema',
   
),

}

@app.callback(
Output(ā€˜control_positionā€™, ā€˜childrenā€™),
[Input(ā€˜positionā€™, ā€˜valueā€™)])

def display_controls(position_value):
# generate 2 dynamic controls based off of the datasource selections
return html.Div([
DYNAMIC_CONTROLS[position_value],
])

def generate_output_id(value1):
return ā€˜{} containerā€™.format(value1)

@app.callback(
dash.dependencies.Output(ā€˜player-infosā€™, ā€˜childrenā€™),
[dash.dependencies.Input(ā€˜positionā€™, ā€˜valueā€™)],
)
def update_player(value):
return html.Div(id = generate_output_id(value))

def generate_output_callback(source):
return ā€˜test {}ā€™.format(source)

for value1 in app.layout[ā€˜positionā€™].options:
app.callback(
Output(generate_output_id(value1), ā€˜childrenā€™),
[Input(generate_control_id(value1), ā€˜valueā€™)])(
generate_output_callback(value1)
)

if name == ā€˜mainā€™:
app.run_server(debug=True, port = 4005)

Thanks in Advance.

Is there a reason you have used itertools.product instead of using more conventional python loops (I donā€™t recognise it so was just wondering if I can change it to something I recognise and still have it work as shown)

1 Like

Hello. I thought I would post my example as it might help:

Here is the modified code of what chriddyp posted:

import dash
from dash.dependencies import Input, Output
import dash_core_components as dcc
import dash_html_components as html
import json

app = dash.Dash()

app.config.supress_callback_exceptions = True

app.layout = html.Div([
    dcc.Input(
        id='datasource-1',
        value=0,
        type="number"
    ),
    html.Hr(),
    html.Div('Dynamic Controls'),
    html.Div(
        id='controls-container'
    ),
    html.Div(
        id='Show_ID_works'
    ),
])

def generate_control_id(value):
    return 'Control {}'.format(value)

@app.callback(
    Output('controls-container', 'children'),
    [Input('datasource-1', 'value')])
def display_controls(datasource_1_value):
    # generate 2 dynamic controls based off of the datasource selections
    DYNAMIC_CONTROLS = {}
    for each in range(datasource_1_value):
        DYNAMIC_CONTROLS[each+1]= dcc.Input(
            id=generate_control_id(each+1),
            value=generate_control_id(each+1)
        )
    List = []
    for i in range(datasource_1_value):
        List.append(DYNAMIC_CONTROLS[i+1])
    return html.Div(
        List
    )


@app.callback(
    Output('Show_ID_works', 'children'),
    [Input('Control 4', 'value')])
def display_controls(Stuff):
    return "Hello"


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

EDIT:

This one allows the ability to call back the value after it has been created. (However it uses a global variable so itā€™s not perfect)

import dash
from dash.dependencies import Input, Output
import dash_core_components as dcc
import dash_html_components as html
import json

app = dash.Dash()

app.config.supress_callback_exceptions = True

Var = 0

app.layout = html.Div([
    html.Div(
        id='Holder'
    ),
    dcc.Input(
        id='datasource-1',
        value=0,
        type="number"
    ),
    html.Hr(),
    html.Div('Dynamic Controls'),
    html.Div(
        id='controls-container'
    ),
    html.Div(
        id='Show_ID_works'
    ),
])

def generate_control_id(value):
    return 'Control {}'.format(value)



@app.callback(
    Output('controls-container', 'children'),
    [Input('datasource-1', 'value')])
def display_controls(datasource_1_value):
    # generate 2 dynamic controls based off of the datasource selections
    DYNAMIC_CONTROLS = {}
    for each in range(datasource_1_value):
        DYNAMIC_CONTROLS[each+1]= dcc.Input(
            id=generate_control_id(each+1),
            value=generate_control_id(each+1)
        )
    List = []
    for i in range(datasource_1_value):
        List.append(DYNAMIC_CONTROLS[i+1])
    return html.Div(
        List
    )


@app.callback(
    Output('Holder', 'children'),
    [Input('datasource-1', 'value')])
def Update_Var(Stuff):
    global Var
    Var = Stuff
    return Var

@app.callback(
    Output('Show_ID_works', 'children'),
    [Input(generate_control_id(Var+1), 'value')])
def Show_Current_ID(Stuff):
    return generate_control_id(Var)

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

Hi,

I found Dash Tabs work great :slight_smile: below is some code which allows to write a single
dynamic callback function which handles an (finite but) arbitrary amount of Tabs in an app.

On my production server (Linux dash=0.26.5) I use also server side sessions. Dash seems to have some problems with this. I wrote a decorator which fixed that problem. However on my Windows machine (python installed via anaconda, dash=0.27.0) the decorator does not work.
My Mock Database work, but uses globals (not to be used in Dash). Perhaps someone has an idea how to fix that in a proper way. I have commented out the decorators so that the example works.

A redirect to a ordinary Flask route, registered with

server=Flask(name)
@server.route(ā€˜/ā€™, methods=[ā€˜GETā€™)
def hello():
return ā€˜some routeā€™

does not show up. I had to register a flask.Blueprintā€¦ why this is happening is not clear to me.

Below is the demo code, which works on my (Windows) system, enjoy.

Question: How would this work for other interactive Dash elements. I would appreciate very much if the following could be done in Dash

  • Access query parameters (e.g. in the POST body under a label query_string)
  • All interactive objects (Buttons etc) should have an additional parameter sending information
    which element was firing the callback (like the Tabs do)

Any help in this direction is very much welcome.
Cheers

BTW: How to insert larger code junks into the forum?

# -*- coding: utf-8 -*-
"""
Dash with dynaminc callbacks
============================

Example how to use tabs when the number of tabs is only known at runtime.
"""

import dash_html_components as html
import dash_core_components as dcc
import random

from dash import Dash
from dash.dependencies import Input, Output
from flask import Flask, Blueprint, request, redirect
from functools import wraps


#******************************************************************************
# needs_request decorator:
#
'''
needs_request
~~~~~~~~~~~~~

When Dash is used with a session (eg. server side SQAlchemySession) it seems
that at startup all layouts which are registered at `app.layout` are run.
At startup we do not have a `flask.request` object available and the server
will not start. Use the needs_request decorator to route this starup call
to a dummy layout.

After the app ist started this decorator will just do nothing and runs the
layout. The startup call is never rendered, so a redirect would not be 
needed.

.. note:: This decorator works on my production system with dash=0.26.5 but
 on my Windoes system, where this snippet was writte, it fails with an 
 error:

 builtins.AttributeError
 
 AttributeError: 'Response' object has no attribute 'traverse'

 Without the decorator the layout fails for access to a LocalProxy.  
'''

def needs_request(func):
'''decorator to prevent layouts from being executed on startup of
the Dash app, when no `request context` is available. This decorator
protects layouts which try to access Flask local proxies such as
g, app_context, session
'''
@wraps(func)
def decorated_view(*args, **kwargs):
    if not request:
        #: sessions work only in a request context, we need to redirect 
        #: Dash calls to Flask as Dash runs layouts on startup when no
        #: request is available
        print('DEBUG: NO REQUEST CONTEXT')
        return redirect('/', 301)
    print('DEBUG: HAS REQUEST')
    return func(*args, **kwargs) 
return decorated_view


#******************************************************************************
# dash app setup:
#
#: basic dash css
external_stylesheets = [
    'https://codepen.io/chriddyp/pen/bWLwgP.css',
]


#: instantiate app object
#: Flask server
server = Flask(__name__)
server.config['WTF_CSRF_ENABLED'] = False

#: a route which is from Flask (eg login route) to redirect to on Dash startup
#: this does not work, I need to register a blueprint as in the production 
#: server
#: WHY?
server.route('/', methods=['GET'])
def dummy_route():
return '''Dash app is located at <a href='/dash/'>/dash</a>. This should \
be a login page.'''

#: blueprint with dummy route
dummy_bp = Blueprint('dummy', __name__,
                    template_folder='templates')

@dummy_bp.route('/')
def default_view():
return '''Dash app is located at <a href='/dash/'>/dash</a>. This should \
be a login page.'''


server.register_blueprint(dummy_bp)

#: Dash app
app = Dash(__name__,
       sharing=True,
       server=server,
       url_base_pathname='/dash/',
       suppress_callback_exceptions=True,
       external_stylesheets=external_stylesheets,
       meta_tags=[{'title': 'Dash dynamic callbacks'},
                  {'author': 'Bertfried'},
                  {'copyright': 'GPL2'}]
       )
app.title = 'Dash dynamic routing'


#******************************************************************************
# DATABASE and SESSION MOCK
#
'''
DATABASE and SESSION MOCK
=========================

.. note:: Do not use globals in `Dash`, code will break when more than one
user is using the app.
'''

# Mock for database access
DATABASE = dict(row_0=dict(id=0, text='Device data for tab-1'),
            row_1=dict(id=1, text='Other data for tab-2'),
            row_2=dict(id=2, text='New data for tab-3'),
            row_3=dict(id=3, text='Hi, my data for tab-4'),
            row_4=dict(id=4, text='Meter data for tab-5'),
            row_5=dict(id=5, text='No data for tab-6'),
            row_6=dict(id=6, text='Your data for tab-7'),
            row_7=dict(id=7, text='My data for tab-8'),
            row_8=dict(id=8, text='Alice\'s data for tab-9'),
            row_9=dict(id=9, text='Bob\'s data for tab-10'),
            )

#: MOCK for server side session, such a session is a dictionary sent as cookie
#: or stored to server (redis, SQLAlchemy) and only a session id is sent as
#: cookie. Here we need to store a
#: list of 'devices' which should get a tab
SESSION = dict(_device_tabs=None) 


#******************************************************************************
# dash layouts
#
#@needs_request  #: needed so that dash can start with sessions 
def layout_main():
'''Main layout with tabs'''

global DATABASE  #: do not use globals in Dash
global SESSION

#: this code does the same thing as the decorator needs_request
if not request:
    print('DEBUG: Error: No request context available')
#: code below this point would not run when sessions, request, g, 
#: cpp_context LocalProxies are used....

#: from API call to database get data how many tabs to render [1,9]
num_tabs = random.randint(1,10)

tab_devices = []
for _i in range(0, num_tabs):
    tab_devices.append(_i)

SESSION['_device_tabs'] = tab_devices

return html.Section(className='os-top', children=[
    html.H6('Dashboard'),
    html.Div('A dashboard with dynamically registered callbacks for a \
random number of tabs.'),
    html.P('Press `F5` (chrome browser) to refresh the page and see that \
a different number of Tabs is created'),
    html.Br(),
    html.A('Back to Flask', href='/'),
    html.Br(),
    dcc.Tabs(id="tabs-unit", value='tab-0-unit', 
    children=[
        dcc.Tab(label='Tab #{}'.format(td), value='tab-{}-unit'.format(num)) \
          for num, td in enumerate(tab_devices)
    ]),
html.Div(id='tabs-content-unit')
])


#@needs_request
def make_tab(i):

global SESSION  #: do not use globals in Dash
global DATABASE

_table = []
_table.append(html.Tr(children=[
    html.Th('Device #'),
    html.Th('Text from Database')
]))
assert isinstance(DATABASE, dict)
assert isinstance(DATABASE.get('row_0'), dict)
_t = html.Tr([
     html.Td(DATABASE.get('row_{}'.format(i)).get('id')),
     html.Td(DATABASE.get('row_{}'.format(i)).get('text')),
]);
_table.append(_t)

return html.Div([
    html.H6(['Dynamic Tab']),
    html.Table(children=_table)
])

#******************************************************************************
# register layout & callbacks
#    
app.layout = layout_main

#==============================================================================
# >>>> dynamic callback <<<<    
#: callback for tabs on the unit dashboard
@app.callback(Output('tabs-content-unit', 'children'),
      [Input('tabs-unit', 'value')])
def render_content(tab):
''' We registed a single callback which gets the `value` (id) data from the
tabs-unit id of the tab element. It can render different layouts for the
different tabs based on this id (here id is an int, production may want
a uuid. 

The Tab element is special as it allows this way an identification of the
calling element. I need this type of functinality in **all** other
elements, especially links, (query parameters, and buttons, possibly
having an 'value' or 'seconday id' type of parameter.
'''
i = int(tab.split('-')[1])
return make_tab(i)
#==============================================================================


if __name__ == '__main__':
#: on windows starting script from anaconda port 127.0.0.1 is blocked use 
#: machine's IP, setup environment
#: set FLASK_APP=dash_ui_routing.py
#: set FLASK_DEBUG=True
#: set SECRET_KEY='my-development-secret-key'
#: set API_SETTINGS=development
#: run flask from cmd line with:
#: flask run --host=192.168.178.26 --port=8050 --reload    
server.run(host='0.0.0.0', port=8050)

Just a heads up for the community. Weā€™re working on a new system for callbacks that would enable binding callbacks for an arbitrary number of components. Itā€™s code-named ā€œwildcard propertiesā€, you can follow along the discussion here: Dynamic Layouts (Wildcard Props) Ā· Issue #475 Ā· plotly/dash Ā· GitHub

6 Likes

I donā€™t get it - I do a very similar thing and when I try to pass a non-yet-existing Input here in the arguments I get the following error:
An invalid input object was used in an Input of a Dash callback.

A post was split to a new topic: Code not working

Note - this limitation will be lifted soon, see šŸ“£ Wildcards & Dynamic Callback Support - In Development, Looking for Feedback for our new approach.

2 Likes

I have a follow up question to the way it works currently:

When i use the syntax as in

app.callback(
   Output(...),
   [Input(....)],
   # yadayada
)(someFunc())

def someFunc():
   def callback():
      pass
   return callback

What is the deeper syntax behind that? In a more generic python way? How does that call to another function on top of the callack () behave? Could I use a lambda function? Iā€™m asking mainly because I would like to clean up some code and for better readability keep stuff in one place that belongs to each other (for a little more complex widget compositions).

Meaning: Iā€™d love to define that function right there, as in:

app.callback(
   Output(),
   # etc...
)(
   callback()
)

Is that syntactically possible? Should I use lambda functions here? Iā€™m struggling to finding good documentation on this, either from dash or python in general.

You should be able to just pass in the name of the function (without the (), otherwise youā€™d be calling it)

app.callback(
   Output(),
   # etc...
)(
   callback
)

Iā€™m pleased to announce that this limitation is no longer with our new Pattern-Matching Callbacks feature released in Dash 1.11.0: šŸ“£ Dash v1.11.0 Release - Introducing Pattern-Matching Callbacks :tada:

If you have a question about this feature, please open a new thread. Thank you everybody for the feedback on this issue :heart:

3 Likes