Dash Visualization to Highlight a Table Row Based on Hover Data

I am trying to build an interactive dash plotly visual , containing a bunch of images and a table. The following code generates some synthetic data, with X having 536 normalized heatmaps of dimension 96x96 and y containing integers 0 to 9. Thus 0 has 200 samples , 1 has 10 samples and so on. For each element in y , there's a 96x96 heatmap with values element/10.

from dash import Dash, dcc, html, Input, Output,callback
from dash import dash_table
import plotly.express as px
import plotly.graph_objects as go
import pandas as pd
import numpy as np
import math


samples = [200,10,5,40,60,100,2,100,9,10]
idx = [n for n in range(10)]

for i,smp in zip(idx,samples):
    if i == 0:
        X = np.full((smp,96, 96), i/10)
        y = np.full((smp,),i)
    else:
        tempx = np.full((smp,96, 96), i/10)
        tempy = np.full((smp,),i)
        X = np.r_[X,tempx]
        y = np.r_[y,tempy]}

Now when I build the visualization, I get the expected behavior. When I select a number from the dropdown , the corresponding images show up along with a data-frame showing the chosen digit and the sample index. I want to add a behavior, such that when I hover on a image, the corresponding row in data-frame should get highlighted.

app = Dash(__name__)
tbl_cols = ['Choice','Samples']

app.layout = html.Div([
    dcc.Dropdown([i for i in range(10)],
                 0,
                 id='my_dropdown'),
    html.Div([
                html.Div([
                    dcc.Graph(id='my_picbox',style={'display':'inline-block'})
                    ],style={'width': '40%', 'display': 'inline-block'}),

                html.Div([  
                    dash_table.DataTable(
                        id = 'table',
                        columns = [{'name': i, 'id': i} for i in tbl_cols]),
                    ],style={'width': '60%', 'display': 'inline-block'}) 
            ],style={'display': 'flex'})
])


@callback(
    [Output(component_id='my_picbox', component_property='figure'),
     Output(component_id='table', component_property='data')],
    Input(component_id='my_dropdown', component_property='value')
)

def update_plot(digit):

        
    if digit is not None:
        samples = np.where(y==digit)[0]
        
        if len(samples)>20:
            samples = np.random.choice(samples,20)

        imgs = np.empty((20,96,96),)
        imgs[:]= np.nan
        imgs[0:len(samples)]=X[samples, :, :]
                
        fig = px.imshow(imgs[:, :, :],
                        binary_string=False,
                        zmin=0,
                        zmax=1,
                        facet_col=0,
                        aspect = 'auto',
                        facet_col_wrap=5,
                        facet_row_spacing = 0,
                        color_continuous_scale='rdylgn')

        for i in fig.layout.annotations:
            n = int(i['text'].split('=')[1])
            try:
                i['text']=str(samples[n])
            except:
                i['text']=' '
    
    fig.update_layout(margin = dict(t=70, l=50, r=0, b=5),
                      #coloraxis_showscale = False,
                      height = 600,
                      width = 600,)
    
    
    df = pd.DataFrame(data = {'Samples':samples})
    df['Choice'] = digit 
    data = df[tbl_cols].to_dict('records')
    

    return fig,data


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

1 Answer

Here is an example of a Dash app where the live hover_data attribute of a Plotly graph synchronously highlights the respective row in a Dash data table

import math

import numpy as np
import pandas as pd
import plotly.express as px
import plotly.graph_objects as go

from dash import Dash, Input, Output
from dash import callback, ctx, dash_table
from dash import dcc, html
from dash.exceptions import PreventUpdate


samples = [200, 10, 5, 40, 60, 100, 2, 100, 9, 10]
idx = [n for n in range(10)]

for i, smp in zip(idx, samples):
    if i == 0:
        X = np.full((smp, 96, 96), i / 10)
        y = np.full((smp,), i)
    else:
        tempx = np.full((smp, 96, 96), i / 10)
        tempy = np.full((smp,), i)
        X = np.concatenate([X, tempx], axis=0)
        y = np.concatenate([y, tempy], axis=0)


app = Dash(__name__)

tbl_cols = ["Choice", "Samples"]

app.layout = html.Div(
    [
        dcc.Dropdown([i for i in range(10)], 0, id="my_dropdown"),
        html.Div(
            [
                html.Div(
                    [
                        dcc.Graph(
                            id="my_picbox",
                        )
                    ],
                ),
                html.Div(
                    [
                        dash_table.DataTable(
                            id="table",
                            columns=[{"name": i, "id": i} for i in tbl_cols],
                            style_cell={"textAlign": "center"}
                        ),
                    ],
                    style={"width": "60%"},
                ),
            ],
            style={"display": "flex"},
        ),
    ]
)


# Add an additional output for the table's style_data_conditional property
@app.callback(
    [
        Output(component_id="my_picbox", component_property="figure"),
        Output(component_id="table", component_property="data"),
        Output(component_id="table", component_property="style_data_conditional"),
    ],
    [
        Input(component_id="my_dropdown", component_property="value"),
        Input(component_id="my_picbox", component_property="hoverData"),
    ],
)
def update_plot(digit, hover_data):
    if digit is None:
        raise PreventUpdate
    else:
        samples = np.where(y == digit)[0]

        if len(samples) > 20:
            # samples = np.random.choice(samples, 20)
            samples = samples[:20]

        imgs = np.empty((20, 96, 96),)
        imgs[:] = np.nan
        imgs[0 : len(samples)] = X[samples, :, :]

        fig = px.imshow(
            imgs[:, :, :],
            binary_string=False,
            zmin=0,
            zmax=1,
            facet_col=0,
            aspect="auto",
            facet_col_wrap=5,
            facet_row_spacing=0,
            color_continuous_scale="rdylgn",
        )

        for i in fig.layout.annotations:
            n = int(i["text"].split("=")[1])
            try:
                i["text"] = str(samples[n])
            except:
                i["text"] = " "

    triggered_component = ctx.triggered_id

    # Extract hover data
    hovered_sample = None
    if hover_data and triggered_component == "my_picbox":
        curve_number = hover_data["points"][0]["curveNumber"]
        if curve_number < len(samples):
            hovered_sample = samples[curve_number]

    fig.update_layout(
        margin=dict(t=70, l=50, r=0, b=5),
        height=600,
        width=600,
    )

    df = pd.DataFrame(data={"Samples": samples})
    df["Choice"] = digit
    data = df[tbl_cols].to_dict("records")

    # Generate conditional styling based on hover data
    style_data_conditional = []
    if hovered_sample is not None:
        style_data_conditional = [
            {
                "if": {
                    "filter_query": "{{Samples}} = {}".format(hovered_sample)
                },
                "backgroundColor": "lightblue",
            }
        ]

    return fig, data, style_data_conditional


if __name__ == "__main__":
    app.run_server(debug=True, port=8056)

gives, for example, the following app functionality:

1
Marcus Vance

Marcus Vance

Cybersecurity & Digital Privacy Researcher

Marcus Vance is a cybersecurity auditor and technology writer dedicated to educating the public about online safety, data privacy regulations, enterprise security, and emerging cyber threats.

Share this article