thiskillsme
thiskillsme

Reputation: 53

Unselect active_cell in dash datatable (python)

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

Answers (2)

Paul Cuddihy
Paul Cuddihy

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

thiskillsme
thiskillsme

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

Related Questions