Loading images on hover (from /assets) is a bit slow with thousands of images. How can I speed it up?

Hi there,

I am using this solution to populate stylized tooltips since the styling options on the SVG/path hover-templates don’t offer box-shadow, border-radius, etc.

@callback(
    Output("some_graphical_figure_bullshit_graph_tooltip", "show"),
    Output("some_graphical_figure_bullshit_graph_tooltip", "bbox"),
    Output("some_graphical_figure_bullshit_graph_tooltip", "children"),
    # get click input
    Input("some_graphical_figure_bullshit", "clickData"),
    Input("some_graphical_figure_bullshit", "hoverData"),
)
def display_hover(clickData, hoverData):
    try:
        pt = hoverData["points"][0]
    except:
        return False, {}, None
    bbox = pt["bbox"]
    num = pt["pointNumber"]
    df_row = real_df.iloc[num]
    img_src = df_row["id"]
    img_ext = df_row["img_ext"]
    img = f"assets/icons/{img_src}.{img_ext}"   # <---------------------- the image
    name = df_row["Token"]
    form = df_row["Platform"]
    price = df_row["Price [USD]"]

    if hoverData is None or hoverData["points"] == []:
        return False, no_update, no_update
    tooltip = [
        html.Div(
            [
                html.Img(
                    src=img,  # <---------------------- the image going into the tooltip
                    style={"width": "80px", "border-radius": "50%"},
                ),
                html.H3(
                    f"{name}",
                    style={"color": "#192437", "font-family": "ProductSans-Black"},
                ),
                html.P(f"{form}"),
                html.P(f"{price:.0f}"),
            ],
            style={
                "width": "225px",
                "white-space": "normal",
                "padding": "5px",
                "display": "flex",
                "flex-direction": "column",
                "align-items": "left",
                "justify-content": "center",
            },
        )
    ]

    return True, bbox, tooltip

I’m including minimal code here to show that there is an image being searched for, some data is selected from an available pandas df, and the tooltip is constructed… nothing too fancy… the assets folder is 13,427 images/logos deep, 250x250.

As I hover across the chart, I get the images to load, but I’m suspecting that getting the images (searching through a few thousand) is taking a relatively long time. In the gif, below, you can see the lag as one “sweeps” across. It gives a laggy feeling.

How would one go about speeding this up? Should I load all several thousand images as base64s into a Storage item? Or just file locally? Ideally, the solution would make the callback-driven tooltip pop-up with the same speed and fluidity as the built-in hovertext that’s generated with SVG elements… (maybe I’m asking too much?).

Thank you <3
plotly_probs

Hello @z.adamg,

You are actually encountering two issues here.

First, the block is displaying slow, because it is a serverside callback.

Second, yes, your image is loading slow because it is also being rendered by the network. Albeit, after the box is being displayed.

There are a couple of ways to handle this, first make the hover data a clientside callback. I’m looking at this right now.

Second, you can use base64, but I think you are going to encounter the browser rendering issue.

You could potentially look into a service worker to be able to load the assets from a cache instead. This would require hosting on a https site.

Another thing you could look into, is downgrading the size of the images for the hover information.

2 Likes

I think you hit a key point, which is that it’s probably mostly the Div tooltip itself that’s taking awhile vs the image load. I didn’t think of making into a clientside… I’m trying to think of how that would work since I’m also including data indexed from a dataframe inside the tooltip. Do I have both a serverside and clientside callback; the latter only handling the actual creation of the tooltip, but not the indexing other info from a dataframe?

Thanks for such a quick reply!

1 Like

You could make two callbacks, one for clientside which would display the box and the other which queries the data.

Like this example:

@z.adamg,

Here, I made a slight alteration to the clientside callback to push the data to the screen immediately.

from dash import Dash, dcc, html, Input, Output, no_update, State
import plotly.graph_objects as go
import pandas as pd

# Small molcule drugbank dataset
Source= 'https://raw.githubusercontent.com/plotly/dash-sample-apps/main/apps/dash-drug-discovery/data/small_molecule_drugbank.csv'

df = pd.read_csv(Source, header=0, index_col=0)

fig = go.Figure(data=[
    go.Scatter(
        x=df["LOGP"],
        y=df["PKA"],
        mode="markers",
        marker=dict(
            colorscale='viridis',
            color=df["MW"],
            size=df["MW"],
            colorbar={"title": "Molecular<br>Weight"},
            line={"color": "#444"},
            reversescale=True,
            sizeref=45,
            sizemode="diameter",
            opacity=0.8,
        )
    )
])

# turn off native plotly.js hover effects - make sure to use
# hoverinfo="none" rather than "skip" which also halts events.
fig.update_traces(hoverinfo="none", hovertemplate=None)

fig.update_layout(
    xaxis=dict(title='Log P'),
    yaxis=dict(title='pkA'),
    plot_bgcolor='rgba(255,255,255,0.1)'
)

app = Dash(__name__, external_scripts=[{'src':"https://ajax.googleapis.com/ajax/libs/jquery/3.6.0/jquery.min.js"}])

app.layout = html.Div([
    dcc.Graph(id="graph-basic-2", figure=fig, clear_on_unhover=True),
    dcc.Tooltip(id="graph-tooltip"),
    dcc.Store(id='graph-basic-2-data', data=df.to_dict('records'))
])


app.clientside_callback(
    """
    function showHover(hv, data) {
        if (hv) {
            //demo only shows the first point, but other points may also be available
            pt = hv["points"][0]
            bbox = pt["bbox"]
            num = pt["pointNumber"]
        
            df_row = data[num]
            img_src = df_row['IMG_URL']
            name = df_row['NAME']
            form = df_row['FORM']
            desc = df_row['DESC']
            if (desc.length > 300) {
                desc = desc.substring(0,100) + '...'
            }
            
            img = jQuery(
                "<img>", {
                    src: img_src,
                    style: "width:100%"
                }
            )
            
            ttl = jQuery("<h2>", {text: name})
            form = jQuery("<p>", {text: form})
            desc = jQuery("<p>", {text: desc})
            
            newDiv = jQuery("<div>", {
                style: 'width:200px;white-space:normal'
            })
            
            $(newDiv).append(img)
            $(newDiv).append(ttl)
            $(newDiv).append(form)
            $(newDiv).append(desc)
            
            $('#graph-tooltip').empty()
            
            $('#graph-tooltip').append($(newDiv))
        
            return [true, bbox]
        }
        return [false, dash_clientside.no_update]
    }
    """,
    Output("graph-tooltip", "show"),
    Output("graph-tooltip", "bbox"),
    Input("graph-basic-2", "hoverData"),
    State('graph-basic-2-data', 'data')
)


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

Now, the slowest part it the loading of the images. Which will be an issue due to loading times. Shrinking the image or caching on the browser side (service worker) can improve this.

3 Likes

Thank you so much for taking the time; huge speed improvement. I’ve downgraded the images with pillow to 150px and they still take a split second longer to load, but it was really the tooltip/Div loading lag that was the core issue… here’s the speed improvement:

plotly_fixes

Appreciate you.

2 Likes