# 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.

    using Dash
    using CSV, DataFrames

    df = CSV.read(download("https://raw.githubusercontent.com/plotly/datasets/master/gapminder2007.csv"), DataFrame)
    app = dash()

    app.layout = html_div([
     dash_datatable(
          id="datatable-interactivity",
          columns=[
              Dict("name" =>  i, "id" =>  i, "deletable" =>  true, "selectable" =>  true) for i in names(df)
          ],
          data=Dict.(pairs.(eachrow(df))),
          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")
    ])
    callback!(app,
        Output("datatable-interactivity", "style_data_conditional"),
        Input("datatable-interactivity", "selected_columns")
    ) do selected_columns
        return [Dict(
            "if"=> Dict( "column_id"=> i ),
            "background_color"=> "#D2F3FF"
        ) for i in selected_columns]
    end

    callback!(app,
        Output("datatable-interactivity-container", "children"),
        Input("datatable-interactivity", "derived_virtual_data"),
        Input("datatable-interactivity", "derived_virtual_selected_rows")
        ) do 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
        # idiosyncrasy 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 isa Nothing
            derived_virtual_selected_rows = []
        end
        dff = (rows isa Nothing) ? df  :  DataFrame(rows)
        colors = [(i in derived_virtual_selected_rows ? "#7FDBFF" : "#0074D9") for i in 1:nrow(dff)]
        return [
            if column in names(dff)
                dcc_graph(
                    id=column,
                    figure=Dict(
                        "data" =>  [
                            Dict(
                                "x" =>  dff[!,"country"],
                                "y" =>  dff[!,column],
                                "type" =>  "bar",
                                "marker" =>  Dict("color" =>  colors),
                            )
                        ],
                        "layout" =>  Dict(
                            "xaxis" =>  Dict("automargin" =>  true),
                            "yaxis" =>  Dict(
                                "automargin" =>  true,
                                "title" =>  Dict("text" =>  column)
                            ),
                            "height" =>  250,
                            "margin" =>  Dict("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.
            end
            for column in ["pop", "lifeExp", "gdpPercap"]
        ]
    end
    run_server(app, "0.0.0.0", debug=true)

|  | country | pop | continent | lifeExp | gdpPercap
--- | --- | --- | --- | --- | --- | ---
× |  | Afghanistan | 31889923 | Asia | 43.828 | 974.5803384
× |  | Albania | 3600523 | Europe | 76.423 | 5937.029525999999
× |  | Algeria | 33333216 | Africa | 72.301 | 6223.367465
× |  | Angola | 12420476 | Africa | 42.731 | 4797.231267
× |  | Argentina | 40301927 | Americas | 75.32 | 12779.37964
× |  | Australia | 20434176 | Oceania | 81.235 | 34435.367439999995
× |  | Austria | 8199783 | Europe | 79.829 | 36126.4927
× |  | Bahrain | 708573 | Asia | 75.635 | 29796.04834
× |  | Bangladesh | 150448339 | Asia | 64.062 | 1391.253792
× |  | Belgium | 10392226 | Europe | 79.441 | 33692.60508

1
/
15

Sign up for Dash Club → Two free cheat sheets plus updates from Chris Parmer and Adam Schroeder delivered to your inbox every two months. Includes tips and tricks, community apps, and deep dives into the Dash architecture. Join now.

## 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 (Int) - may be affected by sorting, filtering, or paging transformations. - `column`: the column index (Int) - `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 `<tab>` or `<enter>` 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.

    using Dash
    using CSV, DataFrames,  DataFramesMeta

    df = CSV.read(download("https://raw.githubusercontent.com/plotly/datasets/master/gapminder2007.csv"), DataFrame)
    df[!,"id"] = df[!,"country"]
    app = dash()

    app.layout = html_div([
      dash_datatable(
            id="datatable-row-ids",
            columns=[
              Dict("name" =>  i, "id" =>  i, "deletable" =>  true, "selectable" =>  true) for i in names(df)
                # omit the id column
                if i != "id"
            ],
            data=Dict.(pairs.(eachrow(df))),
            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")
    ])

    callback!(app,
    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")
        ) do 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
        # idiosyncrasy 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 = selected_row_ids isa Nothing ? [] : selected_row_ids

        if row_ids isa Nothing
            dff = df
            row_ids = df[!,"id"]
        else
            dff = filter(r -> any(occursin.(row_ids, r.country)), df)
        end
        active_row_id = !(active_cell isa Nothing) ? active_cell.row_id : Nothing

        colors = [( id == active_row_id) ? "#FF69B4" : (id in selected_id_set ?
        "#7FDBFF" : "#0074D9") for id in row_ids]
        return [
          if column in names(dff)
            dcc_graph(
                id=string(column, "--row-ids"),
                figure=Dict(
                    "data" => [
                      Dict(
                            "x"=> dff[!,"country"],
                            "y"=> dff[!,column],
                            "type"=> "bar",
                            "marker"=> Dict("color"=> colors),
                      )
                    ],
                    "layout"=> Dict(
                        "xaxis"=> Dict("automargin"=> true),
                        "yaxis"=> Dict(
                            "automargin"=> true,
                            "title"=> Dict("text"=> column)
                        ),
                        "height"=> 250,
                        "margin"=> Dict("t"=> 10, "l"=> 10, "r"=> 10),
                    ),
                ),
            )
          end
            # 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"]
        ]
    end
    run_server(app, "0.0.0.0", debug=true)

|  | country | pop | continent | lifeExp | gdpPercap
--- | --- | --- | --- | --- | --- | ---
× |  | Afghanistan | 31889923 | Asia | 43.828 | 974.5803384
× |  | Albania | 3600523 | Europe | 76.423 | 5937.029525999999
× |  | Algeria | 33333216 | Africa | 72.301 | 6223.367465
× |  | Angola | 12420476 | Africa | 42.731 | 4797.231267
× |  | Argentina | 40301927 | Americas | 75.32 | 12779.37964
× |  | Australia | 20434176 | Oceania | 81.235 | 34435.367439999995
× |  | Austria | 8199783 | Europe | 79.829 | 36126.4927
× |  | Bahrain | 708573 | Asia | 75.635 | 29796.04834
× |  | Bangladesh | 150448339 | Asia | 64.062 | 1391.253792
× |  | Belgium | 10392226 | Europe | 79.441 | 33692.60508

1
/
15

## Update Filter Placeholder Text

> This feature is available in Dash 2.7 and later.

By default, the filter fields, located above each column, display the placeholder text `filter data...`. Update filter placeholder text by setting `placeholder_text` on `filter_options` on the table or individual columns. In this example, we update the `placeholder_text` for the table to be `Filter column...`:

> This example has not been ported to Julia yet - showing the Python version instead.

> Visit the old docs site for Julia at: https://community.plotly.com/c/dash/julia/20

    from dash import Dash, dash_table
    import pandas as pd

    df = pd.read_csv(
        "https://raw.githubusercontent.com/plotly/datasets/master/gapminder2007.csv"
    )[["country", "pop", "continent", "lifeExp"]]

    app = Dash()

    app.layout = dash_table.DataTable(
        df.to_dict("records"),
        [{"name": i, "id": i} for i in df.columns],
        filter_action="native",
        filter_options={"placeholder_text": "Filter column..."},
        page_size=10,
    )

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

country | pop | continent | lifeExp
--- | --- | --- | ---
Afghanistan | 31889923 | Asia | 43.828
Albania | 3600523 | Europe | 76.423
Algeria | 33333216 | Africa | 72.301
Angola | 12420476 | Africa | 42.731
Argentina | 40301927 | Americas | 75.32
Australia | 20434176 | Oceania | 81.235
Austria | 8199783 | Europe | 79.829
Bahrain | 708573 | Asia | 75.635
Bangladesh | 150448339 | Asia | 64.062
Belgium | 10392226 | Europe | 79.441

1
/
15