Closed Casper-Guo closed 3 months ago
@Casper-Guo Can you provide complete sample code that reliably reproduces the problem.
Here is the repo that contains the code for the app. The buggy version is on the dashboard-add-gap
branch.
Build with pip install -e .
and then run the app. Once the app is opened, most input to the dropdowns in the first row will trigger the buggy behavior. To reproduce consistently, select 2024, "Belgian Grand Prix" (the last option), and "Race" before hitting the load session button.
I briefly tried minimizing the app but this bug seems quite sensitive to removing any components. I have reproduced the entire file here but let me know if you would like any clarification or assistance with minimizing.
@Casper-Guo - It's necessary for you to provide a complete minimal example that reproduces the issue. What you provided has way too much code and will take too long to debug. You can find more information on how to make a good example on the forum: https://community.plotly.com/t/how-to-get-your-questions-answered-on-the-plotly-forum/40551
@Casper-Guo Change this line, extend()
just return None
:
Apologies for that. I was just trying to show the behavior and see if it is something easily recognizable.
Here is a substantially minimized version that has the same behavior. Fastf1
is the only external dependency.
"""Dash app layout and callbacks."""
import dash_bootstrap_components as dbc
import fastf1 as f
from dash import Dash, Input, Output, callback, dcc
# setup
min_session = f.get_session(2024, 14, "R")
min_session.load(telemetry=False)
laps = min_session.laps
MIN_LAPS = laps[["Time", "PitInTime", "PitOutTime"]]
scatter_y_options = [
{"label": "Lap Time", "value": "LapTime"},
]
scatter_y_dropdown = dcc.Dropdown(
options=scatter_y_options,
value="LapTime",
clearable=False,
id="scatter-y",
)
app = Dash(
__name__,
external_stylesheets=[dbc.themes.SANDSTONE],
)
app.layout = dbc.Container(
[
dbc.Button(
children="Load Session / Reorder Drivers",
n_clicks=0,
color="success",
id="load-session",
),
dcc.Store(id="laps"),
scatter_y_dropdown,
]
)
@callback(
Output("laps", "data"),
Input("load-session", "n_clicks"),
prevent_initial_call=True,
)
def get_session_laps(
_: int, # ignores actual_value of n_clicks
) -> dict:
"""Save the laps of the selected session into browser cache."""
df = MIN_LAPS
df = df.drop(columns=["Time", "PitInTime", "PitOutTime"])
return df.to_dict()
@callback(
Output("scatter-y", "options"),
Input("laps", "data"),
prevent_initial_call=True,
)
def set_y_axis_dropdowns(data: dict) -> list[dict[str, str]]:
"""Update y axis options based on the columns in the laps dataframe."""
gap_cols = filter(lambda x: x.startswith("Gap"), data.keys()) # should be empty
gap_col_options = [{"label": col, "value": col} for col in gap_cols]
return scatter_y_options.extend(gap_col_options)
if __name__ == "__main__":
app.run(debug=True)
Apologies for that. I was just trying to show the behavior and see if it is something easily recognizable.
Here is a substantially minimized version that has the same behavior.
Fastf1
is the only external dependency."""Dash app layout and callbacks.""" import dash_bootstrap_components as dbc import fastf1 as f from dash import Dash, Input, Output, callback, dcc # setup min_session = f.get_session(2024, 14, "R") min_session.load(telemetry=False) laps = min_session.laps MIN_LAPS = laps[["Time", "PitInTime", "PitOutTime"]] scatter_y_options = [ {"label": "Lap Time", "value": "LapTime"}, ] scatter_y_dropdown = dcc.Dropdown( options=scatter_y_options, value="LapTime", clearable=False, id="scatter-y", ) app = Dash( __name__, external_stylesheets=[dbc.themes.SANDSTONE], ) app.layout = dbc.Container( [ dbc.Button( children="Load Session / Reorder Drivers", n_clicks=0, color="success", id="load-session", ), dcc.Store(id="laps"), scatter_y_dropdown, ] ) @callback( Output("laps", "data"), Input("load-session", "n_clicks"), prevent_initial_call=True, ) def get_session_laps( _: int, # ignores actual_value of n_clicks ) -> dict: """Save the laps of the selected session into browser cache.""" df = MIN_LAPS df = df.drop(columns=["Time", "PitInTime", "PitOutTime"]) return df.to_dict() @callback( Output("scatter-y", "options"), Input("laps", "data"), prevent_initial_call=True, ) def set_y_axis_dropdowns(data: dict) -> list[dict[str, str]]: """Update y axis options based on the columns in the laps dataframe.""" gap_cols = filter(lambda x: x.startswith("Gap"), data.keys()) # should be empty gap_col_options = [{"label": col, "value": col} for col in gap_cols] return scatter_y_options.extend(gap_col_options) if __name__ == "__main__": app.run(debug=True)
The crux of the problem is the same, options received an illegal null value in the callback function.
@CNFeffery You are correct about the fix. Much appreciated. Always forgets that append
and extend
return None
.
The error shown by the minimal example in the debug environment is just node is null
with super cryptic and unhelpful traceback. Is this a dash implementation limitation or something owing to the dcb package?
@Casper-Guo This error for an array type parameter occurs before passing in a specific component rendering, in my opinion, the unclear error prompt in this scenario is an issue that needs to be improved in the dash-renderer
logic, !node?.length
is better.
Thank you so much for helping improve the quality of Dash!
We do our best to catch bugs during the release process, but we rely on your help to find the ones that slip through.
Describe your context Please provide us your environment, so we can easily reproduce the issue.
pip list | grep dash
belowif frontend related, tell us your Browser, Version and OS
Describe the bug
A clear and concise description of what the bug is.
Expected behavior
The app has the following layout:
The dataflow within callbacks is unidirectional from
session
toload-session
using the following callback:I have noticed that sometimes the
n_click
property ofload-session
, which starts from 0, goes to 1 and drops back down to 0. Simultaneously, thevalue
property ofsession
would revert toNone
which is what I initialize it with. This is all without any callback firing.The line causing this behavior is editing a cached (with
dcc.store
) dataframe and doesn't trigger any callback. Might this have something to do with the browser cache?