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
AfghanistanAngolaAustriaBelgiumBosnia and HerzegovinaBulgariaCambodiaCentral African RepublicChinaCongo, Dem. Rep.Cote d'IvoireCzech RepublicDominican RepublicEl SalvadorEthiopiaGabonGhanaGuineaHondurasIcelandIranIsraelJapanKorea, Dem. Rep.LebanonLibyaMalaysiaMauritiusMontenegroMyanmarNetherlandsNigerOmanParaguayPolandReunionSao Tome and PrincipeSerbiaSlovak RepublicSouth AfricaSudanSwitzerlandTanzaniaTrinidad and TobagoUgandaUruguayWest Bank and GazaZimbabwe00.5B1B
pop
AfghanistanAngolaAustriaBelgiumBosnia and HerzegovinaBulgariaCambodiaCentral African RepublicChinaCongo, Dem. Rep.Cote d'IvoireCzech RepublicDominican RepublicEl SalvadorEthiopiaGabonGhanaGuineaHondurasIcelandIranIsraelJapanKorea, Dem. Rep.LebanonLibyaMalaysiaMauritiusMontenegroMyanmarNetherlandsNigerOmanParaguayPolandReunionSao Tome and PrincipeSerbiaSlovak RepublicSouth AfricaSudanSwitzerlandTanzaniaTrinidad and TobagoUgandaUruguayWest Bank and GazaZimbabwe020406080
lifeExp
AfghanistanAngolaAustriaBelgiumBosnia and HerzegovinaBulgariaCambodiaCentral African RepublicChinaCongo, Dem. Rep.Cote d'IvoireCzech RepublicDominican RepublicEl SalvadorEthiopiaGabonGhanaGuineaHondurasIcelandIranIsraelJapanKorea, Dem. Rep.LebanonLibyaMalaysiaMauritiusMontenegroMyanmarNetherlandsNigerOmanParaguayPolandReunionSao Tome and PrincipeSerbiaSlovak RepublicSouth AfricaSudanSwitzerlandTanzaniaTrinidad and TobagoUgandaUruguayWest Bank and GazaZimbabwe020k40k
gdpPercap

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
AfghanistanAngolaAustriaBelgiumBosnia and HerzegovinaBulgariaCambodiaCentral African RepublicChinaCongo, Dem. Rep.Cote d'IvoireCzech RepublicDominican RepublicEl SalvadorEthiopiaGabonGhanaGuineaHondurasIcelandIranIsraelJapanKorea, Dem. Rep.LebanonLibyaMalaysiaMauritiusMontenegroMyanmarNetherlandsNigerOmanParaguayPolandReunionSao Tome and PrincipeSerbiaSlovak RepublicSouth AfricaSudanSwitzerlandTanzaniaTrinidad and TobagoUgandaUruguayWest Bank and GazaZimbabwe00.5B1B
pop
AfghanistanAngolaAustriaBelgiumBosnia and HerzegovinaBulgariaCambodiaCentral African RepublicChinaCongo, Dem. Rep.Cote d'IvoireCzech RepublicDominican RepublicEl SalvadorEthiopiaGabonGhanaGuineaHondurasIcelandIranIsraelJapanKorea, Dem. Rep.LebanonLibyaMalaysiaMauritiusMontenegroMyanmarNetherlandsNigerOmanParaguayPolandReunionSao Tome and PrincipeSerbiaSlovak RepublicSouth AfricaSudanSwitzerlandTanzaniaTrinidad and TobagoUgandaUruguayWest Bank and GazaZimbabwe020406080
lifeExp
AfghanistanAngolaAustriaBelgiumBosnia and HerzegovinaBulgariaCambodiaCentral African RepublicChinaCongo, Dem. Rep.Cote d'IvoireCzech RepublicDominican RepublicEl SalvadorEthiopiaGabonGhanaGuineaHondurasIcelandIranIsraelJapanKorea, Dem. Rep.LebanonLibyaMalaysiaMauritiusMontenegroMyanmarNetherlandsNigerOmanParaguayPolandReunionSao Tome and PrincipeSerbiaSlovak RepublicSouth AfricaSudanSwitzerlandTanzaniaTrinidad and TobagoUgandaUruguayWest Bank and GazaZimbabwe020k40k
gdpPercap

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