Editable DataTable

The DataTable is editable. Like a spreadsheet, it can be used
as an input for controlling models with a variable number
of inputs.

This chapter includes recipes for:

  • Reading the contents of the DataTable
  • Filtering out null values
  • Uploading data
  • Determining which cell has changed
  • Adding or removing columns
  • Adding or removing rows
  • Clearable, deletable, renamable, and hideable columns
  • Export DataTable

Predefined Columns

In this example, we initialize a table with 10 blank rows and
a few predefined columns. To retrieve the data, just listen to the
data property.

A few notes:
- If you copy and paste data that is larger than the rows, then the
table will expand to contain the contents.
Try it out by copying and pasting this dataset.
- Unlike other spreadsheet programs, the DataTable has a fixed number of
rows. So, if your model has an arbitrary number of parameters
(rows or columns), we recommend initializing your table with a
large number of empty rows and columns.
- When copying data from the table to an external spreadsheet or
between two tables in different tabs, you can choose to include column headers
by adding include_headers_on_copy_paste=True. However, headers are ignored
when copying between two tables in the same tab.

Editable DataTable

The DataTable is editable. Like a spreadsheet, it can be used
as an input for controlling models with a variable number
of inputs.

This chapter includes recipes for:

  • Reading the contents of the DataTable
  • Filtering out null values
  • Uploading data
  • Determining which cell has changed
  • Adding or removing columns
  • Adding or removing rows
  • Clearable, deletable, renamable, and hideable columns
  • Export DataTable

Predefined Columns

In this example, we initialize a table with 10 blank rows and
a few predefined columns. To retrieve the data, just listen to the
data property.

A few notes:
- If you copy and paste data that is larger than the rows, then the
table will expand to contain the contents.
Try it out by copying and pasting this dataset.
- Unlike other spreadsheet programs, the DataTable has a fixed number of
rows. So, if your model has an arbitrary number of parameters
(rows or columns), we recommend initializing your table with a
large number of empty rows and columns.
- When copying data from the table to an external spreadsheet or
between two tables in different tabs, you can choose to include column headers
by adding include_headers_on_copy_paste=True. However, headers are ignored
when copying between two tables in the same tab.

import dash
from dash.dependencies import Input, Output
import dash_table
import dash_core_components as dcc
import dash_html_components as html
import pandas as pd

app = dash.Dash(__name__)

params = [
    'Weight', 'Torque', 'Width', 'Height',
    'Efficiency', 'Power', 'Displacement'
]

app.layout = html.Div([
    dash_table.DataTable(
        id='table-editing-simple',
        columns=(
            [{'id': 'Model', 'name': 'Model'}] +
            [{'id': p, 'name': p} for p in params]
        ),
        data=[
            dict(Model=i, **{param: 0 for param in params})
            for i in range(1, 5)
        ],
        editable=True
    ),
    dcc.Graph(id='table-editing-simple-output')
])


@app.callback(
    Output('table-editing-simple-output', 'figure'),
    [Input('table-editing-simple', 'data'),
     Input('table-editing-simple', 'columns')])
def display_output(rows, columns):
    df = pd.DataFrame(rows, columns=[c['name'] for c in columns])
    return {
        'data': [{
            'type': 'parcoords',
            'dimensions': [{
                'label': col['name'],
                'values': df[col['id']]
            } for col in columns]
        }]
    }


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

Integration with Dash loading states

As of table version 4.3.0, Dash loading states also have some
control over whether the table is editable. If the data property
is loading (e.g., while retrieving data from a server), you will
be unable to edit the cells and the dropdowns. This avoids cases
in which the edited value (input from the user) conflicts with the
value that is returned by the server.

In the example below, you can use the dropdown to choose to load
either the style_cell property or the data property. When you
select the property, there will be a simulated delay (to mimic a
delay you might get when communicating with a server). Although
this isn’t the case in the example, you can also use the
.dash-loading CSS selector to apply specific styling while the
table is waiting.

If you select style_cell, you’ll be able to edit the cell as the
value is loading; if you select data, you won’t. Try it out
yourself!

from time import sleep

from random import randint, seed

import dash
from dash.exceptions import PreventUpdate
from dash.dependencies import Input, Output
import dash_table
import dash_core_components as dcc
import dash_html_components as html

# For the documentation to always render the same values
seed(0)

app = dash.Dash(__name__)

app.layout = html.Div([

    'Choose property to load: ',
    dcc.Dropdown(
        id='loading-states-table-prop',
        options=[
            {'label': prop, 'value': prop}
            for prop in ['style_cell', 'data']
        ]
    ),

    html.Br(),

    dash_table.DataTable(
        id='loading-states-table',
        columns=[{
            'name': 'Column {}'.format(i),
            'id': 'column-{}'.format(i),
            'deletable': True,
            'renamable': True
        } for i in range(1, 5)],
        data=[
            {'column-{}'.format(i):
             (randint(0, 100)) for i in range(1, 5)}
            for j in range(5)
        ],
        editable=True
    )
])


@app.callback(
    Output('loading-states-table', 'style_cell'),
    [Input('loading-states-table-prop', 'value')]
)
def loading_style_cell(value):
    if value == 'style_cell':
        sleep(5)
        return {'color': 'rgb({}, {}, {})'.format(
            randint(0, 255),
            randint(0, 255),
            randint(0, 255)
        )}
    raise PreventUpdate


@app.callback(
    Output('loading-states-table', 'data'),
    [Input('loading-states-table-prop', 'value')]
)
def loading_data(value):
    if value == 'data':
        sleep(5)
        return [
            {'column-{}'.format(i):
             (randint(0, 100)) for i in range(5)}
            for j in range(5)
        ]
    raise PreventUpdate


if __name__ == '__main__':
    app.run_server(debug=True)
Choose property to load:

Filtering out Empty Cells

The DataTable will always return all of the cells in the table, even
if the cells haven’t been filled out. So, you’ll likely want to filter
out the empty values.

When you clear a cell, the DataTable will set its contents to ''
(emtpy string). So, for consistency, we recommend initializing your
empty data with ''.

Heads up! In the future, when we introduce proper data types,
we may initialize empty data as something other than ''. For example,
if the column is numerical, we’ll want to avoid having any '' in the
data and we may initialize emty data to None instead.

In this example, we prune away any rows that have empty cells in them.
This is just one way to prune data; you may want to clean your data
differently in your application.

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

import pprint

app = dash.Dash(__name__)

app.layout = html.Div([
    dash_table.DataTable(
        id='editing-prune-data',
        columns=[{
            'name': 'Column {}'.format(i),
            'id': 'column-{}'.format(i)
        } for i in range(1, 5)],
        data=[
            {'column-{}'.format(i): (j + (i-1)*5) for i in range(1, 5)}
            for j in range(5)
        ],
        editable=True
    ),
    html.Div(id='editing-prune-data-output')
])


@app.callback(Output('editing-prune-data-output', 'children'),
              [Input('editing-prune-data', 'data')])
def display_output(rows):
    pruned_rows = []
    for row in rows:
        # require that all elements in a row are specified
        # the pruning behavior that you need may be different than this
        if all([cell != '' for cell in row.values()]):
            pruned_rows.append(row)

    return html.Div([
        html.Div('Raw Data'),
        html.Pre(pprint.pformat(rows)),
        html.Hr(),
        html.Div('Pruned Data'),
        html.Pre(pprint.pformat(pruned_rows)),
    ])

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

Uploading Data

A nice recipe is to tie the
<code>dcc.Upload<code>
with the Table component. After the user has uploaded the data, they
could edit the contents or rename the rows.

Here’s an example that creates a simple “x-y” plotter: upload a CSV
with two columns of data and we’ll plot it.
Try it out by downloading this file
and then uploading it.

import base64
import io
import dash
from dash.dependencies import Input, Output, State
import dash_core_components as dcc
import dash_html_components as html
import dash_table
import pandas as pd

external_stylesheets = ['https://codepen.io/chriddyp/pen/bWLwgP.css']

app = dash.Dash(__name__, external_stylesheets=external_stylesheets)

app.layout = html.Div([
    dcc.Upload(
        id='datatable-upload',
        children=html.Div([
            'Drag and Drop or ',
            html.A('Select Files')
        ]),
        style={
            'width': '100%', 'height': '60px', 'lineHeight': '60px',
            'borderWidth': '1px', 'borderStyle': 'dashed',
            'borderRadius': '5px', 'textAlign': 'center', 'margin': '10px'
        },
    ),
    dash_table.DataTable(id='datatable-upload-container'),
    dcc.Graph(id='datatable-upload-graph')
])


def parse_contents(contents, filename):
    content_type, content_string = contents.split(',')
    decoded = base64.b64decode(content_string)
    if 'csv' in filename:
        # Assume that the user uploaded a CSV file
        return pd.read_csv(
            io.StringIO(decoded.decode('utf-8')))
    elif 'xls' in filename:
        # Assume that the user uploaded an excel file
        return pd.read_excel(io.BytesIO(decoded))


@app.callback([Output('datatable-upload-container', 'data'),
               Output('datatable-upload-container', 'columns')],
              [Input('datatable-upload', 'contents')],
              [State('datatable-upload', 'filename')])
def update_output(contents, filename):
    if contents is None:
        return [{}], []
    df = parse_contents(contents, filename)
    return df.to_dict('records'), [{"name": i, "id": i} for i in df.columns]


@app.callback(Output('datatable-upload-graph', 'figure'),
              [Input('datatable-upload-container', 'data')])
def display_graph(rows):
    df = pd.DataFrame(rows)

    if (df.empty or len(df.columns) < 1):
        return {
            'data': [{
                'x': [],
                'y': [],
                'type': 'bar'
            }]
        }
    return {
        'data': [{
            'x': df[df.columns[0]],
            'y': df[df.columns[1]],
            'type': 'bar'
        }]
    }


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

Adding or removing columns

In the DataTable, we’ve provided a built-in UI for deleting columns
but not for adding columns. We recommend using an external button to
add columns.

This is a simple example that plots the data in the spreadsheet as a
heatmap. Try adding or removing columns!

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

app = dash.Dash(__name__)

app.layout = html.Div([
    html.Div([
        dcc.Input(
            id='editing-columns-name',
            placeholder='Enter a column name...',
            value='',
            style={'padding': 10}
        ),
        html.Button('Add Column', id='editing-columns-button', n_clicks=0)
    ], style={'height': 50}),

    dash_table.DataTable(
        id='editing-columns',
        columns=[{
            'name': 'Column {}'.format(i),
            'id': 'column-{}'.format(i),
            'deletable': True,
            'renamable': True
        } for i in range(1, 5)],
        data=[
            {'column-{}'.format(i): (j + (i-1)*5) for i in range(1, 5)}
            for j in range(5)
        ],
        editable=True,
    ),

    dcc.Graph(id='editing-columns-graph')
])


@app.callback(
    Output('editing-columns', 'columns'),
    [Input('editing-columns-button', 'n_clicks')],
    [State('editing-columns-name', 'value'),
     State('editing-columns', 'columns')])
def update_columns(n_clicks, value, existing_columns):
    if n_clicks > 0:
        existing_columns.append({
            'id': value, 'name': value,
            'renamable': True, 'deletable': True
        })
    return existing_columns


@app.callback(
    Output('editing-columns-graph', 'figure'),
    [Input('editing-columns', 'data'),
     Input('editing-columns', 'columns')])
def display_output(rows, columns):
    return {
        'data': [{
            'type': 'heatmap',
            'z': [[row.get(c['id'], None) for c in columns] for row in rows],
            'x': [c['name'] for c in columns]
        }]
    }


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

Adding or removing rows

Similarly as columns, the DataTable has a built-in UI for removing rows
but not for adding rows. You can add rows to the table through an
external button.

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

app = dash.Dash(__name__)

app.layout = html.Div([
    html.Div([
        dcc.Input(
            id='adding-rows-name',
            placeholder='Enter a column name...',
            value='',
            style={'padding': 10}
        ),
        html.Button('Add Column', id='adding-rows-button', n_clicks=0)
    ], style={'height': 50}),

    dash_table.DataTable(
        id='adding-rows-table',
        columns=[{
            'name': 'Column {}'.format(i),
            'id': 'column-{}'.format(i),
            'deletable': True,
            'renamable': True
        } for i in range(1, 5)],
        data=[
            {'column-{}'.format(i): (j + (i-1)*5) for i in range(1, 5)}
            for j in range(5)
        ],
        editable=True,
        row_deletable=True
    ),

    html.Button('Add Row', id='editing-rows-button', n_clicks=0),

    dcc.Graph(id='adding-rows-graph')
])


@app.callback(
    Output('adding-rows-table', 'data'),
    [Input('editing-rows-button', 'n_clicks')],
    [State('adding-rows-table', 'data'),
     State('adding-rows-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('adding-rows-table', 'columns'),
    [Input('adding-rows-button', 'n_clicks')],
    [State('adding-rows-name', 'value'),
     State('adding-rows-table', 'columns')])
def update_columns(n_clicks, value, existing_columns):
    if n_clicks > 0:
        existing_columns.append({
            'id': value, 'name': value,
            'renamable': True, 'deletable': True
        })
    return existing_columns


@app.callback(
    Output('adding-rows-graph', 'figure'),
    [Input('adding-rows-table', 'data'),
     Input('adding-rows-table', 'columns')])
def display_output(rows, columns):
    return {
        'data': [{
            'type': 'heatmap',
            'z': [[row.get(c['id'], None) for c in columns] for row in rows],
            'x': [c['name'] for c in columns]
        }]
    }


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

Updating Columns of the Same Table

One neat application of DataTable is being able to update the table itself
when you edit cells.

One of the limitations in Dash is that a callback’s Output can’t be
the same as the Input (circular dependencies aren’t supported yet).
So, we couldn’t have Output('table', 'data') and
Input('table', 'data') in the same @app.callback.

However, we can work around this by using State('table', 'data')
and triggering the callback with Input('table', 'data_timestamp').

This example mimics a traditional spreadsheet like excel by computing
certain columns based off of other other columns.

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

app = dash.Dash(__name__)

app.layout = html.Div([
    dash_table.DataTable(
        id='computed-table',
        columns=[
            {'name': 'Input Data', 'id': 'input-data'},
            {'name': 'Input Squared', 'id': 'output-data'}
        ],
        data=[{'input-data': i} for i in range(11)],
        editable=True,
    ),
])


@app.callback(
    Output('computed-table', 'data'),
    [Input('computed-table', 'data_timestamp')],
    [State('computed-table', 'data')])
def update_columns(timestamp, rows):
    for row in rows:
        try:
            row['output-data'] = float(row['input-data']) ** 2
        except:
            row['output-data'] = 'NA'
    return rows


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

Modify the data table content

Columns in the table can be hidden, deleted, cleared, and renamed. Each of these actions
are represented by a small icon in the column headers. If there is more than one header row,
you can choose where the icons appear. If you want to override these icons, you can do so
by using table css selectors, as demonstrated by the example.

When the clear or delete action is performed, the associated filters are also cleared.
Hiding or deleting can only be done if there are more than one column left in the
table after the action is performed.

In this example, we have included a variety of column actions. Try:

  • Clear the first column: the content is cleared (or multiple columns when headers are merged)
    without deleting the column itself.
  • Delete the second column: column is deleted from the table and can’t be seen again unless the
    page is refreshed.
  • Rename the third column: the content of selected column headers is edited.
  • Hide the fourth column: the entire column is hidden from view and can be made visible again
    through the toggle columns button.
dash_table.DataTable(
    columns=[
        {"name": ["", "Year"], "id": "year", "clearable": "first" },
        {"name": ["City", "Montreal"], "id": "montreal", "deletable": [False, True]},
        {"name": ["City", "Toronto"], "id": "toronto", "renamable": True },
        {"name": ["City", "Ottawa"], "id": "ottawa", "hideable": "last"},
        {"name": ["City", "Vancouver"], "id": "vancouver", "clearable": True, "renamable": True, "hideable": True, "deletable": True },
        {"name": ["Climate", "Temperature"], "id": "temp"},
        {"name": ["Climate", "Humidity"], "id": "humidity"},
    ],
    data=[
        {
            "year": i,
            "montreal": i * 10,
            "toronto": i * 100,
            "ottawa": i * -1,
            "vancouver": i * -10,
            "temp": i * -100,
            "humidity": i * 5,
        }
        for i in range(10)
    ],
    css=[
        {"selector": ".column-header--delete svg", "rule": 'display: "none"'},
        {"selector": ".column-header--delete::before", "rule": 'content: "X"'}
    ]
)

Export Data Table

The table data can be exported either as csv or xlsx file. You can customize table
headers in the export file. Headers can be column ids, names or as displayed.
The difference between export_headers: name and export_headers: display is that you have
the option to download the table with merged headers if your table headers are merged.
Finally, if an action was performed on columns (cleared, deleted,hidden, sorted, filtered), then
the downloaded table will display the current view.

  • Note that display mode is only supported for export_format: xlsx due to the fact that
    headers in csv files can not be merged.
dash_table.DataTable(
    columns=[
        {"name": ["", "Year"], "id": "year" },
        {"name": ["City", "Montreal"], "id": "montreal", "deletable": [False, True]},
        {"name": ["City", "Toronto"], "id": "toronto", "renamable": True },
        {"name": ["City", "Ottawa"], "id": "ottawa", "hideable": "last"},
        {"name": ["City", "Vancouver"], "id": "vancouver"},
        {"name": ["Climate", "Temperature"], "id": "temp"},
        {"name": ["Climate", "Humidity"], "id": "humidity"},
    ],
    data=[
        {
            "year": i,
            "montreal": i * 10,
            "toronto": i * 100,
            "ottawa": i * -1,
            "vancouver": i * -10,
            "temp": i * -100,
            "humidity": i * 5,
        }
        for i in range(10)
    ],
    export_format='xlsx',
    export_headers='display',
    merge_duplicate_headers=True
)