Plotly Dash `px.bar` not updating in callback function
Question:
The graph in the app.layout
is not updated at callback
. Code is shown below, however, it requires a csv
file of data that is turned into a pandas.Dataframe
. I will post the first 26 rows here for reference. (This is a subset of results of the Houston Marathon that took place on the Saturday of the 15th of Jan!)
The code takes datetime.time
data, bins it into 5 minutes of length, before creating a histogram in px.bar
. Given a user’s time input – their percentile in the sample distribution is visualized.
place,name,time
1,Dominic Ondoro,02:10:36
2,Tsedat Ayana,02:10:37
3,Teshome Mekonen,02:11:05
4,Parker Stinson,02:12:11
5,Tyler Pennel,02:12:16
6,Kenta Uchida,02:14:13
7,James Ngandu,02:14:28
8,Alvaro Abreu,02:14:28
9,Kevin Salvano,02:16:39
10,Tyler Pence,02:16:44
11,Phillip Reid,02:16:46
12,Mark Messmer,02:17:27
13,Shadrack Biwott,02:17:36
14,Hitomi Niiya,02:19:24
15,David Fuentes,02:20:28
16,Patrice Labonte,02:20:49
17,Joseph Niemiec,02:21:06
18,Jesse Joseph,02:21:09
19,Aaron Davidson,02:21:40
20,Stan Linton,02:21:43
21,Mitchell Klingler,02:21:53
22,Michael Babinec,02:22:52
23,Tom Derr,02:23:07
24,Matt Dynan,02:23:33
25,Alexander Diltz,02:23:41
CSV_FILE_NAME = 'houston_marathon_2023.csv'
# Contents of the CSV file are above
def parse_data_as_df():
df = pd.read_csv(CSV_FILE_NAME, encoding='latin-1')
df['time'] = pd.to_datetime(df['time']).dt.time
return df
def create_histogram(cutoff_time):
BIN_SIZE_MINUTES = 5
MIN_TIME = dt.time(hour=2, minute=0, second=0)
MAX_TIME = dt.time(hour=6, minute=10, second=0)
time_increment = MIN_TIME
time_bins = {}
data = parse_data_as_df()
# get bins for original
while time_increment <= MAX_TIME:
next_time = dt.time(hour=time_increment.hour + int((time_increment.minute + BIN_SIZE_MINUTES) / 60),
minute=(time_increment.minute + BIN_SIZE_MINUTES) % 60,
second=0)
time_bins[time_increment] = 0
for a_time in data['time']:
if time_increment <= a_time < next_time:
time_bins[time_increment] += 1/len(data['time'])
time_increment = next_time
df = pd.DataFrame({'bin_times': time_bins.keys(), 'percent': time_bins.values()})
df["color"] = np.select(
[df["bin_times"].lt(cutoff_time)],
["#fd7e14"],
"#158cba",
)
load_figure_template(['lumen'])
fig = px.bar(
df,
x="bin_times",
y="percent",
color="color",
color_discrete_map={
"#fd7e14": "#fd7e14",
"#158cba": "#158cba",
},
template='lumen',
hover_data={
'color': False,
'percent': ':.1%'
}
)
fig.update_xaxes(tickformat='%H:%M',
ticktext=[d.strftime('%H:%M') for d in df['bin_times']])
fig.update_yaxes(tickformat='0%',
range=[0, 0.05])
fig.update_layout(xaxis_title="Time (HH:MM)",
yaxis_title="Density (%)",
showlegend=False,
bargap=0.025)
return fig
def create_dash_application():
APP_STYLESHEET = "https://cdn.jsdelivr.net/npm/[email protected]/dist/lux/bootstrap.min.css"
DEFAULT_CUTOFF_TIME = dt.time(hour=3)
dash_app = Dash(__name__, external_stylesheets=[APP_STYLESHEET])
dash_app.layout = html.Div([
html.Br(),
dcc.Graph(id='histogram_graph',
figure=create_histogram(DEFAULT_CUTOFF_TIME),
config={'displayModeBar': False},
animate=True),
html.Br(),
html.Div(
[
html.B('Hours: '),
dcc.Input(id='time_hours', value='', type='text', style={'width': '25%'}),
]),
html.Br(),
html.Div(
[
html.B('Minutes: '),
dcc.Input(id='time_minutes', value='', type='text', style={'width': '25%'}),
]),
html.Br(),
html.Div(
[
html.B('Seconds: '),
dcc.Input(id='time_seconds', value='', type='text', style={'width': '25%'}),
]),
html.Br(),
html.Button(children='Analyse', id='button_submit', n_clicks=0, style={'width': '25%'}, type='button'),
], style={'width': '75%'})
@dash_app.callback(
Output('histogram_graph', 'figure'),
[Input('button_submit', 'n_clicks'),
Input('time_hours', 'value'),
Input('time_minutes', 'value'),
Input('time_seconds', 'value')],
prevent_initial_call=True)
def update_output(n_clicks, the_hours, the_minutes, the_seconds):
if n_clicks > 0 and the_hours != '' and the_minutes != '' and the_seconds != '':
in_time = dt.time(hour=int(the_hours), minute=int(the_minutes), second=int(the_seconds))
# Something wrong here, as histogram is correctly generated but not changed in Dash App
return create_histogram(cutoff_time=in_time)
else:
return None
dash_app.run_server(debug=True)
create_dash_application()
I’ve isolated the issue to the callback
returning a correct fig
, that is not being updated in the app.layout
. It is not an issue in the creation of fig
, as confirmed by observing fig.show()
.
I am expecting fig
in dcc.Graph
to be updated.
What am I missing?
Answers:
If you want to use the button to run the inputs, you need to switch those inputs to State
. Please use below code to run:
import datetime as dt
import dash
CSV_FILE_NAME = 'houston_marathon_2023.csv'
# Contents of the CSV file are above
def parse_data_as_df():
df = pd.read_csv(CSV_FILE_NAME, encoding='latin-1')
df['time'] = pd.to_datetime(df['time']).dt.time
return df
def create_histogram(cutoff_time):
BIN_SIZE_MINUTES = 5
MIN_TIME = dt.time(hour=2, minute=0, second=0)
MAX_TIME = dt.time(hour=6, minute=10, second=0)
time_increment = MIN_TIME
time_bins = {}
data = parse_data_as_df()
# get bins for original
while time_increment <= MAX_TIME:
next_time = dt.time(hour=time_increment.hour + int((time_increment.minute + BIN_SIZE_MINUTES) / 60),
minute=(time_increment.minute + BIN_SIZE_MINUTES) % 60,
second=0)
time_bins[time_increment] = 0
for a_time in data['time']:
if time_increment <= a_time < next_time:
time_bins[time_increment] += 1/len(data['time'])
time_increment = next_time
df = pd.DataFrame({'bin_times': time_bins.keys(), 'percent': time_bins.values()})
df["color"] = np.select(
[df["bin_times"].lt(cutoff_time)],
["#fd7e14"],
"#158cba",
)
fig = px.bar(
df,
x="bin_times",
y="percent",
color="color",
color_discrete_map={
"#fd7e14": "#fd7e14",
"#158cba": "#158cba",
},
hover_data={
'color': False,
'percent': ':.1%'
}
)
fig.update_xaxes(tickformat='%H:%M',
ticktext=[d.strftime('%H:%M') for d in df['bin_times']])
fig.update_yaxes(tickformat='0%',
range=[0, 0.05])
fig.update_layout(xaxis_title="Time (HH:MM)",
yaxis_title="Density (%)",
showlegend=False,
bargap=0.025)
return fig
def create_dash_application():
APP_STYLESHEET = "https://cdn.jsdelivr.net/npm/[email protected]/dist/lux/bootstrap.min.css"
DEFAULT_CUTOFF_TIME = dt.time(hour=3)
dash_app = dash.Dash(__name__, external_stylesheets=[APP_STYLESHEET])
dash_app.layout = html.Div([
html.Br(),
dcc.Graph(id='histogram_graph',
figure=create_histogram(DEFAULT_CUTOFF_TIME),
config={'displayModeBar': False},
animate=True),
html.Br(),
html.Div(
[
html.B('Hours: '),
dcc.Input(id='time_hours', value='', type='text', style={'width': '25%'}),
]),
html.Br(),
html.Div(
[
html.B('Minutes: '),
dcc.Input(id='time_minutes', value='', type='text', style={'width': '25%'}),
]),
html.Br(),
html.Div(
[
html.B('Seconds: '),
dcc.Input(id='time_seconds', value='', type='text', style={'width': '25%'}),
]),
html.Br(),
html.Button(children='Analyse', id='button_submit', n_clicks=0, style={'width': '25%'}, type='button'),
], style={'width': '75%'})
@dash_app.callback(
Output('histogram_graph', 'figure'),
[Input('button_submit', 'n_clicks')],
[State('time_hours', 'value'),
State('time_minutes', 'value'),
State('time_seconds', 'value')],
prevent_initial_call=True)
def update_output(n_clicks, the_hours, the_minutes, the_seconds):
if n_clicks > 0 and the_hours != '' and the_minutes != '' and the_seconds != '':
in_time = dt.time(hour=int(the_hours), minute=int(the_minutes), second=int(the_seconds))
# Something wrong here, as histogram is correctly generated but not changed in Dash App
return create_histogram(cutoff_time=in_time)
else:
return None
dash_app.run_server(debug=True)
create_dash_application()
Hope this help
The graph in the app.layout
is not updated at callback
. Code is shown below, however, it requires a csv
file of data that is turned into a pandas.Dataframe
. I will post the first 26 rows here for reference. (This is a subset of results of the Houston Marathon that took place on the Saturday of the 15th of Jan!)
The code takes datetime.time
data, bins it into 5 minutes of length, before creating a histogram in px.bar
. Given a user’s time input – their percentile in the sample distribution is visualized.
place,name,time
1,Dominic Ondoro,02:10:36
2,Tsedat Ayana,02:10:37
3,Teshome Mekonen,02:11:05
4,Parker Stinson,02:12:11
5,Tyler Pennel,02:12:16
6,Kenta Uchida,02:14:13
7,James Ngandu,02:14:28
8,Alvaro Abreu,02:14:28
9,Kevin Salvano,02:16:39
10,Tyler Pence,02:16:44
11,Phillip Reid,02:16:46
12,Mark Messmer,02:17:27
13,Shadrack Biwott,02:17:36
14,Hitomi Niiya,02:19:24
15,David Fuentes,02:20:28
16,Patrice Labonte,02:20:49
17,Joseph Niemiec,02:21:06
18,Jesse Joseph,02:21:09
19,Aaron Davidson,02:21:40
20,Stan Linton,02:21:43
21,Mitchell Klingler,02:21:53
22,Michael Babinec,02:22:52
23,Tom Derr,02:23:07
24,Matt Dynan,02:23:33
25,Alexander Diltz,02:23:41
CSV_FILE_NAME = 'houston_marathon_2023.csv'
# Contents of the CSV file are above
def parse_data_as_df():
df = pd.read_csv(CSV_FILE_NAME, encoding='latin-1')
df['time'] = pd.to_datetime(df['time']).dt.time
return df
def create_histogram(cutoff_time):
BIN_SIZE_MINUTES = 5
MIN_TIME = dt.time(hour=2, minute=0, second=0)
MAX_TIME = dt.time(hour=6, minute=10, second=0)
time_increment = MIN_TIME
time_bins = {}
data = parse_data_as_df()
# get bins for original
while time_increment <= MAX_TIME:
next_time = dt.time(hour=time_increment.hour + int((time_increment.minute + BIN_SIZE_MINUTES) / 60),
minute=(time_increment.minute + BIN_SIZE_MINUTES) % 60,
second=0)
time_bins[time_increment] = 0
for a_time in data['time']:
if time_increment <= a_time < next_time:
time_bins[time_increment] += 1/len(data['time'])
time_increment = next_time
df = pd.DataFrame({'bin_times': time_bins.keys(), 'percent': time_bins.values()})
df["color"] = np.select(
[df["bin_times"].lt(cutoff_time)],
["#fd7e14"],
"#158cba",
)
load_figure_template(['lumen'])
fig = px.bar(
df,
x="bin_times",
y="percent",
color="color",
color_discrete_map={
"#fd7e14": "#fd7e14",
"#158cba": "#158cba",
},
template='lumen',
hover_data={
'color': False,
'percent': ':.1%'
}
)
fig.update_xaxes(tickformat='%H:%M',
ticktext=[d.strftime('%H:%M') for d in df['bin_times']])
fig.update_yaxes(tickformat='0%',
range=[0, 0.05])
fig.update_layout(xaxis_title="Time (HH:MM)",
yaxis_title="Density (%)",
showlegend=False,
bargap=0.025)
return fig
def create_dash_application():
APP_STYLESHEET = "https://cdn.jsdelivr.net/npm/[email protected]/dist/lux/bootstrap.min.css"
DEFAULT_CUTOFF_TIME = dt.time(hour=3)
dash_app = Dash(__name__, external_stylesheets=[APP_STYLESHEET])
dash_app.layout = html.Div([
html.Br(),
dcc.Graph(id='histogram_graph',
figure=create_histogram(DEFAULT_CUTOFF_TIME),
config={'displayModeBar': False},
animate=True),
html.Br(),
html.Div(
[
html.B('Hours: '),
dcc.Input(id='time_hours', value='', type='text', style={'width': '25%'}),
]),
html.Br(),
html.Div(
[
html.B('Minutes: '),
dcc.Input(id='time_minutes', value='', type='text', style={'width': '25%'}),
]),
html.Br(),
html.Div(
[
html.B('Seconds: '),
dcc.Input(id='time_seconds', value='', type='text', style={'width': '25%'}),
]),
html.Br(),
html.Button(children='Analyse', id='button_submit', n_clicks=0, style={'width': '25%'}, type='button'),
], style={'width': '75%'})
@dash_app.callback(
Output('histogram_graph', 'figure'),
[Input('button_submit', 'n_clicks'),
Input('time_hours', 'value'),
Input('time_minutes', 'value'),
Input('time_seconds', 'value')],
prevent_initial_call=True)
def update_output(n_clicks, the_hours, the_minutes, the_seconds):
if n_clicks > 0 and the_hours != '' and the_minutes != '' and the_seconds != '':
in_time = dt.time(hour=int(the_hours), minute=int(the_minutes), second=int(the_seconds))
# Something wrong here, as histogram is correctly generated but not changed in Dash App
return create_histogram(cutoff_time=in_time)
else:
return None
dash_app.run_server(debug=True)
create_dash_application()
I’ve isolated the issue to the callback
returning a correct fig
, that is not being updated in the app.layout
. It is not an issue in the creation of fig
, as confirmed by observing fig.show()
.
I am expecting fig
in dcc.Graph
to be updated.
What am I missing?
If you want to use the button to run the inputs, you need to switch those inputs to State
. Please use below code to run:
import datetime as dt
import dash
CSV_FILE_NAME = 'houston_marathon_2023.csv'
# Contents of the CSV file are above
def parse_data_as_df():
df = pd.read_csv(CSV_FILE_NAME, encoding='latin-1')
df['time'] = pd.to_datetime(df['time']).dt.time
return df
def create_histogram(cutoff_time):
BIN_SIZE_MINUTES = 5
MIN_TIME = dt.time(hour=2, minute=0, second=0)
MAX_TIME = dt.time(hour=6, minute=10, second=0)
time_increment = MIN_TIME
time_bins = {}
data = parse_data_as_df()
# get bins for original
while time_increment <= MAX_TIME:
next_time = dt.time(hour=time_increment.hour + int((time_increment.minute + BIN_SIZE_MINUTES) / 60),
minute=(time_increment.minute + BIN_SIZE_MINUTES) % 60,
second=0)
time_bins[time_increment] = 0
for a_time in data['time']:
if time_increment <= a_time < next_time:
time_bins[time_increment] += 1/len(data['time'])
time_increment = next_time
df = pd.DataFrame({'bin_times': time_bins.keys(), 'percent': time_bins.values()})
df["color"] = np.select(
[df["bin_times"].lt(cutoff_time)],
["#fd7e14"],
"#158cba",
)
fig = px.bar(
df,
x="bin_times",
y="percent",
color="color",
color_discrete_map={
"#fd7e14": "#fd7e14",
"#158cba": "#158cba",
},
hover_data={
'color': False,
'percent': ':.1%'
}
)
fig.update_xaxes(tickformat='%H:%M',
ticktext=[d.strftime('%H:%M') for d in df['bin_times']])
fig.update_yaxes(tickformat='0%',
range=[0, 0.05])
fig.update_layout(xaxis_title="Time (HH:MM)",
yaxis_title="Density (%)",
showlegend=False,
bargap=0.025)
return fig
def create_dash_application():
APP_STYLESHEET = "https://cdn.jsdelivr.net/npm/[email protected]/dist/lux/bootstrap.min.css"
DEFAULT_CUTOFF_TIME = dt.time(hour=3)
dash_app = dash.Dash(__name__, external_stylesheets=[APP_STYLESHEET])
dash_app.layout = html.Div([
html.Br(),
dcc.Graph(id='histogram_graph',
figure=create_histogram(DEFAULT_CUTOFF_TIME),
config={'displayModeBar': False},
animate=True),
html.Br(),
html.Div(
[
html.B('Hours: '),
dcc.Input(id='time_hours', value='', type='text', style={'width': '25%'}),
]),
html.Br(),
html.Div(
[
html.B('Minutes: '),
dcc.Input(id='time_minutes', value='', type='text', style={'width': '25%'}),
]),
html.Br(),
html.Div(
[
html.B('Seconds: '),
dcc.Input(id='time_seconds', value='', type='text', style={'width': '25%'}),
]),
html.Br(),
html.Button(children='Analyse', id='button_submit', n_clicks=0, style={'width': '25%'}, type='button'),
], style={'width': '75%'})
@dash_app.callback(
Output('histogram_graph', 'figure'),
[Input('button_submit', 'n_clicks')],
[State('time_hours', 'value'),
State('time_minutes', 'value'),
State('time_seconds', 'value')],
prevent_initial_call=True)
def update_output(n_clicks, the_hours, the_minutes, the_seconds):
if n_clicks > 0 and the_hours != '' and the_minutes != '' and the_seconds != '':
in_time = dt.time(hour=int(the_hours), minute=int(the_minutes), second=int(the_seconds))
# Something wrong here, as histogram is correctly generated but not changed in Dash App
return create_histogram(cutoff_time=in_time)
else:
return None
dash_app.run_server(debug=True)
create_dash_application()
Hope this help