Dash data table add a column on user input with predefined values

Question:

I have a simple dash app containing a data table.Two user inputs make it possible to add a row or a column. Juste like when I add a row I get default values (here 0 hours) for every column, I would also like to have default values for all rows when adding a new column. Here is the code:

import pathlib as pl
import dash
from dash import dash_table
from dash.dash_table.Format import Format, Scheme, Sign, Symbol
from dash import dcc
from dash import html
import plotly.graph_objs as go
import pandas as pd
from dash.dependencies import Input, Output, State

table_header_style = {
    "backgroundColor": "rgb(2,21,70)",
    "color": "white",
    "textAlign": "center",
}


app = dash.Dash(__name__)
app.title = "Trial"
server = app.server

APP_PATH = str(pl.Path(__file__).parent.resolve())

list_rows = ['a', 'b', 'c', 'd']
tasks = ['task' + str(i) for i in range(5)]
data = {task:[0 for i in range(len(list_rows))] for task in tasks}

app.layout = html.Div(
    className="",
    children=[
        html.Div(
            # className="container",
            children=[
                html.Div(
                    # className="row",
                    style={},
                    children=[
                        html.Div(
                            # className="four columns pkcalc-settings",
                            children=[
                                html.P(["Study Design"]),
                                html.Div(
                                    [
                                        html.Label(
                                            [
                                                dcc.Input(
                                                    id="new-row",
                                                    placeholder="Row to be added...",
                                                    type="text",
                                                    debounce=True,
                                                    maxLength=20,
                                                    style={
                                                        'width':'66%',
                                                        'margin-left': '5px'
                                                    }
                                                ),
                                                html.Button(
                                                    'Add', 
                                                    id='add-row-button', 
                                                    n_clicks=0,
                                                    style={
                                                        'font-size': '10px', 
                                                        'width': '140px', 
                                                        'display': 'inline-block', 
                                                        'margin-bottom': '5px', 
                                                        'margin-right': '5px',
                                                        'margin-left': '5px', 
                                                        'height':'38px', 
                                                        'verticalAlign': 'top'
                                                        }
                                                    ),
                                            ]
                                        ),
                                        html.Label(
                                            [
                                                dcc.Input(
                                                    id="new-task",
                                                    placeholder="Task to be added...",
                                                    type="text",
                                                    debounce=True,
                                                    maxLength=50,
                                                    style={'width':'66%'}
                                                ),
                                                html.Button(
                                                    'Add', 
                                                    id='add-task-button', 
                                                    n_clicks=0,
                                                    style={
                                                        'font-size': '10px', 
                                                        'width': '140px', 
                                                        'display': 'inline-block', 
                                                        'margin-bottom': '5px', 
                                                        'margin-right': '5px',
                                                        'margin-left': '5px', 
                                                        'height':'38px', 
                                                        'verticalAlign': 'top'
                                                    }
                                                ),
                                            ]
                                        ),
                                    ]
                                ),
                            ],
                        ),
                        html.Div(
                            # className="eight columns pkcalc-data-table",
                            children=[
                                dash_table.DataTable(
                                    id='table',
                                    columns=(
                                        [{
                                            'id': 'name', 
                                            'name': 'Name',
                                            'type': 'text',
                                            'deletable': True,
                                            'renamable': True,
                                        }] +
                                        [{
                                            'id': task, 
                                            'name': task,
                                            'type': 'numeric',
                                            'deletable': True,
                                            'renamable': True,
                                            'format': Format(
                                                precision=0,
                                                scheme=Scheme.fixed,
                                                symbol=Symbol.yes,
                                                symbol_suffix='h'
                                            ),
                                        } for task in tasks]
                                    ),
                                    data=[dict(name=i, **{task: 0 for task in tasks}) for i in list_rows],
                                    editable=True,
                                    style_header=table_header_style,
                                    active_cell={"row": 0, "column": 0},
                                    selected_cells=[{"row": 0, "column": 0}],
                                ),
                            ],
                        ),
                    ],
                ),
            ],
        ),
    ],
)



# Callback to add column
@app.callback(
    Output(component_id='table', component_property='columns'),
    Input(component_id='add-task-button', component_property='n_clicks'),
    State(component_id='new-task', component_property='value'),
    State(component_id='table', component_property='columns'),)
def update_columns(n_clicks, new_task, existing_tasks):
    if n_clicks > 0:
        existing_tasks.append({
            'id': new_task, 'name': new_task,
            'renamable': True, 'deletable': True
        })
    return existing_tasks

# Callback to add row
@app.callback(
    Output(component_id='table', component_property='data'),
    Input(component_id='add-row-button', component_property='n_clicks'),
    State(component_id='new-row', component_property='value'),
    State(component_id='table', component_property='columns'),
    State(component_id='table', component_property='data'))
def update_rows(n_clicks, new_row, columns, rows):
    if n_clicks > 0:
        rows.append(
            {
                'name': new_row,
                **{column['id']: 0 for column in columns[1:]}
            }
        )
    return rows


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

Any help would be greatly appreciated!

Asked By: The Governor

||

Answers:

I figured out a way using one callback only to add a column or a row. It’s not the prettiest but it works. If anyone has a better way, allowing to keep the two callbacks separated I would appreciate it.

Here is the code:

import pathlib as pl
import dash
from dash import dash_table
from dash.dash_table.Format import Format, Scheme, Sign, Symbol
from dash import dcc
from dash import html
import plotly.graph_objs as go
import pandas as pd
from dash.dependencies import Input, Output, State

table_header_style = {
    "backgroundColor": "rgb(2,21,70)",
    "color": "white",
    "textAlign": "center",
}


app = dash.Dash(__name__)
app.title = "Trial"
server = app.server

APP_PATH = str(pl.Path(__file__).parent.resolve())

list_rows = ['a', 'b', 'c', 'd']
tasks = ['task' + str(i) for i in range(5)]
data = {task:[0 for i in range(len(list_rows))] for task in tasks}

app.layout = html.Div(
    className="",
    children=[
        html.Div(
            # className="container",
            children=[
                dcc.Store(id='n_clicks-column', data=0),
                dcc.Store(id='n_clicks-row', data=0),
                html.Div(
                    # className="row",
                    style={},
                    children=[
                        html.Div(
                            # className="four columns pkcalc-settings",
                            children=[
                                html.P(["Study Design"]),
                                html.Div(
                                    [
                                        html.Label(
                                            [
                                                dcc.Input(
                                                    id="new-row",
                                                    placeholder="Row to be added...",
                                                    type="text",
                                                    debounce=True,
                                                    maxLength=20,
                                                    style={
                                                        'width':'66%',
                                                    }
                                                ),
                                                html.Button(
                                                    'Add', 
                                                    id='add-row-button', 
                                                    n_clicks=0,
                                                    style={
                                                        'font-size': '10px', 
                                                        'width': '140px', 
                                                        'display': 'inline-block', 
                                                        'margin-bottom': '5px', 
                                                        'margin-right': '5px',
                                                        'margin-left': '5px', 
                                                        'height':'38px', 
                                                        'verticalAlign': 'top'
                                                        }
                                                    ),
                                            ]
                                        ),
                                        html.Label(
                                            [
                                                dcc.Input(
                                                    id="new-task",
                                                    placeholder="Task to be added...",
                                                    type="text",
                                                    debounce=True,
                                                    maxLength=50,
                                                    style={'width':'66%'}
                                                ),
                                                html.Button(
                                                    'Add', 
                                                    id='add-task-button', 
                                                    n_clicks=0,
                                                    style={
                                                        'font-size': '10px', 
                                                        'width': '140px', 
                                                        'display': 'inline-block', 
                                                        'margin-bottom': '5px', 
                                                        'margin-right': '5px',
                                                        'margin-left': '5px', 
                                                        'height':'38px', 
                                                        'verticalAlign': 'top'
                                                    }
                                                ),
                                            ]
                                        ),
                                    ]
                                ),
                            ],
                        ),
                        html.Div(
                            # className="eight columns pkcalc-data-table",
                            children=[
                                dash_table.DataTable(
                                    id='table',
                                    columns=(
                                        [{
                                            'id': 'name', 
                                            'name': 'Name',
                                            'type': 'text',
                                            'deletable': True,
                                            'renamable': True,
                                        }] +
                                        [{
                                            'id': task, 
                                            'name': task,
                                            'type': 'numeric',
                                            'deletable': True,
                                            'renamable': True,
                                            'format': Format(
                                                precision=0,
                                                scheme=Scheme.fixed,
                                                symbol=Symbol.yes,
                                                symbol_suffix='h'
                                            ),
                                        } for task in tasks]
                                    ),
                                    data=[dict(name=i, **{task: 0 for task in tasks}) for i in list_rows],
                                    editable=True,
                                    style_header=table_header_style,
                                    active_cell={"row": 0, "column": 0},
                                    selected_cells=[{"row": 0, "column": 0}],
                                ),
                            ],
                        ),
                    ],
                ),
            ],
        ),
    ],
)


@app.callback(
    Output(component_id='table', component_property='columns'),
    Output(component_id='table', component_property='data'),
    Output(component_id='n_clicks-column', component_property='data'),
    Output(component_id='n_clicks-row', component_property='data'),
    Input(component_id='add-task-button', component_property='n_clicks'),
    Input(component_id='add-row-button', component_property='n_clicks'),
    State(component_id='new-task', component_property='value'),
    State(component_id='new-row', component_property='value'),
    State(component_id='table', component_property='columns'),
    State(component_id='table', component_property='data'),
    State(component_id='n_clicks-column', component_property='data'),
    State(component_id='n_clicks-row', component_property='data')
    )
def update_table(n_clicks_column, n_clicks_row, new_task, new_row, columns, table_data, stored_n_clicks_column, stored_n_clicks_row):
    
    if n_clicks_column > stored_n_clicks_column:
        columns.append({
            'id': new_task, 
            'name': new_task,
            'type': 'numeric',
            'renamable': True, 
            'deletable': True,
            'format': Format(
                precision=0,
                scheme=Scheme.fixed,
                symbol=Symbol.yes,
                symbol_suffix='h'
            ),
        })

        for row_dict in table_data:
            row_dict[new_task] = 0

        stored_n_clicks_column += 1

    if n_clicks_row > stored_n_clicks_row:
        table_data.append(
            {
                'name': new_row,
                **{column['id']: 0 for column in columns[1:]}
            }
        )

        stored_n_clicks_row += 1
    

    return columns, table_data, stored_n_clicks_column, stored_n_clicks_row

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

I had to add 2 dcc.Store to keep in memory the number of time each button has been clicked. Then I had to gather the 2 callbacks into one in order to modify the 'data' and 'columns' properties of the data table at once.

I tried keeping the two callbacks separated but in order to add default values to the new column, the only way I found was to modify both the 'data' and 'columns' properties in the add column callback. And dash doesn’t allow to have 2 callbacks modifying the same element, here the 'data' property that was also being updated in the callback to add a row.

Answered By: The Governor

In your situation, the duplicate callback outputs is an issue you cannot work around since both callbacks need to update table.data, so combining these two into one callback function is the right solution.

However, you could refactor a bit to use specific functions for each action in order to reduce the size of the callback :

def add_column(new_task, columns, data):
    columns.append({
        'id': new_task,
        'name': new_task,
        'type': 'numeric',
        'deletable': True,
        'renamable': True,
        'format': Format(
            precision=0,
            scheme=Scheme.fixed,
            symbol=Symbol.yes,
            symbol_suffix='h'
        )
    })

    for row in data:
        row[new_task] = 0

    return columns, data


def add_row(new_row, columns, data):
    data.append(
        {
            'name': new_row,
            **{column['id']: 0 for column in columns[1:]}
        }
    )
    return data

Also note that you can distinguish which component/property pair has triggered the callback by using dash.callback_context, which simplify things :

@app.callback(
    Output(component_id='table', component_property='columns'),
    Output(component_id='table', component_property='data'),
    Input(component_id='add-row-button', component_property='n_clicks'),
    Input(component_id='add-task-button', component_property='n_clicks'),
    State(component_id='new-row', component_property='value'),
    State(component_id='new-task', component_property='value'),
    State(component_id='table', component_property='columns'),
    State(component_id='table', component_property='data'),
    prevent_initial_call=True)
def update_table(nc_row, nc_task, new_row, new_task, columns, data):
    ctx = dash.callback_context
    id, prop = ctx.triggered[0]['prop_id'].split('.')

    if id == 'add-row-button':
        columns, data = add_row(new_row, columns, data)
    elif id == 'add-task-button':
        data = add_column(new_task, columns, data)

    return columns, data

[EDIT] : Actually there exists a workaround, but it requires to install dash-extensions :

The package provides a proxy component and something called a MultiplexerTransform, which can be used to make possible for multiple callbacks to target the same output (note that it will chain/merge the callbacks under the hood).

Answered By: EricLavault
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.