Persist dash DataTable after user adds rows and changes tab

Hi all,

I am making a multi tab app in which one tab displays a few user-editable DataTable’s. The user has to be able to add and delete rows. The inputs are then dynamically saved in a dcc.Store in the main script for future use in the computation of the model. I have managed to make the user selections persist when changing tabs as long as no rows are added or deleted. However, if the user adds or deletes rows whilst defining inputs, the data does not persist when returning to the tab. How can I make this happen?

Here is a sample of my code:

instr_types = ("N", "O", "P")

arrays = ("Array", "Double")
df_vars = pd.DataFrame([
    ('Name', "N"),
    ('Array/Double', 'Array'),
    ('Initial Value', "0.00")
])

layout = html.Div([
    dcc.Store(id = "stored_variables", storage_type="session"), #actually in index.py
    html.Div(html.H1("Variables")),
    dash_table.DataTable(
        id = 'variables-table',
        data = df_vars.to_dict('records'),
        columns = [
            {"id" : "Name", "name" : "Name", "presentation" : "dropdown"},
            {"id" : "Array/Double", "name" : "Array/Double", "presentation" : "dropdown"},
            {"id" : "Initial Value", "name" : "Initial Value", "type" : "numeric"}
            ],
        dropdown = {
            "Name" : {
                "options" : [
                { "label" : "g_{}".format(i), "value" : "g_{}".format(i)} for i in instr_types
                ] },
            "Array/Double" : {
                "options" : [
                    { "label" : j, "value" : j} for j in arrays
                ]
            }
        },
    editable=True,
    row_deletable=True,
    persistence = True,
    persisted_props = ['columns.name', 'data', 'filter_query', 'hidden_columns', 'page_current', 'selected_columns', 'selected_rows', 'sort_by'],
    persistence_type = "session"
    ),
    html.Div(id='variables-table-dropdown-container'),
    html.Button('Add Row', id='variables-addrows-button', n_clicks=0)
])

@app.callback(Output('variables-table', 'data'),
                Input('variables-addrows-button', 'n_clicks'),
                State('variables-table', 'data'),
                State('variables-table', 'columns'))
    def add_row(n_clicks, rows, columns):
        if n_clicks > 0:
            rows.append({c['id']: '' for c in columns})
        return rows

    @app.callback(Output("stored_variables", "data"),
                Input('variables-table', 'data'))
    def store_instruments(input_table):
        if input_table is None:
                raise PreventUpdate
        return pd.DataFrame(input_table).to_json(orient='split')

Many thanks.

Is there any alternative way of achieving this in Plotly Dash? @chriddyp @adamschroeder

Hello @ngambaro,

Welcome to the community! Sorry no one answered your post yet.

So, for this, will the table need to receive updates from the server?

If not, then what you can do is setup a callback triggered by the data table’s data to store the data into a dcc.store. Then on return to page you pull from the dcc.store instead of the other data’s info.

Hi @jinnyzor, thank you for taking a look at this! Would this solution be compatible with the user adding and removing rows as well though?

Yes, should be able to do so, as long as the user is interacting with the data of the dash table.

Here is a very basic example:

import dash
from dash import Dash, Output, Input, State, dash_table, dcc, html
import pandas as pd

df = pd.DataFrame({str(i):[1,2,3,4] for i in range(0,10)})

app = Dash()

app.layout = html.Div([
    dcc.Store(id='tableInfo', storage_type='session'),
    dash_table.DataTable(
        data=df.to_dict('records'),
        columns=[{'name':i, 'id':i} for i in df.columns],
        id='dataTable',
        editable=True
    )]
)

@app.callback(
    Output('dataTable','data'),
    Input('dataTable','id'),
    State('tableInfo', 'data')
)
def updateUserChanges(i, d):
    if d:
        return d
    return dash.no_update

@app.callback(
    Output('tableInfo','data'),
    Input('dataTable','data')
)
def mimicChanges(d):
    return d

app.run(debug=True, port=12345)

Thank you @jinnyzor . What is this Input for?

Its to trigger the load of the dataset upon page load, since layouts cant directly use things like dcc.Stores.

1 Like

I see, thanks very much. Your solution works if you combine updateUserChanges with the addrow callback. Thanks for your help @jinnyzor

1 Like

Delete row is much nicer than add row, haha. :slight_smile:

Hello @ngambaro, could you share how you combined updateUserChanges with the addrow callback?
I have the same problem and couldn’t solve it.
Thanks

Hello @gustavomoers,

Welcome back!

I’m assuming you mean something along the lines of this:

import dash
from dash import Dash, Output, Input, State, dash_table, dcc, html
import pandas as pd

df = pd.DataFrame({str(i):[1,2,3,4] for i in range(0,10)})

app = Dash()

app.layout = html.Div([
    dcc.Store(id='tableInfo', storage_type='session'),
    dash_table.DataTable(
        data=df.to_dict('records'),
        columns=[{'name':i, 'id':i} for i in df.columns],
        id='dataTable',
        editable=True
    ),
    html.Button(id='addRow',children='add row')
]
)

@app.callback(
    Output('dataTable','data'),
    Input('dataTable','id'),
    Input('addRow','n_clicks'),
    State('tableInfo', 'data'),
)
def updateUserChanges(i, n1, d):
    if ctx.triggered_id == 'addRow':
        d.append({i: ''} for i in df.columns)
        return d
    if d:
        return d
    return dash.no_update

@app.callback(
    Output('tableInfo','data'),
    Input('dataTable','data')
)
def mimicChanges(d):
    return d

app.run(debug=True, port=12345)

I havent tested this yet, but I think it should work. :slight_smile:

Hey @jinnyzor , thank you for your fast answer, I’ve tried to apply this on my code but I get an error, I tested your code as well and I keep getting the same error whenever I try to add a row after a previous modification:

"dash.exceptions.InvalidCallbackReturnValue: The callback for <Output dataTable.data>
returned a value having type list
which is not JSON serializable.

The value in question is either the only value returned,
or is in the top level of the returned list,

and has string representation
[{'0': 1, '1': 1, '2': 1, '3': 1, '4': 1, '5': 1, '6': 1, '7': 1, '8': 1, '9': 1}, {'0': 2, '1': 2, '2': 2, '3': 2, '4': 2, '5': 2, '6': 2, '7': 2, '8': 2, '9': 2}, {'0': 3, '1': 3, '2': 3, '3': 3, '4': '5', '5': 3, '6': 3, '7': 3, '8': 3, '9': 3}, {'0': 4, '1': 4, '2': 4, '3': 4, '4': 4, '5': 4, '6': 4, '7': 4, '8': 4, '9': 4}, <generator object updateUserChanges.<locals>.<genexpr> at 0x0000027A61F5E190>]

In general, Dash properties can only be
dash components, strings, dictionaries, numbers, None,
or lists of those."

I tried to fix it, tried json.dumps, etc, but it looks like nothing works with a generator object. Any ideia how to fix this? Thank you in advance!!

Hello @gustavomoers,

There were a few things that I needed to alter, try this:

import dash
from dash import Dash, Output, Input, State, dash_table, dcc, html, ctx
import pandas as pd

df = pd.DataFrame({str(i):[1,2,3,4] for i in range(0,10)})

app = Dash()

app.layout = html.Div([
    dcc.Store(id='tableInfo', storage_type='session'),
    dash_table.DataTable(
        data=df.to_dict('records'),
        columns=[{'name':i, 'id':i} for i in df.columns],
        id='dataTable',
        editable=True
    ),
    html.Button(id='addRow',children='add row')
]
)

@app.callback(
    Output('dataTable','data'),
    Input('dataTable','id'),
    Input('addRow', 'n_clicks'),
    State('tableInfo', 'data'),
    State('dataTable','data'),
)
def updateUserChanges(i, n1, d, d2):
    if ctx.triggered_id == 'addRow':
        d2.append({str(i): '' for i in df.columns})
        return d2
    if d:
        return d
    return dash.no_update

@app.callback(
    Output('tableInfo','data'),
    Input('dataTable','data'),
)
def mimicChanges(d):

    return d

app.run(debug=True, port=12345)
2 Likes