DataTable Interactivity

DataTable includes several features for modifying and transforming
the view of the data. These include:

  • Sorting by column (sort_action='native')
  • Filtering by column (filter_action='native')
  • Editing the cells (editable=True)
  • Deleting rows (row_deletable=True)
  • Deleting columns (columns[i].deletable=True)
  • Selecting rows (row_selectable='single' | 'multi')
  • Selecting columns (column_selectable='single' | 'multi' and columns[i].selectable=True)
  • Paging front-end (page_action='native')
  • Hiding columns (hidden_columns=[])

A quick note on filtering. We have defined our own
syntax for performing filtering operations. Here are some
examples for this particular dataset:

  • Enter Asia in the “continent” column
  • Enter > 5000 in the “gdpPercap” column
  • Enter < 80 in the lifeExp column

Note: simple strings can be entered plain, but if you have
spaces or special characters (including -, particularly in dates)
you need to wrap them in quotes.
Single quotes ', double quotes ", or backticks ``` all work.
Full filter syntax reference

By default, these transformations are done clientside.
Your Dash callbacks can respond to these modifications
by listening to the data property as an Input.

Note that if data is an Input then the entire
data will be passed over the network: if your dataframe is large,
then this will become slow. For large dataframes, you can perform the
sorting or filtering in Python instead.

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

df = pd.read_csv('https://raw.githubusercontent.com/plotly/datasets/master/gapminder2007.csv')

app = dash.Dash(__name__)

app.layout = html.Div([
    dash_table.DataTable(
        id='datatable-interactivity',
        columns=[
            {"name": i, "id": i, "deletable": True, "selectable": True} for i in df.columns
        ],
        data=df.to_dict('records'),
        editable=True,
        filter_action="native",
        sort_action="native",
        sort_mode="multi",
        column_selectable="single",
        row_selectable="multi",
        row_deletable=True,
        selected_columns=[],
        selected_rows=[],
        page_action="native",
        page_current= 0,
        page_size= 10,
    ),
    html.Div(id='datatable-interactivity-container')
])

@app.callback(
    Output('datatable-interactivity', 'style_data_conditional'),
    [Input('datatable-interactivity', 'selected_columns')]
)
def update_styles(selected_columns):
    return [{
        'if': { 'column_id': i },
        'background_color': '#D2F3FF'
    } for i in selected_columns]

@app.callback(
    Output('datatable-interactivity-container', "children"),
    [Input('datatable-interactivity', "derived_virtual_data"),
     Input('datatable-interactivity', "derived_virtual_selected_rows")])
def update_graphs(rows, derived_virtual_selected_rows):
    # When the table is first rendered, `derived_virtual_data` and
    # `derived_virtual_selected_rows` will be `None`. This is due to an
    # idiosyncracy in Dash (unsupplied properties are always None and Dash
    # calls the dependent callbacks when the component is first rendered).
    # So, if `rows` is `None`, then the component was just rendered
    # and its value will be the same as the component's dataframe.
    # Instead of setting `None` in here, you could also set
    # `derived_virtual_data=df.to_rows('dict')` when you initialize
    # the component.
    if derived_virtual_selected_rows is None:
        derived_virtual_selected_rows = []

    dff = df if rows is None else pd.DataFrame(rows)

    colors = ['#7FDBFF' if i in derived_virtual_selected_rows else '#0074D9'
              for i in range(len(dff))]

    return [
        dcc.Graph(
            id=column,
            figure={
                "data": [
                    {
                        "x": dff["country"],
                        "y": dff[column],
                        "type": "bar",
                        "marker": {"color": colors},
                    }
                ],
                "layout": {
                    "xaxis": {"automargin": True},
                    "yaxis": {
                        "automargin": True,
                        "title": {"text": column}
                    },
                    "height": 250,
                    "margin": {"t": 10, "l": 10, "r": 10},
                },
            },
        )
        # check if column exists - user may have deleted it
        # If `column.deletable=False`, then you don't
        # need to do this check.
        for column in ["pop", "lifeExp", "gdpPercap"] if column in dff
    ]


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

Row IDs

When using transformations - sorting, filtering, pagination - it can be
difficult to match up rows - visible rows, selected rows, active rows -
to the original data, because row indices may not have their original
meaning. To simplify this logic we’ve added support for Row IDs.
Each row of data can have an 'id' key, which should contain a string
or a number.
If you want to display these values you can include a column with
id='id', but normally they stay hidden.
All properties that list certain rows by index also have variants
listing row IDs:
- derived_virtual_indices / derived_virtual_row_ids: the order of
rows across all pages (for front-end paging) after filtering and
sorting.
- derived_viewport_indices / derived_viewport_row_ids: the order of
rows on the currently visible page.
- selected_rows / selected_row_ids: when row_selectable is
enabled and there is a checkbox next to each row, these are the
selected rows. Note that even filtered-out or paged-out rows can remain
selected.
- derived_virtual_selected_rows / derived_virtual_selected_row_ids:
the set of selected rows after filtering and sorting, across all pages
- derived_viewport_selected_rows /
derived_viewport_selected_row_ids: the set of selected rows on the
currently visible page.

Often several of these properties contain the same data, but in other
cases it’s important to choose the right one for the specific user
interaction you have in mind. Do you want to respond to selected rows
even when they’re not on the current page? Even when they’re filtered
out?

There are also properties that reference specific cells in the table.
Along with the row and column indices, these include the row and
column IDs of the cell:
- active_cell: this is the data cell the user has put the cursor on,
by clicking and/or arrow keys. It’s a dictionary with keys:
- row: the row index (integer) - may be affected by sorting,
filtering, or paging transformations.
- column: the column index (integer)
- row_id: the id field of the row, which always stays with it
during transformations.
- column_id: the id field of the column.
- start_cell: if the user selects multiple cells, by shift-click or
shift-arrow-keys, this is where the selection was initiated. Has the
same form as active_cell, and usually the same value although after
selecting a region the user can change active_cell by pressing
&lt;tab&gt; or &lt;enter&gt; to cycle through the selected cells.
- end_cell: the corner of the selected region opposite start_cell.
Also has the same form as active_cell.
- selected_cells: an array of dicts, each one with the form of
active_cell, listing each selected cell.

Here’s the same example, plus active cell highlighting, implemented
using row IDs.
One advantage here is that we don’t need to pass the entire data set
back, we can just pass the IDs.
Even the full set of IDs is only necessary in order to sync with
sorting and filtering.

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

df = pd.read_csv('https://raw.githubusercontent.com/plotly/datasets/master/gapminder2007.csv')
# add an id column and set it as the index
# in this case the unique ID is just the country name, so we could have just
# renamed 'country' to 'id' (but given it the display name 'country'), but
# here it's duplicated just to show the more general pattern.
df['id'] = df['country']
df.set_index('id', inplace=True, drop=False)

app = dash.Dash(__name__)

app.layout = html.Div([
    dash_table.DataTable(
        id='datatable-row-ids',
        columns=[
            {'name': i, 'id': i, 'deletable': True} for i in df.columns
            # omit the id column
            if i != 'id'
        ],
        data=df.to_dict('records'),
        editable=True,
        filter_action="native",
        sort_action="native",
        sort_mode='multi',
        row_selectable='multi',
        row_deletable=True,
        selected_rows=[],
        page_action='native',
        page_current= 0,
        page_size= 10,
    ),
    html.Div(id='datatable-row-ids-container')
])


@app.callback(
    Output('datatable-row-ids-container', 'children'),
    [Input('datatable-row-ids', 'derived_virtual_row_ids'),
     Input('datatable-row-ids', 'selected_row_ids'),
     Input('datatable-row-ids', 'active_cell')])
def update_graphs(row_ids, selected_row_ids, active_cell):
    # When the table is first rendered, `derived_virtual_data` and
    # `derived_virtual_selected_rows` will be `None`. This is due to an
    # idiosyncracy in Dash (unsupplied properties are always None and Dash
    # calls the dependent callbacks when the component is first rendered).
    # So, if `rows` is `None`, then the component was just rendered
    # and its value will be the same as the component's dataframe.
    # Instead of setting `None` in here, you could also set
    # `derived_virtual_data=df.to_rows('dict')` when you initialize
    # the component.
    selected_id_set = set(selected_row_ids or [])

    if row_ids is None:
        dff = df
        # pandas Series works enough like a list for this to be OK
        row_ids = df['id']
    else:
        dff = df.loc[row_ids]

    active_row_id = active_cell['row_id'] if active_cell else None

    colors = ['#FF69B4' if id == active_row_id
              else '#7FDBFF' if id in selected_id_set
              else '#0074D9'
              for id in row_ids]

    return [
        dcc.Graph(
            id=column + '--row-ids',
            figure={
                'data': [
                    {
                        'x': dff['country'],
                        'y': dff[column],
                        'type': 'bar',
                        'marker': {'color': colors},
                    }
                ],
                'layout': {
                    'xaxis': {'automargin': True},
                    'yaxis': {
                        'automargin': True,
                        'title': {'text': column}
                    },
                    'height': 250,
                    'margin': {'t': 10, 'l': 10, 'r': 10},
                },
            },
        )
        # check if column exists - user may have deleted it
        # If `column.deletable=False`, then you don't
        # need to do this check.
        for column in ['pop', 'lifeExp', 'gdpPercap'] if column in dff
    ]


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