Highlighting a selected row in DataTable

I have a relatively complex dash app with 2 datatables (A and B) and 3 graphs (P and Q).
On selection of rows in table A, a callback retrieves data (expensive operation), plots graph P and fills table B. It also generates conditional rules to set a background color on rows in table B, based on what item was selected in table A (ie. there is 1-to-many relationship between table A records and table B records, and I use colors to make them clear).

Table B also allows selection of multiple rows. Based on the selection, I go retrieve more data (another expensive operation) and plot graph Q.
I would like to also find a way of using colors to tie in table B rows and the Q graph. I was therefore hoping to be able to set a border colour around the selected rows in table B when they are selected. (border color would show link between table B and graph Q, whereas background color shows link between table B and table A).

I did not find a direct way of setting border color when selecting the row.
I then thought that maybe I could use a callback, using Input(“tableB”, “selected_rows”) and Output(“tableB”, “style_data_conditional”).

Unfortunately when I do that, I get the following error:
Multi output ..tableB.style_data_conditional.. contains an `Output` object that was already assigned. Duplicates: {'table_outputs.style_data_conditional'}

Maybe I could add to the other set of style_data_conditional in the first callback (filling B from table A selection), but that would force all the data being retrieved again to fill B when I am just selecting a row…

Any recommendation?

1 Like

Here is an adapted example from the documentation that highlights the selected row in a dash datatable. The use of derived_virtual_selected_rows allos the highlighting to follow the entry around as you filter and sort the table. The table banding colors are optional.

import dash
from dash.dependencies import Input, Output
import dash_table
import dash_core_components as dcc
import dash_html_components as html
import pandas as pd

def get_style_data_conditional(selected_rows: list = []) -> list:
    non_selected_band_color = "rgb(229, 236, 246)"
    selected_band_color = '#98c21f'
    return [
        {
            'if': {'row_index': 'odd'},
            'backgroundColor': non_selected_band_color
        },
        {
            'if': {'row_index': 'even'},
            'backgroundColor': "white"
        },
        {
            'if': {'row_index': selected_rows},
            'backgroundColor': selected_band_color,
            'fontWeight': 'bold',
            'color': 'white',
        },
    ]

df = pd.read_csv('https://raw.githubusercontent.com/plotly/datasets/master/gapminder2007.csv')
# add an id column and set it as the index
# in this case the unique ID is just the country name, so we could have just
# renamed 'country' to 'id' (but given it the display name 'country'), but
# here it's duplicated just to show the more general pattern.
df['id'] = df['country']
df.set_index('id', inplace=True, drop=False)

app = dash.Dash(__name__)

app.layout = html.Div([
    dash_table.DataTable(
        id='datatable-row-ids',
        columns=[
            {'name': i, 'id': i, 'deletable': True} for i in df.columns
            # omit the id column
            if i != 'id'
        ],
        data=df.to_dict('records'),
        editable=True,
        filter_action="native",
        sort_action="native",
        sort_mode='multi',
        row_selectable='multi',
        row_deletable=True,
        selected_rows=[],
        page_action='native',
        style_data_conditional=get_style_data_conditional(),
        page_current= 0,
        page_size= 100,
    ),
    html.Div(id='datatable-row-ids-container')
])


@app.callback(
    [Output('datatable-row-ids-container', 'children'),
     Output('datatable-row-ids', 'style_data_conditional')],
    Input('datatable-row-ids', 'derived_virtual_row_ids'),
    Input('datatable-row-ids', 'selected_row_ids'),
    Input('datatable-row-ids', 'active_cell'),
    Input('datatable-row-ids', 'derived_virtual_selected_rows')) 
def update_graphs(row_ids, selected_row_ids, active_cell, selected_rows):
    # When the table is first rendered, `derived_virtual_data` and
    # `derived_virtual_selected_rows` will be `None`. This is due to an
    # idiosyncrasy in Dash (unsupplied properties are always None and Dash
    # calls the dependent callbacks when the component is first rendered).
    # So, if `rows` is `None`, then the component was just rendered
    # and its value will be the same as the component's dataframe.
    # Instead of setting `None` in here, you could also set
    # `derived_virtual_data=df.to_rows('dict')` when you initialize
    # the component.
    selected_id_set = set(selected_row_ids or [])

    table_data_style_conditional = get_style_data_conditional(selected_rows)

    if row_ids is None:
        dff = df
        # pandas Series works enough like a list for this to be OK
        row_ids = df['id']
    else:
        dff = df.loc[row_ids]

    active_row_id = active_cell['row_id'] if active_cell else None

    colors = ['#FF69B4' if id == active_row_id
              else '#7FDBFF' if id in selected_id_set
              else '#0074D9'
              for id in row_ids]

    return [
        dcc.Graph(
            id=column + '--row-ids',
            figure={
                'data': [
                    {
                        'x': dff['country'],
                        'y': dff[column],
                        'type': 'bar',
                        'marker': {'color': colors},
                    }
                ],
                'layout': {
                    'xaxis': {'automargin': True},
                    'yaxis': {
                        'automargin': True,
                        'title': {'text': column}
                    },
                    'height': 250,
                    'margin': {'t': 10, 'l': 10, 'r': 10},
                },
            },
        )
        # check if column exists - user may have deleted it
        # If `column.deletable=False`, then you don't
        # need to do this check.
        for column in ['pop', 'lifeExp', 'gdpPercap'] if column in dff
    ], table_data_style_conditional



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