Issue with 'Nonexistent objects in an 'Input' of a Dash callback'

Hi,

This is my first post, I am a student new to Dash and having trouble.

Background:
My app currently consists of a dbc.Table and a bdc.DropdownMenu.
The app loads and displays a table of multiple machines that I wish to monitor, loaded from a SQLite DB.
When a location is selected the table filters the machines to that specific region. This is done by a callback.
I have a PAHO MQTT client collecting my data and storing it in the DB.
Then I want to have a callback that refreshes my table when values change i.e. CPU Usage.

Issue
The update selector works fine until I select a location and then I start getting the ‘Nonexistent objects in an ‘Input’ of a Dash callback’ error messages.

Code
Location callback

`@app.callback(
    Output('continents_selector', 'label'),
    Output('summary_table', 'children'),
    [Input(continent[0], 'n_clicks') for continent in location_dropdown.continents]
)
def update_dropdown(*args):
    ctx = dash.callback_context

    if args is None or not ctx.triggered:
        return "Location", summary_table.all_machines()

    return ctx.triggered_id, summary_table.filtered_by(ctx.triggered_id)`

# Update callback:
`@app.callback(
    [Output("cpu_" + name[0], 'children') for name in settings.machines],
    Input('update', 'n_intervals'),
    [Input(name[0], 'children') for name in settings.machines]
)
def update_cpu(timer, *args):
    return [db.get_machine_cpu(db.get_machine_id(arg)) for arg in args]`

Understanding
My understanding of the error is that once I select a location my ids are no longer present and the callback fails.

Things I have tried:
I tried having the Output/Input generated by stored values, but my understanding here is they’re are only created once on app start up.

I believe my issue is similar to this post but I am not sure about dcc.Store that was proposed.

I have tried setting suppress_callback_exceptions=True but this didn’t get rid of my errors.

Thank you for any help you can give me as this currently has be blocked.

Is anyone able to help me?

Any advice on making my post more visible/detailed to get replies?

1 Like

Hi @QSD449 and welcome to the Dash community :slight_smile:

There isn’t quite enough information in your post to know what’s causing the error. It would be helpful if you could make a complete minimal example including some sample data that we could run and it would duplicate the error you are seeing.

You can learn more about how to do that here: How to Get your Questions Answered on the Plotly Forum

3 Likes

Hi @AnnMarieW, thank you for getting back to me.

Below is some rough code with that shows the issue I am having.

As explained previously, the app works fine until you select a location and then errors show because it cannot find the ids.

import dash
from dash import html, dcc, Input, Output
import dash_bootstrap_components as dbc
import pandas as pd

data = {
    "Location": ["Europe", "Europe", "Asia", "Europe", "America", "Asia", "Europe"],
    "Name": ["Test-PC-0" + str(number) for number in range(7)],
    "Status": ["Online", "Offline", "Online", "Offline", "Online", "Offline", "Online"],
}

machines = pd.DataFrame(data)
locations = machines.Location.unique()
machine_names = machines.Name.unique()

dropdown = dbc.DropdownMenu(
    id="location_selector",
    label="Location",
    children=[dbc.DropdownMenuItem(location, id=location) for location in locations]
)

table_header = [
    html.Thead(html.Tr(
        [html.Th("Name"), html.Th("Status")]))
]


def all_machines():
    table_body = create_rows(machines)

    return table_header + table_body


def filtered_by(location):
    filtered = machines.loc[(machines['Location'] == location)]
    table_body = create_rows(filtered)

    return table_header + table_body


def create_rows(machine_list):
    rows = []

    for i in range(len(machine_list)):
        row = html.Tr([html.Td(machines.loc[i, "Name"], id=machines.loc[i, "Name"]),
                       html.Td(machines.loc[i, "Status"], id="status_"+machines.loc[i, "Name"])])
        rows.append(row)

    table_body = [html.Tbody(rows)]

    return table_body


table_content = all_machines()
table = dbc.Table(table_content, id="summary_table", bordered=True, hover=True, striped=True)


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

app.layout = dbc.Container(
    [
        dcc.Interval(id='update', n_intervals=0, interval=1000 * 3),
        dropdown,
        html.Br(),
        table,
    ]
)


def swap_status(name):
    idx = machines.index[machines["Name"] == name]
    old = machines.iloc[idx]["Status"]

    if old.values[0] == 'Online':
        machines.loc[idx, 'Status'] = "Offline"
        return "Offline"
    else:
        machines.loc[idx, 'Status'] = "Online"
        return "Online"


@app.callback(
    Output('location_selector', 'label'),
    Output('summary_table', 'children'),
    [Input(location, 'n_clicks') for location in locations]
)
def update_dropdown(*args):
    ctx = dash.callback_context

    if args is None or not ctx.triggered:
        return "Location", all_machines()

    return ctx.triggered_id, filtered_by(ctx.triggered_id)


@app.callback(
    [Output("status_" + name, 'children') for name in machine_names],
    Input('update', 'n_intervals'),
    [Input(name, 'children') for name in machine_names]
)
def update_status(timer, *args):
    return [swap_status(arg) for arg in args]


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

HI @QSD449

I think that is because you filter by continent and then the available components change. At startup, all components (machines) are available, but if you switch to Europe for example, you recreate your table and then there is no Test-PC-04 but you want to use it as input in your static list comprehension.

I think pattern matching callbacks would be better for this usecase because then you could use

Input({'type': 'Test-PC', 'index': ALL, 'children') 

Hi @AIMPED, thank you for your reply.

What would I be replacing in my example with the code you suggested?

Could you explain, what this app is supposed to do? Right now you are changing the Status from Offline to Online in n_intervals if I understand correctly.

EDIT: I don’t think you can make this work even with pattern matching callbacks. The problem is, that you are constantly recreating the table content instead of hiding the information based on your drop down.

Here is a quick & dirty prototype you might consider as starting point. I changed a lot of your initial code, so I’m not even sure, if it meets your needs. Basically instead of recreating the table each time, I hide rows depending on the drop down selection. Pattern matching callbacks are not necessary in this case, but I included them to show how the syntax would look like.

import dash
from dash import html, dcc, Input, Output, ALL, ctx
import dash_bootstrap_components as dbc
import pandas as pd
import uuid

data = {
    "Location": ["Europe", "Europe", "Asia", "Europe", "America", "Asia", "Europe"],
    "Name": [f"Test-PC-{number}" for number in range(7)],
    "Status": ["Online", "Offline", "Online", "Offline", "Online", "Offline", "Online"],
}

machines = pd.DataFrame(data)
locations = machines.Location.unique()
machine_names = machines.Name.unique()

initial_state = {name: state for name, state in zip(machine_names, machines.Status)}

dropdown = dbc.DropdownMenu(
    id="location_selector",
    label="Location",
    children=[dbc.DropdownMenuItem(location, id=location) for location in locations]
)

table_header = [
    html.Thead(html.Tr(
        [html.Th("Name"), html.Th("Status")]))
]


def extract_indices(m_names):
    return [int(num[-1]) for num in m_names]


def all_machines():
    table_body = create_rows(machine_names)

    return table_header + table_body


def filtered_by(location):
    return machines.Name.loc[(machines['Location'] == location)]


def create_rows(m_list):
    # extract index from name
    indices = extract_indices(m_list)

    # set variable
    rows = []

    for idx, name in zip(indices, m_list):
        row = html.Tr(
            id={'type': 'row', 'index': idx},
            children=[
                html.Td(name, id={'type': 'Test-PC-', 'index': idx}),
                html.Td(name, id={'type': 'status_Test-PC-', 'index': idx})
            ]
        )
        rows.append(row)

    table_body = [html.Tbody(rows)]

    return table_body


# create table
table_content = all_machines()
table = dbc.Table(table_content, id="summary_table", bordered=True, hover=True, striped=True)

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

app.layout = dbc.Container(
    [
        dcc.Interval(id='update', n_intervals=0, interval=1000 * 5),
        dropdown,
        html.Br(),
        table,
    ]
)


# def swap_status(name):
#     idx = machines.index[machines["Name"] == name]
#     old = machines.iloc[idx]["Status"]
#
#     if old.values[0] == 'Online':
#         machines.loc[idx, 'Status'] = "Offline"
#         return "Offline"
#     else:
#         machines.loc[idx, 'Status'] = "Online"
#         return "Online"


@app.callback(
    Output('location_selector', 'label'),
    [Input(location, 'n_clicks') for location in locations]
)
def update_dropdown(*args):
    trigger = ctx.triggered_id
    if args is None or not trigger:
        return "Location"
    return trigger


@app.callback(
    Output({'type': 'status_Test-PC-', 'index': ALL}, 'children'),
    Output({'type': 'row', 'index': ALL}, 'hidden'),
    Input('update', 'n_intervals'),
    Input("location_selector", 'label'),
)
def update_status(timer, selected_location):
    # get the machine names corresponding to the location
    names = filtered_by(selected_location)

    # index is the last char of the machine name
    indices = extract_indices(names)

    # set the current status to initial values
    current_status = [initial_state[name] for name in machine_names]

    # prepare list for row visibility
    hide_rows = [True for _ in machine_names]

    for idx, item in enumerate(current_status):
        if idx in indices:
            current_status[idx] = str(uuid.uuid4())
            # ^^ assign new value
            hide_rows[idx] = False

    # this is actually just for startup: show all rows
    if selected_location == 'Location':
        hide_rows = [False for _ in hide_rows]

    return current_status, hide_rows


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

1 Like

I have not had time to fully digest your reply and code changes.
But I wanted to let you know I have seen your reply.

Thank you for your help

1 Like

Hi @AIMPED

Thank you again for getting back to me. I have a few questions if that’s ok.

The main purpose of the app will be live monitoring with rows of machines, with multiple columns being updated/refreshed via MQTT messages.
I have a number of features I wish to include, the first being filtering the rows of machines.

What is the best practice when refreshing/updating multiple cells of the table via callbacks would you suggest a callback for each column or refresh the whole table?
i.e. The CPU usage changing but the status likely remaining the same

Do this hidden rows still refresh their values? Does this impact on performance?
Also, when the rows are unhidden what value would they show? The last updated value before being filtered? But I assume I could handle this by updating the pandas dataframe and querying it.

They certainly look a lot cleaner when passing in multiple ids rather than my list comprehension solution.
I guess they work the same when used as an input. Is there a best practice for only updating a value when it changes rather than updating all values at the same time?

Thank you for your code and helpful comments. I split the callback so the location filtering and status updating were separate, this made it so the status now refresh on app load and the location doesn’t require in interval input.
What do you think?

import dash
from dash import html, dcc, Input, Output, ALL, ctx
import dash_bootstrap_components as dbc
import pandas as pd
import uuid

data = {
    "Location": ["Europe", "Europe", "Asia", "Europe", "America", "Asia", "Europe"],
    "Name": [f"Test-PC-{number}" for number in range(7)],
    "Status": [str(uuid.uuid4()) for _ in range(7)],
}

machines = pd.DataFrame(data)
locations = machines.Location.unique()
machine_names = machines.Name.unique()

initial_state = {name: state for name, state in zip(machine_names, machines.Status)}

dropdown = dbc.DropdownMenu(
    id="location_selector",
    label="Location",
    children=[dbc.DropdownMenuItem(location, id=location) for location in locations]
)

table_header = [
    html.Thead(html.Tr(
        [html.Th("Name"), html.Th("Status")]))
]


def extract_indices(m_names):
    return [int(num[-1]) for num in m_names]


def all_machines():
    table_body = create_rows(machine_names)

    return table_header + table_body


def filtered_by(location):
    if location == 'Location':
        return machines.Name
    else:
        return machines.Name.loc[(machines['Location'] == location)]


def create_rows(m_list):
    # extract index from name
    indices = extract_indices(m_list)

    # set variable
    rows = []

    for idx, name in zip(indices, m_list):
        row = html.Tr(
            id={'type': 'row', 'index': idx},
            children=[
                html.Td(name, id={'type': 'Test-PC-', 'index': idx}),
                html.Td(initial_state[name], id={'type': 'status_Test-PC-', 'index': idx})
            ]
        )
        rows.append(row)

    table_body = [html.Tbody(rows)]

    return table_body


# create table
table_content = all_machines()
table = dbc.Table(table_content, id="summary_table", bordered=True, hover=True, striped=True)

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

app.layout = dbc.Container(
    [
        dcc.Interval(id='update', n_intervals=0, interval=1000 * 5),
        dropdown,
        html.Br(),
        table,
    ]
)


@app.callback(
    Output('location_selector', 'label'),
    [Input(location, 'n_clicks') for location in locations]
)
def update_dropdown(*args):
    trigger = ctx.triggered_id
    if args is None or not trigger:
        return "Location"
    return trigger


@app.callback(
    Output({'type': 'row', 'index': ALL}, 'hidden'),
    Input("location_selector", 'label'),
)
def update_filter(selected_location):
    # get the machine names corresponding to the location
    names = filtered_by(selected_location)

    # index is the last char of the machine name
    indices = extract_indices(names)

    # set the current status to initial values
    current_status = [initial_state[name] for name in machine_names]

    # prepare list for row visibility
    hide_rows = [True for _ in machine_names]

    for idx, item in enumerate(current_status):
        if idx in indices:
            hide_rows[idx] = False

    # this is actually just for startup: show all rows
    if selected_location == 'Location':
        hide_rows = [False for _ in hide_rows]

    return hide_rows


@app.callback(
    Output({'type': 'status_Test-PC-', 'index': ALL}, 'children'),
    Input('update', 'n_intervals'),
    Input("location_selector", 'label'),
)
def update_status(timer, location):
    # get the machine names corresponding to the location
    names = filtered_by(location)

    # index is the last char of the machine name
    indices = extract_indices(names)

    # set the current status to initial values
    current_status = [initial_state[name] for name in machine_names]

    for idx, item in enumerate(current_status):
        if idx in indices:
            current_status[idx] = str(uuid.uuid4())

    return current_status


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

Thank you once again for your time and continued help, it is greatly appreciated.

Hi @AIMPED, have you seen my reply? Are you able to answer my follow up questions please?