Black Lives Matter. Please consider donating to Black Girls Code today.

Dash_table updates with chained callbacks

I am wondering about some oddities around dash_table updates with chained callbacks. Not sure if I can improve it within my code or if it is a bug. So every help is appreciated.

I have created an example which shows my problem, see code below.
After pressing the start button I would expect that

  1. ‘That is output1’ appears
  2. ‘That is output2’ appears
  3. The data in the table changes
  4. ‘That is output3’ appears
    But only 1.) happens.

If I remove the callback with get_output3, at least 1.) - 3.) happen.
I have recognized an error message when I include the callback with update_table:
Uncaught (in promise) TypeError: Cannot read property 'forEach' of null

Maybe the problem is connected to topic here.

Thanks in advance :slight_smile:

app = dash.Dash(__name__)

app.layout = html.Div(
    [
        html.Button(id='start-button', n_clicks=0, children='Start'),
        html.Div(id='output1'),
        html.Div(id='output2'),
        dash_table.DataTable(
            id='test_table',
            columns=[{"name": 'column1', "id": 'column1'},
            	   {"name": 'column2', "id": 'column2'}],
            data=[{u'column1': 1, u'column2': 2}],
            content_style = 'fit'
        ),
        html.Div(id='output3')
    ]
)

@app.callback(
    Output('output1', 'children'),
    [Input('start-button', 'n_clicks')])
def get_output1(n_clicks):
    print('get output1')
    if n_clicks > 0:
        print('check')
        return 'That is output1'

@app.callback(
    Output('output2', 'children'),
    [Input('output1', 'children')])
def get_output2(output):
    print('get output2')
    if output is not None:
        print('check')
        return 'That is output2'

@app.callback(Output('test_table', 'data'),
            [Input('output2', 'children')])
def update_table(output):
    print('update table')
    if output is not None:
        print('check')
        data=[{u'column1': 'test1', u'column2': 'test2'}]
        return data

@app.callback(
    Output('output3', 'children'),
    [Input('test_table', 'data_timestamp')])
def get_output3(output):
    print('get output3')
    if output is not None:
        print('check')
        return 'That is output3'

Hi @Schult1, I think the problem is that you use data_timestamp, which would trigger the callback when you edit the table on the page and when the table is set to editable=True.

It won’t change if you update the table in the function so it won’t trigger the output3.

Hi @caiyij,
thanks for your support.
Unfortunately I am not totally satisfied, because if that is true what you have said, why does my code only run until step 1(print output1)?
And why if I delete step 4 does it run until step 3?

Maybe more important: Do you have a suggestion how the example should work?
Thanks again :slight_smile:

Hi everyone!
I’ve just started to play around with dash, like it a lot but soon ran into a related problem (I think).
What’s bothering me is that I find several “unresolved” topics here in the forum, all seemingly related to “callbacks not firing”, if you use more than 1 callback.

So once again my (possibly stupid) newbie question: Has this issue ever been resolved?
Please give me a hint!

Thanks in advance for your support!

hey @miby - many of these issues have been solved on our end. if you are seeing a new issue, can you share a simple reproducible example? if it’s a bug, then we’ll fix it!

Hi,
ok, here we go…Sorry, but I wasn’t able to strip it down further.
The below example reads a csv-file with date and various columns as in

t;X;Y
8.3.2020;5;1
9.3.2020;5;2
10.3.2020;6;5
11.3.2020;13;12
12.3.2020;20;26
13.3.2020;29;37

The columns (apart from date) are passed to the dash_table.DataTable as rows.
I want to be able to select the rows which are then visualized in the dash_core_components.Graph
Displaying the graph works fine all the time.
Now on 1st load of the program the DataTable is not showing the correct values when I load the csv.
If I then stop the and restart the dash-server (Strg-C and re-run in the same ipython session) everything works fine as intended.

Any help appreciated!!

Greetings! Stay safe everyone!

# -*- coding: utf-8 -*-

import dash
from dash.dependencies import Input, Output, State
import dash_core_components as dcc
import dash_html_components as html
import dash_table

import base64
import pandas as pd
import io
# import json


df_init = pd.DataFrame(['leer'], columns=['channels'])


app = dash.Dash(__name__)
# app.config['suppress_callback_exceptions'] = True

app.layout = html.Div([
        # html.Div(id='waitfor'),
        dcc.Upload(
            id='upload',
            children=html.Div([
                'Drag and Drop or ',
                html.A('Select a File')
            ]),
            style={
                'width': '100%',
                'height': '60px',
                'lineHeight': '60px',
                'borderWidth': '1px',
                'borderStyle': 'dashed',
                'borderRadius': '5px',
                'textAlign': 'center',
                'margin': '10px'
            }
        ),
        html.Hr(),
        dcc.Graph(id='graph'),
        html.Hr(),
        html.Div([
            dash_table.DataTable(id='table',
                #columns=[{"name": i, "id": i} for i in df_init.columns],
                columns=[{"name": 'channels', "id": 'channels'}],
                data=[],
                # data=df_init.to_dict('records'),
                row_selectable="multi")]),
        html.Div(id='error'),
        # Hidden div inside the app that stores data
        html.Div(id='datastore_table', style={'display': 'none'}),
        html.Div(id='datastore_graph', style={'display': 'none'})
    ])


def extract_data(data):
    # extract filename + dataframe from string data
    pos = data.find('\n')
    filename = data[:pos]
    data = data[pos+1:]
    df = pd.read_json(data, orient='split')
    return filename, df


@app.callback([Output('datastore_table', 'children'),
               Output('datastore_graph', 'children'),
               Output('error', 'children')],
              [Input('upload', 'contents'),
               Input('upload', 'filename')])
def data_changed(contents, filename):
    if contents is None:
        return dash.no_update, dash.no_update, dash.no_update

    df = parse_data(contents, filename)
    if isinstance(df, pd.DataFrame):
        error = ''  # clear error msg
    else:
        error = df  # html.Div with error msg
        data_table = df_init.to_json(date_format='iso', orient='split')
        # print('error: ', data_table)
        dff = pd.DataFrame()
        data_graph = filename + '\n' + dff.to_json(date_format='iso', orient='split')
        return data_table, data_graph, error

    df2 = pd.DataFrame(df.columns[1:], columns=['channels'])
    data_table = df2.to_json(date_format='iso', orient='split')
    # print('data_changed: data_table: %s' % data_table)

    data_graph = filename + '\n' + df.to_json(date_format='iso', orient='split')

    return data_table, data_graph, error


def parse_data(contents, filename):
    content_type, content_string = contents.split(',')
    decoded = base64.b64decode(content_string)
    try:
        if 'csv' in filename or 'txt' in filename:
            df = pd.read_csv(
                io.StringIO(decoded.decode('utf-8')),
                delimiter=';', decimal=',')
            # convert excel time to datetime
            # df[df.columns[0]] = pd.TimedeltaIndex(df[df.columns[0]], unit='d') + dt.datetime(1899, 12, 30)
        elif 'xls' in filename:
            df = pd.read_excel(decoded)
            # convert excel time to datetime
            # df[df.columns[0]] = pd.TimedeltaIndex(df[df.columns[0]], unit='d') + dt.datetime(1899, 12, 30)
        else:
            # df = pd.DataFrame()
            # return df
            return html.Div([
                    html.Hr(),
                    html.Div('There was an error processing file %s: ' % filename),
                    html.Div('unknown file type')])
    except Exception as e:
        print("Exception parse_data: ", e)
        return html.Div([
            html.Div('There was an error processing file %s' % filename),
            html.Hr(),
            html.Div(e)
        ])
    return df


@app.callback([Output('table', 'data'),
               Output('table', 'selected_rows')],
              [Input('datastore_table', 'children')],
              [State('table', 'selected_rows')])
def update_table(data, selected_rows):
    if data:
        # print('update_table: data: %s' % data)
        df = pd.read_json(data, orient='split')
    else:
        df = df_init

    dff = df.to_dict('records')  # df.to_dict('rows')
    if selected_rows is None:
        rows = []  # dash.no_update
    else:
        # print('update_table, sel:%s, rows:%s' % (selected_rows,rows))
        col = len(df.columns)
        rows = []
        for i in selected_rows:
            if i <= col:
                rows.append(i)
    print('update_table, data: %s, rows:%s' % (dff, rows))
    return dff, rows


@app.callback(Output('graph', 'figure'),
              [Input('datastore_graph', 'children'),
               Input('table', 'selected_rows')])
def update_graph(data, selected_rows):
    filter_rows = False
    ctx = dash.callback_context
    if ctx.triggered:
        """ctx_msg = json.dumps({
            'states': ctx.states,
            'triggered': ctx.triggered,
            'inputs': ctx.inputs
        }, indent=2)
        # print('ctx_msg: ', ctx_msg)
        """

        for trig in ctx.triggered:
            id = trig['prop_id'].split('.')[1]
            if id == 'selected_rows' and selected_rows is not None:
                # print("id selected rows: %s" % selected_rows)
                filter_rows = True
                break

    if data:
        filename, df = extract_data(data)

        if filter_rows:  # update graph
            if selected_rows == []:
                df = pd.DataFrame()
            else:  # select column 0 (time) & selected
                sel = [0]
                # print('col: ', df.columns)
                col = len(df.columns) - 1
                # print('len col:', col + 1)
                for i in selected_rows:
                    if i < col:
                        sel.extend([i+1])
                # print('update graph sel: ', sel)
                df = df.iloc[:, sel]
        else:  # called by datastore graph
            pass  # print('update graph: datastore graph changed')

        if not isinstance(df, pd.DataFrame):
            df = pd.DataFrame()
            filename = 'Error: ' + filename
        data = [dict(
                        x=df[df.columns[0]],
                        y=df[df.columns[i+1]],
                        name=col,
                        ) for i, col in enumerate(df.columns[1:])]
        # print('update_graph: ', data)

        return {'data': data,
                'layout': dict(
                        title=filename,
                        showlegend=True,
                        legend=dict(
                                x=0,
                                y=1.0
                                ),
                        margin=dict(l=40, r=0, t=40, b=30)
                               )
                }
    else:
        return dash.no_update


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

@chriddyp: Can you please take a look at the above? Thx

Is the problem present with Dash 1.6.1?

Yes. I’m using 1.8.0

So it’t present both with 1.8.0 and 1.6.1? I am asking because a bug was introduced after 1.6.1, which causes problems with callbacks in some cases.

Hi Emil,
your suspicion is correct. I verified that my code is working correctly with Dash 1.6.1.
I guess I can live with this workaround for now.

So my problem seems to be related to your issue:

Did you ever get feedback to your issue? Is someone working on it?

Thanks for your help, @Emil !!

Great to hear that it works! Yes, the problem has actually already been solved in one of the current dash feature branches,

However, as you have noted, the problem is present in the latest official dash release. Hence, at the moment, your only options are to stay with Dash 1.6.1 or to build Dash from source. I ended up doing the latter,

1 Like