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.
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()
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,
)
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.
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()
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,
)