Error when combining pattern matching and circular callback with multiple sliders and inputs

I’m quite new to Python and Plotly Dash, currently facing issues when refactoring this dashboard to use pattern matching and circular callback.

Key Features of dashboard

  • User adjust weightage of each factor via Slider or Input. Each Slider and Input are synchronized
  • Each city have base score for each factor in excel sheet.
  • Index of each city is calculated by weighted average for all factors (base score * weightage)

Issues when refactoring:

  1. When printing value for one factor for testing, values of all factors were printed instead of one.
  2. As all values are triggered instead of one, unable to differentiate for weighted average calculation for each factor.
  3. Only the highlighted components can be adjusted. For example, Slider for Smart mobility can be adjusted, which change value for corresponding input. But Input value can’t be adjusted to change slider value.

issue

#Attempt to refactor
@app.callback(
    [Output({'type': 'slider', 'index': MATCH}, 'value'),
    Output({'type': 'num_input', 'index': MATCH}, 'value')],
    inputs={
        "all_inputs": {
            "slider": Input({'type': 'slider','index': MATCH}, "value"),
            "num_input": Input({'type': 'num_input','index': MATCH}, "value")
        }
    }
)


def callback(all_inputs):
    weightage = []
    c = ctx.args_grouping.all_inputs
    value_Smart_Mobility = 10 #Quick fix to prevent unbounded error

    value_Smart_Mobility = c.num_input.value if c.num_input.id.index == 'Smart_Mobility' else c.slider.value
    print(value_Smart_Mobility) #Values for all factors were printed out, instead of one only
    value_Smart_Mobility = c.slider.value if c.slider.id.index == 'Smart_Mobility' else c.num_input.value
    print(value_Smart_Mobility) #Values for all factors were printed out, instead of one only
    ....

    return value_Smart_Mobility, value_Smart_Mobility, ...

The original long callback version worked but its too lengthy and require manual change in App Callback whenever column names in excel changes.

# Original long callback version which worked.
df= pd.read_excel("C:\\Desktop\\Smart_City.xlsx", sheet_name='cities')

# Extract column names from excel file
factors = []
for i in df.columns[1:]:
    factors.append(i)

app = dash.Dash(__name__, external_stylesheets=[dbc.themes.BOOTSTRAP])

# Using list comprehension to create slider and input. ID's Index is column name from dataframe/excel
app.layout = app.layout = html.Div([

        dbc.Row(
            [
                dbc.Col(html.Div([
                            dbc.Row(dbc.Col(html.Div([
                            dcc.Slider(
                            id={'type': 'slider','index': name}, 
                        )]
                    )))
                    for name in factors])),


                dbc.Col(html.Div([
                            dbc.Row(dbc.Col(html.Div([
                            daq.NumericInput(
                            id={'type': 'num_input','index': name}
                            )]
                        )))

                    for name in factors])),

                dbc.Col(html.Div(dbc.Card(html.Div([
                                                dcc.Graph(id="chart")])
                                               )))
            ]
        ),
        
])

# Long callback listing every input and output. Trying to use pattern matching, so that index wont be hardcoded
@app.callback(
    [Output({'type': 'slider','index': "Smart_Mobility"}, "value"),
    Output({'type': 'num_input','index': "Smart_Mobility"}, "value"), 
    ...,
    Output("chart", "figure")],
    [Input({'type': 'slider','index': "Smart_Mobility"}, "value"),
    Input({'type': 'num_input','index': "Smart_Mobility"}, "value"),
    ...]
)

# Circular callback to synchronize each slider to input. Iterate to calculate weighted average Index for chart.
def callback(slider_Smart_Mobility, num_input_Smart_Mobility, ...):

    weightage = []
    value_Smart_Mobility = num_input_Smart_Mobility if ctx.triggered_id == {'type': 'num_input','index': "Smart_Mobility"} else slider_Smart_Mobility
    weightage.append(value_Smart_Mobility / 100 )
    ...

    city_index = []
    for index, row in df.iterrows():
        base_list = [int(row[i]) for i in factors]
        index_list = [int(j*k) for j, k in zip(base_list, weightage)]
        index = float("{:.2f}".format(sum(index_list) / sum(weightage)))
        city_index.append(index)

    df['Index'] = city_index
    df['Index'] = pd.to_numeric(df['Index'])
    df2 = df.sort_values(by=['Index'], ascending=True)

    figure = px.bar(df2, x='Index', y='City', height=500, color='Index', color_continuous_scale='inferno')

    return value_Smart_Mobility, value_Smart_Mobility, ... , figure

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

Hi,

I am not much on par on how ctx.args_grouping works, but I suspect that it returns all Inputs and States even though you are using a MATCH wildcard. This would explain why you are seen the behavior described in points 1 and 2. Then point 3 is just some wrong logic that assumes that the object in the conditional are str/int instead of list.

If you are interested in updating only one pair of Slider+Input, then you can probably use ctx.triggered to see which one of the two components was updated and use the corresponding value from all_inputs (the function parameter) to update the other component of the pair.

Please let me know if it isn’t clear, I can quickly give you a snippet if I weren’t on mobile… :slight_smile:

1 Like

Hi,

Thanks for reaching out, I will appreciate if you could share a snippet. Meanwhile I will share the excel file and full code :slight_smile:

Download File Here

Attempt to refactor

import dash
from dash.dependencies import Input, Output, MATCH
from dash import dcc, ctx
import dash_bootstrap_components as dbc
import dash_daq as daq
from dash import html
import plotly.express as px
import pandas as pd

df= pd.read_excel("C:\\Desktop\\Smart_City.xlsx", sheet_name='cities')

factors = []
for i in df.columns[1:]:
    factors.append(i)

PLOTLY_LOGO = "https://images.plot.ly/logo/new-branding/plotly-logomark.png"

app = dash.Dash(__name__, external_stylesheets=[dbc.themes.BOOTSTRAP])
app.title = "Smart Cities"

# make a reuseable navitem for the different examples
nav_item = dbc.NavItem(dbc.NavLink("Link", href="https://plotly.com"))

# make a reuseable dropdown for the different examples
dropdown = dbc.DropdownMenu(
    children=[
        dbc.DropdownMenuItem("Entry 1"),
        dbc.DropdownMenuItem("Entry 2"),
        dbc.DropdownMenuItem(divider=True),
        dbc.DropdownMenuItem("Entry 3"),
    ],
    nav=True,
    in_navbar=True,
    label="Menu",
)

# this example that adds a logo to the navbar brand
logo = dbc.Navbar(
    dbc.Container(
        [
            html.A(
                # Use row and col to control vertical alignment of logo / brand
                dbc.Row(
                    [
                        dbc.Col(html.Img(src=PLOTLY_LOGO, height="30px")),
                        dbc.Col(dbc.NavbarBrand("Smart Cities Index", className="ms-2")),
                    ],
                    align="center",
                    className="g-0",
                ),
                href="https://plotly.com",
                style={"textDecoration": "none"},
            ),
            dbc.NavbarToggler(id="navbar-toggler2", n_clicks=0),
            dbc.Collapse(
                dbc.Nav(
                    [nav_item, dropdown],
                    className="ms-auto",
                    navbar=True,
                ),
                id="navbar-collapse2",
                navbar=True,
            ),
        ],
    ),
    color="dark",
    dark=True,
    className="mb-5",
)

app.layout = html.Div([

        logo,

        dbc.Row(
            [
                dbc.Col(html.Div([
                    
                    dbc.Row(dbc.Col(html.Div([
                            html.Label(f'{name}'),
                            dcc.Slider(
                            id={'type': 'slider','index': name}, 
                            min=0, max=100, 
                            marks=None,
                            value=10,
                        )],
                        style={
                            'font-size': '14px',
                            'height': '38px',
                            'width': '100%',
                            'display': 'inline-block',
                            'vertical-align': 'bottom',
                        })
                    ))

                    for name in factors]), width={"size": 3, "offset": 1}),


                dbc.Col(html.Div([
                    
                        dbc.Row(dbc.Col(html.Div([
                            html.Br(),
                            daq.NumericInput(
                            id={'type': 'num_input','index': name},
                            min=0, max=100, value=10, size=60,
                            ),],
                                style={
                                    'height': '38px',
                                    'width': '100%',
                                    'display': 'inline-block',
                                    'vertical-align': 'bottom',
                                })
                        ))

                    for name in factors]), width=1),

                dbc.Col(html.Div(dbc.Card(html.Div([
                                            dcc.Loading([
                                                dcc.Graph(id="chart", )])], style={'horizontal-align': 'middle' ,'vertical-align': 'middle', 'overflowY': 'scroll'},)
                                                , style={'height': '450px', 'display':'inline-block',})), width=7)
            ]
        ),
        
])


@app.callback(
    [Output({'type': 'slider', 'index': MATCH}, 'value'),
    Output({'type': 'num_input', 'index': MATCH}, 'value')],
    inputs={
        "all_inputs": {
            "slider": Input({'type': 'slider','index': MATCH}, "value"),
            "num_input": Input({'type': 'num_input','index': MATCH}, "value")
        }
    }
)


def callback(all_inputs):
    c = ctx.args_grouping.all_inputs
    value_Smart_Mobility = 10 #Quick fix to prevent unbounded error

    value_Smart_Mobility = c.num_input.value if c.num_input.id.index == 'Smart_Mobility' else c.slider.value
    print(value_Smart_Mobility) #Values for all factors were printed out, instead of one only
    value_Smart_Mobility = c.slider.value if c.slider.id.index == 'Smart_Mobility' else c.num_input.value
    print(value_Smart_Mobility) #Values for all factors were printed out, instead of one only
    return value_Smart_Mobility, value_Smart_Mobility

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

Original long callback version

import dash
from dash.dependencies import Input, Output, MATCH
from dash import dcc, ctx
import dash_bootstrap_components as dbc
import dash_daq as daq
from dash import html
import plotly.express as px
import pandas as pd

# Extract column names from excel file
df= pd.read_excel("C:\\Desktop\\Smart_City.xlsx", sheet_name='cities')

factors = []
for i in df.columns[1:]:
    factors.append(i)

PLOTLY_LOGO = "https://images.plot.ly/logo/new-branding/plotly-logomark.png"

app = dash.Dash(__name__, external_stylesheets=[dbc.themes.BOOTSTRAP])
app.title = "Smart Cities"

# make a reuseable navitem for the different examples
nav_item = dbc.NavItem(dbc.NavLink("Link", href="https://plotly.com"))

# make a reuseable dropdown for the different examples
dropdown = dbc.DropdownMenu(
    children=[
        dbc.DropdownMenuItem("Entry 1"),
        dbc.DropdownMenuItem("Entry 2"),
        dbc.DropdownMenuItem(divider=True),
        dbc.DropdownMenuItem("Entry 3"),
    ],
    nav=True,
    in_navbar=True,
    label="Menu",
)

# this example that adds a logo to the navbar brand
logo = dbc.Navbar(
    dbc.Container(
        [
            html.A(
                # Use row and col to control vertical alignment of logo / brand
                dbc.Row(
                    [
                        dbc.Col(html.Img(src=PLOTLY_LOGO, height="30px")),
                        dbc.Col(dbc.NavbarBrand("Smart Cities Index", className="ms-2")),
                    ],
                    align="center",
                    className="g-0",
                ),
                href="https://plotly.com",
                style={"textDecoration": "none"},
            ),
            dbc.NavbarToggler(id="navbar-toggler2", n_clicks=0),
            dbc.Collapse(
                dbc.Nav(
                    [nav_item, dropdown],
                    className="ms-auto",
                    navbar=True,
                ),
                id="navbar-collapse2",
                navbar=True,
            ),
        ],
    ),
    color="dark",
    dark=True,
    className="mb-5",
)

# Using list comprehension to create slider and input. ID's Index is column name from dataframe/excel
app.layout = html.Div([

        logo,

        dbc.Row(
            [
                dbc.Col(html.Div([
                    
                    dbc.Row(dbc.Col(html.Div([
                            html.Label(f'{name}'),
                            dcc.Slider(
                            id={'type': 'slider','index': name}, 
                            min=0, max=100, 
                            marks=None,
                            value=10,
                        )],
                        style={
                            'font-size': '14px',
                            'height': '38px',
                            'width': '100%',
                            'display': 'inline-block',
                            'vertical-align': 'bottom',
                        })
                    ))

                    for name in factors]), width={"size": 3, "offset": 1}),


                dbc.Col(html.Div([
                    
                        dbc.Row(dbc.Col(html.Div([
                            html.Br(),
                            daq.NumericInput(
                            id={'type': 'num_input','index': name},
                            min=0, max=100, value=10, size=60,
                            ),],
                                style={
                                    'height': '38px',
                                    'width': '100%',
                                    'display': 'inline-block',
                                    'vertical-align': 'bottom',
                                })
                        ))

                    for name in factors]), width=1),

                dbc.Col(html.Div(dbc.Card(html.Div([
                                            dcc.Loading([
                                                dcc.Graph(id="chart", )])], style={'horizontal-align': 'middle' ,'vertical-align': 'middle', 'overflowY': 'scroll'},)
                                                , style={'height': '500px', 'display':'inline-block',})), width=7)
            ]
        ),
        
])


# Long callback listing every input and output. Trying to use pattern matching, so that index wont be hardcoded
@app.callback(
    [Output({'type': 'slider','index': "Smart_Mobility"}, "value"),
    Output({'type': 'num_input','index': "Smart_Mobility"}, "value"),
    Output({'type': 'slider','index': "Smart_Environment"}, "value"),
    Output({'type': 'num_input','index': "Smart_Environment"}, "value"),
    Output({'type': 'slider','index': "Smart_Government"}, "value"),
    Output({'type': 'num_input','index': "Smart_Government"}, "value"),
    Output({'type': 'slider','index': "Smart_Economy"}, "value"),
    Output({'type': 'num_input','index': "Smart_Economy"}, "value"),
    Output({'type': 'slider','index': "Smart_People"}, "value"),
    Output({'type': 'num_input','index': "Smart_People"}, "value"),
    Output({'type': 'slider','index': "Smart_Living"}, "value"),
    Output({'type': 'num_input','index': "Smart_Living"}, "value"),
    Output("chart", "figure")],
    [Input({'type': 'slider','index': "Smart_Mobility"}, "value"),
    Input({'type': 'num_input','index': "Smart_Mobility"}, "value"),
    Input({'type': 'slider','index': "Smart_Environment"}, "value"),
    Input({'type': 'num_input','index': "Smart_Environment"}, "value"),
    Input({'type': 'slider','index': "Smart_Government"}, "value"),
    Input({'type': 'num_input','index': "Smart_Government"}, "value"),
    Input({'type': 'slider','index': "Smart_Economy"}, "value"),
    Input({'type': 'num_input','index': "Smart_Economy"}, "value"),
    Input({'type': 'slider','index': "Smart_People"}, "value"),
    Input({'type': 'num_input','index': "Smart_People"}, "value"),
    Input({'type': 'slider','index': "Smart_Living"}, "value"),
    Input({'type': 'num_input','index': "Smart_Living"}, "value")]
)

# Circular callback to synchronize each slider to input. Iterate to calculate weighted average Index for chart.
def callback(slider_Smart_Mobility, num_input_Smart_Mobility, slider_Smart_Environment, num_input_Smart_Environment, slider_Smart_Government, num_input_Smart_Government, slider_Smart_Economy, num_input_Smart_Economy, slider_Smart_People, num_input_Smart_People, slider_Smart_Living, num_input_Smart_Living):
    weightage = []
    value_Smart_Mobility = num_input_Smart_Mobility if ctx.triggered_id == {'type': 'num_input','index': "Smart_Mobility"} else slider_Smart_Mobility
    weightage.append(value_Smart_Mobility / 100 )
    value_Smart_Environment = num_input_Smart_Environment if ctx.triggered_id == {'type': 'num_input','index': "Smart_Environment"} else slider_Smart_Environment
    weightage.append(value_Smart_Environment / 100 )
    value_Smart_Government = num_input_Smart_Government if ctx.triggered_id == {'type': 'num_input','index': "Smart_Government"} else slider_Smart_Government
    weightage.append(value_Smart_Government / 100 )
    value_Smart_Economy = num_input_Smart_Economy if ctx.triggered_id == {'type': 'num_input','index': "Smart_Economy"} else slider_Smart_Economy
    weightage.append(value_Smart_Economy / 100 )
    value_Smart_People = num_input_Smart_People if ctx.triggered_id == {'type': 'num_input','index': "Smart_People"} else slider_Smart_People
    weightage.append(value_Smart_People / 100 )
    value_Smart_Living = num_input_Smart_Living if ctx.triggered_id == {'type': 'num_input','index': "Smart_Living"} else slider_Smart_Living
    weightage.append(value_Smart_Living / 100 )

    city_index = []
    for index, row in df.iterrows():
        base_list = [int(row[i]) for i in factors]
        index_list = [int(j*k) for j, k in zip(base_list, weightage)]
        index = float("{:.2f}".format(sum(index_list) / sum(weightage)))
        city_index.append(index)

    df['Index'] = city_index
    df['Index'] = pd.to_numeric(df['Index'])
    df2 = df.sort_values(by=['Index'], ascending=True)

    figure = px.bar(df2, x='Index', y='City', height=500, color='Index', color_continuous_scale='inferno')

    return value_Smart_Mobility, value_Smart_Mobility, value_Smart_Environment, value_Smart_Environment, value_Smart_Government, value_Smart_Government, value_Smart_Economy, value_Smart_Economy, value_Smart_People, value_Smart_People, value_Smart_Living, value_Smart_Living, figure

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