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

    using Dash
    using CSV, DataFrames

    app = dash()

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

    app.layout = html_div([
        dash_datatable(
            id="table-editing-simple",
            columns=(
                vcat([Dict("id" =>  "Model", "name" =>  "Model")],
                [Dict("id" =>  p, "name" =>  p) for p in params])
            ),
            data=[Dict("Model" => i, [param => 0
              for param in params]...) for i in 1:5
            ],
            editable=true
        ),
        dcc_graph(id="table-editing-simple-output")
    ])

    callback!(app,
        Output("table-editing-simple-output", "figure"),
        Input("table-editing-simple", "data"),
        Input("table-editing-simple", "columns")
        ) do rows, columns
        df = DataFrame(rows)
        select!(df, [c.name for c in columns])
        return (
            "data" =>  [Dict(
                "type" =>  "parcoords",
                "dimensions" =>  [Dict(
                    "label" =>  col.name,
                    "values" =>  df[!, col.id]
                ) for col in columns]
            )]
        )
    end

    run_server(app, "0.0.0.0", debug=true)

Model | Weight | Torque | Width | Height | Efficiency | Power | Displacement
--- | --- | --- | --- | --- | --- | --- | ---
1 | 0 | 0 | 0 | 0 | 0 | 0 | 0
2 | 0 | 0 | 0 | 0 | 0 | 0 | 0
3 | 0 | 0 | 0 | 0 | 0 | 0 | 0
4 | 0 | 0 | 0 | 0 | 0 | 0 | 0

WebGL is not supported by your browser - visit https://get.webgl.org for more info

WebGL is not supported by your browser - visit https://get.webgl.org for more info

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

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.

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!

    using Dash
    using Random

    Random.seed!(123)

    using CSV, DataFrames

    app = dash()

    app.layout = html_div([
        "Choose property to load:  ",
        dcc_dropdown(
            id="loading-states-table-prop",
            options=[
                Dict("label" =>  prop, "value" =>  prop)
                for prop in ["style_cell", "data"]
            ]
        ),
        html_br(),
        dash_datatable(
            id="loading-states-table",
            columns=[Dict(
                "name" =>  "Column $i",
                "id" =>  "column-$i",
                "deletable" =>  true,
                "renamable" =>  true
            ) for i in 1:5],
            data=data=[
              Dict("column-$i" => (rand(1:100)) for i in 1:5)
               for j in 1:5],
            editable=true
        )
    ])

    callback!(app,
        Output("loading-states-table", "style_cell"),
        Input("loading-states-table-prop", "value")
    ) do value
        if value == "style_cell"
            sleep(5)
            return Dict("color" =>  "rgb($(rand(0:255)), $(rand(0:255)), $(rand(0:255)))")
        end
        throw(PreventUpdate())
    end

    callback!(app,
        Output("loading-states-table", "data"),
        Input("loading-states-table-prop", "value")
    ) do value
        if value == "data"
            sleep(5)
            return [
                Dict("column-$i" =>
                 (rand(0:100)) for i in 1:5)
                for j in 1:5
            ]
        end
        throw(PreventUpdate())
    end

    run_server(app, "0.0.0.0", debug=true)

Choose property to load:
Select...

Column 1 | Column 2 | Column 3 | Column 4
--- | --- | --- | ---
49 | 97 | 53 | 5
33 | 65 | 62 | 51
100 | 38 | 61 | 45
74 | 27 | 64 | 17
36 | 17 | 96 | 12

## 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 `""` (empty 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 epmty data to ``nothing` 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.

    using Dash
    using Random

    Random.seed!(123)

    using CSV, DataFrames

    app = dash()

    app.layout = html_div([
    dash_datatable(
          id="editing-prune-data",
          columns=[Dict(
              "name" =>  "Column $i",
              "id" =>  "column-$i"
          ) for i in 1:4],
          data=[
              Dict("column-$i" =>  (j + (i-1)*5)-1 for i in 1:4)
              for j in 1:5
          ],
          editable=true
      ),
      html_div(id="editing-prune-data-output")
    ])

    callback!(app,
    Output("editing-prune-data-output", "children"),
                  Input("editing-prune-data", "data")
    ) do rows
        pruned_rows = NamedTuple[]
        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 values(row)])
                push!(pruned_rows,row)
            end
        end
        return html_div([
            html_div("Raw Data"),
            html_pre(string(rows)),
            html_hr(),
            html_div("Pruned Data"),
            html_pre(string(pruned_rows)),
        ])
    end

    run_server(app, "0.0.0.0", debug=true)

Column 1 | Column 2 | Column 3 | Column 4
--- | --- | --- | ---
0 | 5 | 10 | 15
1 | 6 | 11 | 16
2 | 7 | 12 | 17
3 | 8 | 13 | 18
4 | 9 | 14 | 19

Raw Data

    [{'column-1': 0, 'column-2': 5, 'column-3': 10, 'column-4': 15},
     {'column-1': 1, 'column-2': 6, 'column-3': 11, 'column-4': 16},
     {'column-1': 2, 'column-2': 7, 'column-3': 12, 'column-4': 17},
     {'column-1': 3, 'column-2': 8, 'column-3': 13, 'column-4': 18},
     {'column-1': 4, 'column-2': 9, 'column-3': 14, 'column-4': 19}]

Pruned Data

    [{'column-1': 0, 'column-2': 5, 'column-3': 10, 'column-4': 15},
     {'column-1': 1, 'column-2': 6, 'column-3': 11, 'column-4': 16},
     {'column-1': 2, 'column-2': 7, 'column-3': 12, 'column-4': 17},
     {'column-1': 3, 'column-2': 8, 'column-3': 13, 'column-4': 18},
     {'column-1': 4, 'column-2': 9, 'column-3': 14, 'column-4': 19}]

## Uploading Data

A nice recipe is to tie the `dcc.Upload` 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.

    using Dash
    using CSV, DataFrames
    using Base64

    external_stylesheets = ["https://codepen.io/chriddyp/pen/bWLwgP.css"]
    app = dash(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=Dict(
              "width" =>  "100%", "height" =>  "60px", "lineHeight" =>  "60px",
              "borderWidth" =>  "1px", "borderStyle" =>  "dashed",
              "borderRadius" =>  "5px", "textAlign" =>  "center", "margin" =>  "10px"
          ),
      ),
      dash_datatable(id="datatable-upload-container"),
      dcc_graph(id="datatable-upload-graph")
    ])
    function parse_contents(contents, filename)
        content_type, content_string = split(contents, ',')
        decoded = base64decode(content_string)
        df = DataFrame()
        try
          if occursin("csv", filename)
            str = String(decoded)
            df =  CSV.read(IOBuffer(str), DataFrame)
          end
        catch e
          print(e)
          return html_div([
              "There was an error processing this file."
          ])
        end
        return df
      end

    callback!(app,
        Output("datatable-upload-container", "data"),
        Output("datatable-upload-container", "columns"),
        Input("datatable-upload", "contents"),
        State("datatable-upload", "filename")
    ) do contents, filename
        if contents isa Nothing
            return [Dict()], []
        end
        df = parse_contents(contents, filename)
        return Dict.(pairs.(eachrow(df))), [Dict("name" =>  i, "id" =>  i) for i in names(df)]
    end

    callback!(app,
        Output("datatable-upload-graph", "figure"),
        Input("datatable-upload-container", "data")
    ) do rows
        if any([isempty(rw) for rw in rows])
            return Dict(
                "data" =>  [Dict(
                    "x" =>  [],
                    "y" =>  [],
                    "type" =>  "bar"
                )]
            )
        end
        df = DataFrame(rows)
        nme = names(df)
        return Dict(
            "data" =>  [Dict(
                "x" =>  df[!, nme[1]],
                "y" =>  df[!, nme[2]],
                "type" =>  "bar"
            )]
        )
    end

    run_server(app, "0.0.0.0", debug=true)

Drag and Drop or Select Files

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

    using Dash
    using CSV, DataFrames

    app = dash()

    app.layout = html_div([
        html_div([
            dcc_input(
                id="editing-columns-name",
                placeholder="Enter a column name...",
                value="",
                style=Dict("padding" =>  10)
            ),
            html_button("Add Column", id="editing-columns-button", n_clicks=0)
        ], style=Dict("height" =>  50)),

        dash_datatable(
            id="editing-columns",
            columns=[Dict(
                "name" =>  "Column $i",
                "id" =>  "column-$i",
                "deletable" =>  true,
                "renamable" =>  true
            ) for i in 1:4],
            data=[
                Dict("column-$i" =>  (j + (i-1)*5)-1 for i in 1:4)
                for j in 1:5
            ],
            editable=true,
        ),
        dcc_graph(id="editing-columns-graph")
    ])

    callback!(app,
        Output("editing-columns", "columns"),
        Input("editing-columns-button", "n_clicks"),
        State("editing-columns-name", "value"),
        State("editing-columns", "columns")
    )  do n_clicks, value, existing_columns
        if n_clicks > 0 && value != ""
            push!(existing_columns, Dict(
                "id" =>  value, "name" =>  value,
                "renamable" =>  true, "deletable" =>  true
            ))
        end
        return existing_columns
    end

    callback!(app,
        Output("editing-columns-graph", "figure"),
        Input("editing-columns", "data"),
        Input("editing-columns", "columns")
    ) do rows, columns
        try
            return Dict(
                "data" =>  [Dict(
                    "type" =>  "heatmap",
                    "z" =>  [[row[Symbol(c.id)] for c in columns] for row in rows],
                    "x" =>  [c.name for c in columns]
                )]
            )
        catch
            throw(PreventUpdate())
        end
    end

    run_server(app, "0.0.0.0", debug=true)

Column 1 | Column 2 | Column 3 | Column 4
--- | --- | --- | ---
0 | 5 | 10 | 15
1 | 6 | 11 | 16
2 | 7 | 12 | 17
3 | 8 | 13 | 18
4 | 9 | 14 | 19

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

    using Dash
    using CSV, DataFrames

    app = dash()

    app.layout = html_div([
        html_div([
            dcc_input(
                id="adding-rows-name",
                placeholder="Enter a column name...",
                value="",
                style=Dict("padding" =>  10)
            ),
            html_button("Add Column", id="adding-rows-button", n_clicks=0)
        ], style=Dict("height" =>  50)),

        dash_datatable(
            id="adding-rows-table",
            columns=[Dict(
                "name" =>  "Column $i",
                "id" =>  "column-$i",
                "deletable" =>  true,
                "renamable" =>  true
            ) for i in 1:4],
            data=[
                Dict("column-$i" =>  (j + (i-1)*5)-1 for i in 1:4)
                for j in 1:5
            ],
            editable=true,
            row_deletable=true
        ),

        html_button("Add Row", id="editing-rows-button", n_clicks=0),

        dcc_graph(id="adding-rows-graph")
    ])

    callback!(app,
        Output("adding-rows-table", "data"),
        Input("editing-rows-button", "n_clicks"),
        State("adding-rows-table", "data"),
        State("adding-rows-table", "columns")
    ) do n_clicks, rows, columns
        if n_clicks > 0
            push!(rows, Dict(c.id =>  "" for c in columns))
        end
        return rows
    end

    callback!(app,
        Output("adding-rows-table", "columns"),
        Input("adding-rows-button", "n_clicks"),
        State("adding-rows-name", "value"),
        State("adding-rows-table", "columns")
    ) do n_clicks, value, existing_columns
        if (n_clicks > 0) && (value != "")
            push!(existing_columns, Dict(
                "id" =>  value, "name" =>  value,
                "renamable" =>  true, "deletable" =>  true
            ))
        end
        return existing_columns
    end

    callback!(app,
        Output("adding-rows-graph", "figure"),
        Input("adding-rows-table", "data"),
        Input("adding-rows-table", "columns")
    ) do rows, columns
        try
            return Dict(
                "data" =>  [Dict(
                    "type" =>  "heatmap",
                    "z" =>  [[row[Symbol(c.id)] for c in columns] for row in rows],
                    "x" =>  [c.name for c in columns]
                )]
            )
        catch
            throw(PreventUpdate())
        end
    end

    run_server(app, "0.0.0.0", debug=true)

| Column 1 | Column 2 | Column 3 | Column 4
--- | --- | --- | --- | ---
× | 0 | 5 | 10 | 15
× | 1 | 6 | 11 | 16
× | 2 | 7 | 12 | 17
× | 3 | 8 | 13 | 18
× | 4 | 9 | 14 | 19

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

    using Dash
    using CSV, DataFrames

    app = dash()

    app.layout = html_div([
        dash_datatable(
            id="computed-table",
            columns=[
                Dict("name" =>  "Input Data", "id" =>  "input-data"),
                Dict("name" =>  "Input Squared", "id" =>  "output-data")
            ],
            data=[Dict("input-data" =>  i) for i in 1:11],
            editable=true,
        ),
    ])

    callback!(app,
        Output("computed-table", "data"),
        Input("computed-table", "data_timestamp"),
        State("computed-table", "data")
        ) do timestamp, rows
        rows_dict =  map(r->Dict(keys(r) .=> values(r)),rows)
        for row in rows_dict
            try
                row[Symbol("output-data")] = Float64(row[Symbol("input-data")])^2
            catch
                row[Symbol("input-data")] = "NA"
            end
        end
        return rows_dict
    end

    run_server(app, "0.0.0.0", debug=true)

Input Data | Input Squared
--- | ---
0 | 0
1 | 1
2 | 4
3 | 9
4 | 16
5 | 25
6 | 36
7 | 49
8 | 64
9 | 81
10 | 100

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

    using Dash
    using CSV, DataFrames

    app = dash()

    app.layout = dash_datatable(
        columns=[
            Dict("name" =>  ["", "Year"], "id" =>  "year", "clearable" =>  "first" ),
            Dict("name" =>  ["City", "Montreal"], "id" =>  "montreal", "deletable" =>  [false, true]),
            Dict("name" =>  ["City", "Toronto"], "id" =>  "toronto", "renamable" =>  true ),
            Dict("name" =>  ["City", "Ottawa"], "id" =>  "ottawa", "hideable" =>  "last"),
            Dict("name" =>  ["City", "Vancouver"], "id" =>  "vancouver", "clearable" =>  true, "renamable" =>  true, "hideable" =>  true, "deletable" =>  true ),
            Dict("name" =>  ["Climate", "Temperature"], "id" =>  "temp"),
            Dict("name" =>  ["Climate", "Humidity"], "id" =>  "humidity"),
        ],
        data=[
            Dict(
                "year" =>  i,
                "montreal" =>  i * 10,
                "toronto" =>  i * 100,
                "ottawa" =>  i * -1,
                "vancouver" =>  i * -10,
                "temp" =>  i * -100,
                "humidity" =>  i * 5,
            )
            for i in 1:10
        ],
        css=[
            Dict("selector" =>  ".column-header--delete svg", "rule" =>  "display:  none"),
            Dict("selector" =>  ".column-header--delete::before", "rule" =>  "content: X")
        ]
    )

    run_server(app, "0.0.0.0", debug=true)

| City | City | City | City | Climate | Climate
--- | --- | --- | --- | --- | --- | ---
Year | Montreal | Toronto | Ottawa | Vancouver | Temperature | Humidity
0 | 0 | 0 | 0 | 0 | 0 | 0
1 | 10 | 100 | -1 | -10 | -100 | 5
2 | 20 | 200 | -2 | -20 | -200 | 10
3 | 30 | 300 | -3 | -30 | -300 | 15
4 | 40 | 400 | -4 | -40 | -400 | 20
5 | 50 | 500 | -5 | -50 | -500 | 25
6 | 60 | 600 | -6 | -60 | -600 | 30
7 | 70 | 700 | -7 | -70 | -700 | 35
8 | 80 | 800 | -8 | -80 | -800 | 40
9 | 90 | 900 | -9 | -90 | -900 | 45

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

    using Dash
    using CSV, DataFrames

    app = dash()

    app.layout = dash_datatable(
        columns=[
            Dict("name" =>  ["", "Year"], "id" =>  "year" ),
            Dict("name" =>  ["City", "Montreal"], "id" =>  "montreal", "deletable" =>  [false, true]),
            Dict("name" =>  ["City", "Toronto"], "id" =>  "toronto", "renamable" =>  true ),
            Dict("name" =>  ["City", "Ottawa"], "id" =>  "ottawa", "hideable" =>  "last"),
            Dict("name" =>  ["City", "Vancouver"], "id" =>  "vancouver"),
            Dict("name" =>  ["Climate", "Temperature"], "id" =>  "temp"),
            Dict("name" =>  ["Climate", "Humidity"], "id" =>  "humidity"),
        ],
        data=[
            Dict(
                "year" =>  i,
                "montreal" =>  i * 10,
                "toronto" =>  i * 100,
                "ottawa" =>  i * -1,
                "vancouver" =>  i * -10,
                "temp" =>  i * -100,
                "humidity" =>  i * 5,
            )
            for i in 1:10
        ],
        export_format="xlsx",
        export_headers="display",
        merge_duplicate_headers=true
    )

    run_server(app, "0.0.0.0", debug=true)

| City | Climate
--- | --- | ---
Year | Montreal | Toronto | Ottawa | Vancouver | Temperature | Humidity
0 | 0 | 0 | 0 | 0 | 0 | 0
1 | 10 | 100 | -1 | -10 | -100 | 5
2 | 20 | 200 | -2 | -20 | -200 | 10
3 | 30 | 300 | -3 | -30 | -300 | 15
4 | 40 | 400 | -4 | -40 | -400 | 20
5 | 50 | 500 | -5 | -50 | -500 | 25
6 | 60 | 600 | -6 | -60 | -600 | 30
7 | 70 | 700 | -7 | -70 | -700 | 35
8 | 80 | 800 | -8 | -80 | -800 | 40
9 | 90 | 900 | -9 | -90 | -900 | 45