plotly interactive tooltip / hover text / popup

Question:

Tooltips of a figure are only displayed while hovering over the data point:
https://plotly.com/python/hover-text-and-formatting

I’d like to have an easy way to customize the duration the tooltip is displayed after hovering over it or possibly display the tooltip permanently when clicking the data point.

This will allow me to include clickable links in the tooltip.

For data tables you can customize the tooltip display duration, but I don’t see a similar option for figures:
https://dash.plotly.com/datatable/tooltips

I think you can add your own tooltips via the event system or maybe change the css style of the resulting HTML somehow, but that seems to be overkill. I’d still accept an answer with a working example.

Asked By: phobic

||

Answers:

This is a css solution that disables normal tooltips and instead uses a callback display the information in a div element. When the mouse moves away from the node we use css to transition the div to be invisible in a few seconds.

Not a great solution by any means, but it works.

I also tried a solution based on the css attribute animation which works, but is a bit more complex.

I tried to change the div’s class name instead of the style and stop the transition when the mouse hovers over the div by adding a css :not:hover condition to the transition, but did not get it to work completely.

from dash import Dash, dcc, html, Input, Output, no_update
import plotly.express as px 

df = px.data.tips()

fig = px.scatter(df, x="total_bill", y="tip")

# 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)

# Hover distance defaults to 20 and means call hover event if mouse pointer is within x units close to the node.
fig.update_layout(hoverdistance=5)

app = Dash(__name__)

# Clear on unhover means the hover callback is called with hoverDate=None when moving away from the point.log_queue
app.layout = html.Div(
    [
        dcc.Graph(id='graph', figure=fig, clear_on_unhover=True),
        html.Div(id='graph-tooltip'),
    ],
)

# Store previous style globally.
previous_style = None


@app.callback(
    Output('graph-tooltip', 'style'),
    Output('graph-tooltip', 'children'),
    Input('graph', 'hoverData'),
    prevent_initial_call=True,
)
def display_hover(hoverData):
    '''When hovering over a node, show a HTML div element with some Node info and two links. When moving the mouse away from the node the div fades out slowly.'''
    global previous_style
    if hoverData is None:
        # When the mouse moves a way from the node hoverData is None and we hide the element with opacity=0.
        # The transition to hide the element happens over 4 seconds as defined below via the css attribute transition.
        previous_style['opacity'] = 0
        return previous_style, no_update

    # Get attribute NAME from dataframe to display it.
    # And get the x/y coordinates of the current node the mouse is hovering over.
    pt = hoverData['points'][0]
    box = pt['bbox']
    num = pt['pointNumber']

    df_row = df.iloc[num]
    df_value = df_row['time']

    children = [
        html.P(f'{df_value}'),
        html.A('Link to external site 1', href='https://plot.ly', target='_blank'),
        html.Br(),
        html.A('Link to external site 2', href='https://plot.ly', target='_blank'),
    ]
    previous_style = {
        'position': 'absolute',
        'left': f'{box["x1"] + 20}px',  # Display the div next to the node.
        'top': f'{box["y1"] + 20}px',
        'background-color': 'rgba(100, 100, 100, 0.8)',
        'transition': 'opacity 4s  ease-out',  # Fade in / Fade out the div element gradually.

    }
    return previous_style, children


app.run_server()
Answered By: phobic

Here is a more complex solution that prevents the tooltip to fade out while you hover over it. For that you need an external css file with a css animation.

See https://stackoverflow.com/a/75277310/2474025 for a more simple solution.

assets/my.css:


.hideclass:not(:hover) {
  animation: hideanimation 5s ease-in;
  animation-fill-mode: forwards;
}
.showclass {
  animation: showanimation 5s ease-out;
  animation-fill-mode: forwards;
}


@keyframes showanimation {
  0% {
    opacity: 0;
  }
  25% {
    opacity: 0.4;
  }
  50% {
    opacity: 0.7;
  }

  75% {
    opacity: 0.8;
  }
  100% {
    opacity: 1;
  }
}


@keyframes hideanimation {
  0% {
    opacity: 1;
  }
  25% {
    opacity: 0.5;
  }
  50% {
    opacity: 0;
  }

  75% {
    opacity: 0;
    max-width: 0;
    max-height: 0;
    overflow: hidden;
  }
  100% {
    opacity: 0;
    max-width: 0;
    max-height: 0;
    overflow: hidden;
  }
}

Python notebook with the following code needs to be in the same directory as the assets folder:


from dash import Dash, dcc, html, Input, Output, no_update
import plotly.express as px

df = px.data.tips()

fig = px.scatter(df, x='total_bill', y='tip')

# 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)

# Hover distance defaults to 20 and means call hover event if mouse pointer is within x units close to the node.
fig.update_layout(hoverdistance=5)

app = Dash(__name__)

app.layout = html.Div(
    [
        html.Link(rel='stylesheet', href='/assets/my.css'),
        # clear on unhover means the hover callback is called with hoverDate=None when moving away from the point.log_queue
        dcc.Graph(
            id='graph-basic-2',
            figure=fig,
            clear_on_unhover=True,
        ),
        html.Div(
            id='graph-tooltip',
            className='dash-bootstrap',
        ),
    ],
    className='dash-bootrstrap',
)

previous_style = None


@app.callback(
    Output('graph-tooltip', 'style'),
    Output('graph-tooltip', 'className'),
    Output('graph-tooltip', 'children'),
    Input('graph-basic-2', 'hoverData'),
    prevent_initial_call=True,
)
def display_hover(hoverData):
    global previous_style
    if hoverData is None:
        return no_update, 'hideclass', no_update

    print('display')

    # demo only shows the first point, but other points may also be available
    pt = hoverData['points'][0]
    bbox = pt['bbox']

    children = [
        html.A('Link to external site 1', href='https://plot.ly', target='_blank'),
        html.Br(),
        html.A('Link to external site 2', href='https://plot.ly', target='_blank'),
    ]
    previous_style = {
        'position': 'absolute',
        'left': f'{bbox["x1"] + 20}px',
        'top': f'{bbox["y1"] + 20}px',
        'background-color': 'rgba(100, 100, 100, 0.8)',
        'padding': '1em',
    }
    return previous_style, 'showclass', children


# if __name__ == '__main__':
app.run_server(
    dev_tools_hot_reload=True,
)
Answered By: phobic
Categories: questions Tags: , ,
Answers are sorted by their score. The answer accepted by the question owner as the best is marked with
at the top-right corner.