'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 |