Why would Dash dynamic pattern matching callback only pass the first value in?

Viewed 808

I am writing a small Dash app that will allow you to upload a csv, then manipulate column types. One of the features is to "ignore" a specific column. The end-state would be that the "ignored" column will be dropped from the later-previewed data table.

Since I am assuming I do NOT know the number of columns when the file is uploaded, I have built a callback that creates a table based on the number of columns in the input file.

# SECTION: PARSING UPLOAD DATA
@app.callback(Output('data_picker_table', 'children'),
              Input('hidden_df', 'children'))
def parse_file(hidden_df):
    if not hidden_df:
        raise dash.exceptions.PreventUpdate
    else:
        df = pd.read_json(hidden_df, orient='split')
        cols = df.columns.tolist()
        tbl_styles = {'verticalAlign': 'middle', 'textAlign': 'center'}
        headers = [
            html.Thead(html.Tr([html.Th('Column', style=tbl_styles), html.Th('Casted Type', style=tbl_styles),
                                html.Th('Ignore', style=tbl_styles)]))]
        id = 0
        for col in cols:
            # print(df[col].dtype)
            if df[col].dtype == 'datetime64[ns]':
                dropdown = dbc.Select(id={'type': 'dtype_dropdown', 'index': id}, placeholder='Casted dtype',
                                      options=[{'label': type, 'value': type} for type in
                                               ['datetime64', 'object/string']],
                                      className='custom-select')
            toggle = dbc.Checklist(id={'type': 'column_toggle', 'index': id},
                                   options=[{'label': 'Drop', 'value': 1}],
                                   switch=True)
            headers.append(
                html.Tr(id={'type': 'data_row', 'index': id},
                        children=[html.Td(col, style=tbl_styles, id={'type': 'col_name', 'index': id}),
                                  html.Td(dropdown, style=tbl_styles),
                                  html.Td(toggle, style=tbl_styles)]))
            id += 1
        table = dbc.Table(headers, striped=True, bordered=True, className='table-secondary')
        return table

You'll see that each row of the table appended to the headers list contains a column name, dropdown, and a toggle switch.

Now when I go to test that the toggle switches work with the below callback, I would expect that this would ingest EVERY 'column_toggle', but it seems to only care about the first toggle in row one.

@app.callback(Output({'type': 'printer', 'index': MATCH}, 'children'),
              Input({'type': 'column_toggle', 'index': MATCH}, 'value'),
              State({'type': 'col_name', 'index': MATCH}, 'children'))
def print_dropped(value, col):
    if value and value[0] == 1:
        print(value, col)
    return ''

Even when playing in debug mode, the breakpoint for value[0] == 1 provides nothing unless the first toggle is activated. When the first toggle is hit, the debug output shows the following.

col = {str} 'Date'
value = {list: 1} [1]
 0 = {int} 1
 __len__ = {int} 1

EDIT -- I have looked into the dash.callback_context information to debug further and have found that the inputs_list seems to confirm that only one of the column_toggle IDs is being picked up.

"in_list": [
    {
      "id": "button",
      "property": "n_clicks",
      "value": 4
    },
    {
      "id": {
        "index": 0,
        "type": "column_toggle"
      },
      "property": "value",
      "value": false
    }
  ]

Any guidance on what key error I am making in this design would be greatly appreciated. From what I have found, what I am trying to do should fall nicely into the dynamic callback functionality I would think.

1 Answers
id = 0
for col in cols:
  ....
  id += 1

First of all, rather than using 'id' as the incrementor, I suggest that you write your code as such:

for num, col in enumerate(cols):

num would be your index value for your id. Avoid using 'id' as you might come across issue such as name shadow (Dash also uses keyword 'id' as argument).

I haven't tried your code but I believe another way of doing this is to use Dash's DataTable instead of DBC's Table. Using DataTable, column can be set directly for it to be hidable, clearable, editable, deletable etc. This way you can avoid having to create a callback for hiding/deleting a column.

Here you can find the documentation as well as its reference

Related