'Python Dash: Get the edited cell value from filterable table

Take the code from Dash's Userguide (https://dash.plot.ly/datatable/filtering) as example:

import dash
from dash.dependencies import Input, Output
import dash_table
import pandas as pd


app = dash.Dash(__name__)

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

app.layout = dash_table.DataTable(
    id='table-filtering-be',
    columns=[
        {"name": i, "id": i} for i in sorted(df.columns)
    ],
    editable=True,
    filter_action='custom',
    filter_query=''
)

operators = [['ge ', '>='],
             ['le ', '<='],
             ['lt ', '<'],
             ['gt ', '>'],
             ['ne ', '!='],
             ['eq ', '='],
             ['contains '],
             ['datestartswith ']]


def split_filter_part(filter_part):
    for operator_type in operators:
        for operator in operator_type:
            if operator in filter_part:
                name_part, value_part = filter_part.split(operator, 1)
                name = name_part[name_part.find('{') + 1: name_part.rfind('}')]

                value_part = value_part.strip()
                v0 = value_part[0]
                if (v0 == value_part[-1] and v0 in ("'", '"', '`')):
                    value = value_part[1: -1].replace('\\' + v0, v0)
                else:
                    try:
                        value = float(value_part)
                    except ValueError:
                        value = value_part

                # word operators need spaces after them in the filter string,
                # but we don't want these later
                return name, operator_type[0].strip(), value

    return [None] * 3


@app.callback(
    Output('table-filtering-be', "data"),
    [Input('table-filtering-be', "filter_query")])
def update_table(filter):
    filtering_expressions = filter.split(' && ')
    dff = df
    for filter_part in filtering_expressions:
        col_name, operator, filter_value = split_filter_part(filter_part)

        if operator in ('eq', 'ne', 'lt', 'le', 'gt', 'ge'):
            # these operators match pandas series operator method names
            dff = dff.loc[getattr(dff[col_name], operator)(filter_value)]
        elif operator == 'contains':
            dff = dff.loc[dff[col_name].str.contains(filter_value)]
        elif operator == 'datestartswith':
            # this is a simplification of the front-end filtering logic,
            # only works with complete fields in standard format
            dff = dff.loc[dff[col_name].str.startswith(filter_value)]

    return dff.to_dict('records')


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

Question

I want to edit cells of a filtered datatable.

Is there a way to fire a callback when one of the cells gets edited? I imagine using Input('table-filtering-be', 'data') won't work, because every time you filter the table the 'data' gets changed and therefore the callback gets fired.

Is there a way to target exactly the cell that I edited and get the edited text, while keeping the filtering function?



Solution 1:[1]

In order to trigger the callback when a cell value gets changed, you have to take as input data_timestamp.

So in your case, it would be : Input('table-filtering-be', "data_timestamp").

To know exactly what cell has changed, this proves to be more challenging. Dash mentions it in their documentation (https://dash.plotly.com/datatable/editable), but the section is empty.

There's currently an open issue on Github, but without an answer yet. (https://github.com/plotly/dash/issues/1162)

EDIT : I tried to answer this problem, here is the code snippet that does it (the function returns the modified line, take what you need in order to do what you want) :

def get_modified_line(timestamp, table, table_previous):
    # Here, table comes from State('table', 'data') and table_previous from State('table', 'data_previous'). 
    # To trigger the callback, use Input('table', 'data_timestamp').

    table = pd.DataFrame(table)
    previous_table = pd.DataFrame(table_previous)


    editable_columns = [] # This list should contain the columns that are editable (those that can change with user input, plus a line identifier such as the index)

    # We concatenate the two tables and drop the duplicates in order to keep the previous state and the actual state of the line that has changed
    modified = pd.concat([table , previous_table]).drop_duplicates(keep=False)

    # We keep the last line of the two remaining lines (last line because of the order in concat)
    modified = modified.drop_duplicates(keep='last', subset=editable_columns)

    # We grab the only line (just to be safe), assuming that only one line can change at a time, using .iloc[[0]] 
    modified = modified.reset_index(drop=True).iloc[[0]]

    return modified

This is not very elegant, but it got the job done in my case.

I hope this answers your question.

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1