✊🏿 Black Lives Matter. Please consider donating to Black Girls Code today.
⚡️ Concerned about the grid? Kyle Baranko teaches how to predicting peak loads using XGBoost. Register for the August webinar!

Dynamically update data table column options in a dropdown

I have a data table that has a column with a dropdown for COM ports, which are dynamically read in using pyserial (this is a Dash DAQ application). It’s pretty easy to set the available comports on startup - but I haven’t successfully been able to update the dropdown options dynamically. Has anyone been successful with something like this?

It seems like a few others have asked similar questions without any substantive replies and/or final resolutions:

I’m having a similar issue with Check Lists. I’ve tried all sorts of different formatting for the return to ‘options’ but nothing seems to work.

This should work. Could you create a simple, reproducable example?

import os
import dash
import dash_core_components as dcc
import dash_html_components as html
import dash_table as dt
import dash_daq as daq
from dash.dependencies import Input, Output, State
import plotly.graph_objs as go
import serial
from serial.tools.list_ports import comports


def get_comports():
    return list([{"label": a.device, "value": a.device} for a in comports()])


baud_options = [
    {"label": "4800", "value": 4800},
    {"label": "9600", "value": 9600},
    {"label": "14400", "value": 14400},
    {"label": "19200", "value": 19200},
    {"label": "28800", "value": 28800},
    {"label": "38400", "value": 38400},
    {"label": "57600", "value": 57600},
    {"label": "115200", "value": 115200},
    {"label": "230400", "value": 230400},
    {"label": "460800", "value": 460800},
]

app = dash.Dash(__name__, meta_tags=[
                {"name": "viewport", "content": "width=device-width, initial-scale=1.0"}])


app.layout = html.Div([
    html.H2("Example App"),
    dt.DataTable(
        id='devices-table',
        style_data={
            "whiteSpace": "normal",
            "height": "auto"
        },
        style_cell_conditional=[
            {"if": {
                "column_id": 'com'}, "width": "50%"},
            {"if": {
                "column_id": 'baud'}, "width": "25%"},
            {"if": {
                "column_id": 'status'}, "width": "25%"},
        ],
        columns=[
            {"id": 'com', "name": "COM",
             "presentation": "dropdown"},
            {"id": 'baud', "name": "Baud",
             "presentation": "dropdown"},
            {"id": 'status', "name": "Status",
             "presentation": "input"},
        ],
        editable=True,
        row_deletable=True,
        dropdown={
            "com": {
                "options": get_comports()
            },
            "baud": {
                "options": baud_options
            }
        },
        persistence=True,
        persistence_type="session",
        persisted_props=["data"],
        data=[]
    ),
    html.Button(
        'Add Analyzer', id='editing-rows-button', n_clicks=0, style={"marginTop": "15px"}),
])


# CALLBACKS
@app.callback(
    Output("devices-table", "data"),
    [
        Input("editing-rows-button", "n_clicks"),
    ],
    [
        State("devices-table", "data"),
        State("devices-table", "columns")
    ]
)
def add_row(n_clicks, rows, columns):
    if n_clicks > 0:
        rows.append({"status": False})
    return rows


@app.callback(
    Output("com", "options"),
    [
        Input("devices-table", "data")
    ]
)
def update_com_ports(value):
    return get_comports()

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

1 Like

My problem was that I was returning nothing to Dash from the button callback upon entering the application. Maybe when you enter the application it goes to this button callback and the output to ‘devices-table’ is nothing.

@app.callback(
        Output("devices-table", "data"),[
        Input("editing-rows-button", "n_clicks"),],[
        State("devices-table", "data"),
        State("devices-table", "columns")])

def add_row(n_clicks, rows, columns):
    if n_clicks > 0:
            rows.append({"status": False})
    if rows: #if there's anything in the value for 'rows' this statement evaluates true
        return rows
    else:
            return []

Same problem!!! Excellent example provided above by @dhhagan. The error I get is the same. “ID not found in layout.” Any solutions to this yet? What did you do to dynamically update the checklist options, @shane? Do you have some code you can share?

This is what ended up working for me. Hope it helps!

.
.
.

    def create_table_card(self, id):

        return dbc.Card(
            children = [
                html.Div(
                    dash_table.DataTable(
                        id      = id,
                        data    = [ 
                            dict(filename = '', waveform = '', expression = '', result = ''),
                            dict(filename = '', waveform = '', expression = '', result = ''),
                            dict(filename = '', waveform = '', expression = '', result = '')
                        ],
                        columns = [
                            { 'id': 'filename',   'name': 'Filename', 'presentation': 'dropdown'},
                            { 'id': 'waveform',   'name': 'Waveform', 'presentation': 'dropdown'},
                            { 'id': 'expression', 'name': 'Expression', 'editable': True        },
                            { 'id': 'result',     'name': 'Result',     'editable': False       }
                        ],
                        style_cell = { 
                            'textAlign'   : 'center', 
                            'fontSize'    : 14, 
                            'font-family' :'sans-serif' 
                        },
                        editable      = True,
                        row_deletable = True
                    ),
                    style = {
                        'padding-top'    : 25,
                        'padding-bottom' : 20,
                        'padding-left'   : 35,
                        'padding-right'  : 35
                    }
                ),
                html.Div(
                    dbc.Button('Add Row', id = 'table-add-row-button', className = 'ml-auto', n_clicks = 0),
                    style = {
                        'padding-left'   : 20,
                        'padding-bottom' : 20
                    }
                )
            ]
        )
.
.
.

@app.callback(
    Output('table', 'dropdown'),
    [
        Input('table', 'data_timestamp')
    ],
    [
        State('table', 'data')
    ]
)
def table_dropdown(timestamp, rows):

    logging.debug('table_dropdown')

    return { 'filename' : { 'options': get_uploads() } }

@app.callback(
    Output('table', 'dropdown_conditional'),
    [
        Input('table', 'data_timestamp')
    ],
    [
        State('table', 'data')
    ]
)
def table_dropdown_conditional(timestamp, rows):

    logging.debug('rows = {}'.format(rows))

    filenames = []

    for row in rows:

        if row['filename'] is not None:

            if os.path.isfile(os.path.join(FOLDER_UPLOADS, row['filename'])):

                filenames.append(row['filename'])

    # logging.debug('filenames = {}'.format(filenames))

    conditions = []

    for filename in set(filenames):

        d = None

        if filename.endswith('.feather'):

            d = pd.read_feather(os.path.join(FOLDER_UPLOADS, filename))

        elif filename.endswith('.parquet'):

            # NOTE: By using Dask (dd) instead of Pandas (pd) below, we avoid
            # loading all of the data when all we need are the column names.
            # Unfortunately, Dask doesn't support feather right now.

            d = dd.read_parquet(os.path.join(FOLDER_UPLOADS, filename))

        if d is not None:

            conditions.append({
                'if'      : {
                    'column_id'    : 'waveform',
                    'filter_query' : '{filename} eq \'' + filename + '\''
                }, 
                'options' : [ { 'label': i, 'value': i } for i in d.columns[1:] ]
            })

    # logging.debug('conditions = {}'.format(conditions))

    return conditions

@app.callback(
    Output('table', 'data'),
    [
        Input('table-add-row-button', 'n_clicks'),
        Input('table', 'data_timestamp')
    ],
    [
        State('table', 'data'),
        State('table', 'columns')
    ]
)
def table(n, timestamp, rows, columns):

    context = dash.callback_context
    context = context.triggered[0]['prop_id'].split('.')[0] if context.triggered else None

    logging.debug('context = {}'.format(context))

    # If the user clicked the Add Row button, then add an empty row:

    if context == 'table-add-row-button':

        rows.append({ column['id']: '' for column in columns })

    # If the user cleared a filename, then clear the associated waveform:

    if context == 'table':

        for row in rows:

            if row['filename'] is None:

                row['waveform'] = ''

    return rows