Black Lives Matter. Please consider donating to Black Girls Code today.
https://www.blackgirlscode.com

Datatable 'Add Row' button only ever appends one row to datatable

Hi everyone,
I’ve a datatable that implements the ‘add row’ button using the code below:

if n_clicks_1 > 0:
    data.append({c['id']: 0 for c in columns})

However, if i press the button again it doesn’t append another row to the table. In fact, if i had entered some values into the row i first added, click the ‘add row’ button erases evrything.

Where am i getting this wrong?

I just did an experiment to see why my code doesn’t seem to work the same way as the one in the example presented here: https://dash.plotly.com/datatable/editable

Here’s my code:

import dash

import dash_table

import dash_bootstrap_components as dbc

import dash_core_components as dcc

import dash_html_components as html

from dash.dependencies import Input, Output, State

from dash.exceptions import PreventUpdate

import pandas as pd

app = dash.Dash(external_stylesheets=[dbc.themes.BOOTSTRAP])

forecasts = ["Demand Plan", "Stat Forecast", "Year -1", "% growth (-1)", "Year -2", "% growth (-2)"]

app.layout = html.Div([

    dash_table.DataTable(

        id = 'table',

        columns = [{'name': 'Forecasts', 'id': 'Forecasts'}] + [{

                    'name': '{}'.format(j),

                    'id': 'column-{}'.format(i),

                    'deletable': True,

                    'renamable': True

                } for i,j in zip(range(5), [1,2,3,4,5])],

        data=[

                dict(Forecasts=i, **{'column-{}'.format(k): 0*j for k in range(5)})

                for i, j in zip(forecasts, range(len(forecasts)))

            ],

        editable = True,

        row_selectable='multi',

        #row_deletable=True,

        selected_rows=[1],

        selected_row_ids=['Stat Forecast'],

        fixed_columns={ 'headers': True, 'data': 1 },

        style_cell={'textAlign': 'center'},

        style_table={'overflowX': 'scroll',

                    'minWidth': '100%',

                    'padding':'10px'},

        css=[{"selector": ".show-hide", "rule": "display: none"}]

    ),

    html.Button('Add Row', id='add-row-button', n_clicks=0),

])

@app.callback(

    Output('table', 'data'),

    [Input('add-row-button', 'n_clicks')],

    [State('table', 'data'),

     State('table', 'columns')])

def add_row(n_clicks, rows, columns):

    rows=[

            dict(id=i, **{'column-{}'.format(k): j+1 for k in range(len(columns))})

            for i, j in zip(forecasts, range(len(forecasts)))

        ]

        

    if n_clicks > 0:

        rows.append({c['id']: '' for c in columns})

    return rows

if __name__ == '__main__':

    app.run_server(debug = True)

If the callback you’re using to append rows to the table also involves modifying the rows somehow, the “add row” button only ever appends one row to the table. If you press it again, the last appended row is simply overwritten. Still not sure why this is so and how to get around it, but progress. @chriddyp, can you please help?

Hello,
check your callback function “add_row”, you are getting the rows value and changing it every time the function is called, that’s why just one row is added since you are updating all the rows instead of this you can just append the new row and it should be enough

def add_row(n_clicks, rows, columns):
    if n_clicks > 0:
        rows.append({c['id']: '' for c in columns})
    return rows

And that’s exactly the way it is used to add a row in the example you mentioned.

Hi Timo,

Thanks for the explanation. If I’m understanding what you’re saying, everytime n_clicks changes, it also creates a new set of rows then appends to that set of rows, which is why only one row is ever added?

The thing is, the callback above is only a representation of a much larger callback that i’ve in my real app. This callback modifies the rows in different ways for different purposes, so “rows” changes very regularly.

However, I would still like the functionality of adding rows to my table. I can’t have something like:

@app.callback
(Output('table', 'data'),
[Input('some-id', 'some-value')])
def modify_rows(some_value):
    columns = [{'name': 'Forecasts', 'id': 'Forecasts'}] + [{
                    'name': '{}'.format(j),
                    'id': 'column-{}'.format(i),
                    'deletable': True,
                    'renamable': True
                } for i,j in zip(range(5), [1,2,3,4,5])],

rows=[
            dict(id=i, **{'column-{}'.format(k): j+1 for k in range(len(columns))})
            for i, j in zip(forecasts, range(len(forecasts)))
        ]
    return rows

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

…because dash won’t allow me to have 2 callbacks that have the same output.

I did answer your question about why you are having just one row added when you click on the button since you override the rows content every time, now for your app (since you didn’t post the actual code) you should be able to build on that simple callback function to add other functionalities.

I don’t quite understand that last part. The structure of my actual app and what I posted earlier aren’t that different.

Now that you’ve revealed to me what’s going on with my callback, let me ask my question another way:

Is there a way I can have a callback that updates the rows in my datatable, while also allowing me to have a button that appends rows to the updated table?

In principle, you could just create one callback that delegates the different control flow. However, since i encountered this issue too, i have written i component that handles this kind of issue. In essence, you just register your callback to a proxy component, DashCallbackBlueprint, when then takes care of the control flow delegation. The syntax would be something like,

from dash_extensions.callback import DashCallbackBlueprint

dcb = DashCallbackBlueprint()

@dcb.callback(...)
def modify_rows(some_value):
...

@dcb.callback(...)
def add_row(n_clicks, rows, columns):
...

dcb.register(app)

I am sorry i think i didn’t really understood your problem, for your issue i took a look another time on your code and i think you just needed to add also a value to forecasts since it is the problem why you are not getting more than one entry in your table since it’s fixed to 6 elements so each time you modify your rows it adds just 6 rows + the one you add using the append later, so the solution is to append also a value to forecasts in order to be able to add also the new rows in the next callback + the new one, like this

def add_row(n_clicks, rows, columns):

    rows=[
            dict(Forecasts=i, **{'column-{}'.format(k): j+1 for k in range(len(columns))})
            for i, j in zip(forecasts, range(len(forecasts)))]

    if n_clicks > 0:
       
        rows.append({c['id']: '' for c in columns})
        forecasts.append("")

    return rows

Thank you so much, Timo. This solves my issue precisely.