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.

Result

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

Answered By: hoa tran
Categories: questions Tags: ,
Answers are sorted by their score. The answer accepted by the question owner as the best is marked with
at the top-right corner.