plotly / dash-table

OBSOLETE: now part of https://github.com/plotly/dash
https://dash.plotly.com
MIT License
419 stars 74 forks source link

[BUG] DataTable 'data_previous' is not correctly set, when 'data' is modified by callback. #655

Open fpriebe opened 4 years ago

fpriebe commented 4 years ago

I recently discovered that my change management for an editable DataTable did not work as expected. The problem is that the 'data_previous' value of the DataTable is not modified if you change the data of the table with a callback.

The reference states that the 'data_previous' value is updated both ways: by user changes using the editable function and by callbacks manipulating the table data.

Here is a minimal example, which shows the bug: I discovered the bug in dash 1.4.0 and dash-table 4.4.0, but it is also present in the latest versions of dash and dash-table.

import json

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

app = dash.Dash(__name__)

table_data = [
    {'A': 1, 'B': 2, 'C': 3},
    {'A': 3, 'B': 4, 'C': 5},
]
columns = [{'id': 'A', 'name': 'A'},
           {'id': 'B', 'name': 'C'},
           {'id': 'C', 'name': 'C'}]

app.layout = html.Div(children=[
    html.H1(children='Hello Dash'),

    html.Div(children='''
        Dash: A web application framework for Python.
    '''),

    html.Button('Add Row', id='adding_rows_button', n_clicks=0),

    dt.DataTable(
        id='test_table',
        columns=columns,
        data=table_data,
        editable=True,
        row_deletable=True,
    ),

    html.Div(id='out_div'),
    html.P(),
    html.Div(id='out_div_prev')
])

@app.callback(
    Output('test_table', 'data'),
    [Input('adding_rows_button', 'n_clicks')],
    [State('test_table', 'data')])
def add_row(n_clicks, rows):
    if n_clicks:
        rows.append({'A': 3, 'B': 4, 'C': 5})
    return rows

@app.callback(
    [Output("out_div", "children"),
     Output("out_div_prev", "children")],
    [Input("test_table", "data")],
    [State("test_table", "data_previous")])
def show_prev(data, data_prev):
    return json.dumps(data), json.dumps(data_prev)

if __name__ == '__main__':
    app.run_server(debug=True)
Marc-Andre-Rivet commented 4 years ago

First thought was for a simple change where the table internally checks whether newData == oldData and triggers a data_previous update based on that. But I don't think this can work, because:

@app.callback(
  Output('table', 'data'),
  [Input('table', 'data_previous')],
  [State('table', 'data')]
)
def some_callback(previous, current):
  # do something

Forcing the table to update data_previous when data is updated would cause an infinite loop for the callback defined above.

Some user action caused data --> data* and data_previous=data The callback does (data, data*) --> data** The table checks data** != data* and assigns data_previous=data* The callback is triggered again, potentially ad finitum.


For the scenario above, it should be possible to combine both callbacks into a single callback that updates data and uses the baseline and new value to update the divs at the same time. This will also have the added advantage of avoiding a 2nd roundtrip with data and data_previous.


In light of this, it might be necessary to define data_previous as the previous value of data, when data was updated by the table itself.