Reputation: 53
All,
I am trying to implement a dash datatable, where I select rows by a direct click on it (no radio buttons). Currently, I am doing this with the active_cell
and it works well:
No matter in which cell of a row a user clicks, a graph is updated with the data in that row. If he clicks another cell in the same row, the data is unselected (via a dcc.storage)
Here comes my problem: If the user clicks the same cell again, there is no active_cell
event triggered. Therefore, my callback is not triggered and nothing happens.
I would like to deselect that cell the second time the user clicks it. How can I implement that?
Thanks!
Upvotes: 1
Views: 5152
Reputation: 567
A simpler solution (but it has other drawbacks) is to select a hidden cell. This makes it appear to the user that nothing is selected.
In the example below a cell is processed and de-selected by a callback. Clearly this callback could also be for a "deselect all" button or whatever else you need.
Add column called "cant_see":
df = pd.read_csv("my_data.csv")
df.insert(0, "cant_see", ["" for i in df.iloc[:, 0] ])
Make it hidden when you create the DataTable using style_data_conditional and style_header_conditional:
dash_table.DataTable(
id="table",
columns=[{"name": i, "id": i} for i in df.columns],
data=df.to_dict("records"),
is_focused=True,
style_data_conditional=[
{'if': {'column_id': 'cant_see',}, 'display': 'None',}
],
style_header_conditional=[
{'if': {'column_id': 'cant_see',}, 'display': 'None',}
],
)
and then a callback can set the table's "active_cell" and/or "selected_cells"
@app.callback(
Output("table", "active_cell"),
Output("table", "selected_cells"),
Input("table", "active_cell"),)
def cell_clicked(cell):
# Do something useful with cell,
# such as toggling it's style to mimic select/de-select
# Now make it look like the cell is de-selected
# by selecting a hidden cell
#
# return active cell 0,0 and selected_cells list [ 0,0 ]
return {'column':0, 'row':0}, [{'column':0, 'row':0}]
Upvotes: 3
Reputation: 53
So... I solved this... it is not pretty but it works - it includes a loopbreaker which I had to implement to avoid a circular dependency, but yeah - I am absolutely open for cleaner solutions.
Find below the callbacks
# takes user selected cell (active_cell) and the current state of a dcc.storage (tableclick) which stores the last saved row that was clicked
# output: updated selected_rows for the datatable, styling for the selected row and update for dcc.storage
# if no cell is selected, do nothing
# if no cell is selected, but there is a row stored as selected, highlight that row (this is a consequence from the circular dependency)
# if a cell is selected that is different from the previous cell, highlight that new row. Otherwise, deselect the row.
@app.callback(
[Output('performancedatatable', 'style_data_conditional'), Output('tableclick', 'data'),
Output('performancedatatable', 'selected_rows')],
[
Input('performancedatatable', 'active_cell'),
], [State('tableclick', 'data')]
)
def highlight_row(cell, prevrow):
if cell is None:
if prevrow is None:
return [{}], None, []
else:
return [{}], None, prevrow
elif "row" in cell:
if cell.get("row", "") == prevrow:
return [{}], None, []
else:
return ([{
"if": {"row_index": cell.get("row", "")},
"backgroundColor": "rgba(146, 192, 234, 0.5)",
}], cell.get("row", ""), [cell.get("row", "")])
# Is triggered by changing the dcc.storage "tableclick"
# resets active cell and selected cell via callback below
@app.callback([Output('loopbreaker_div', "children")], [Input('tableclick', 'data')])
def reset_active_cell(input):
return [html.Div(id='loopbreaker', children=True)]
#loopbreaker to avoid circular dependency
@app.callback([Output('performancedatatable', "active_cell"), Output('performancedatatable', 'selected_cells')], [Input('loopbreaker', 'children')])
def reset_active_cell(input):
time.sleep(1)
return (None, [])
Shoutout to http://yaaics.blogspot.com/2019/03/circular-references-in-plotlydash.html for helping resolving the circular dependency
Upvotes: 4