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!
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.
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).
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!
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.
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).