Skip to content

Commit 5b0e8d3

Browse files
Hover format or skip (#2377)
* added statsmodels to dependencies for CI * hover can be a dict for skipping or formatting hover * Revert "added statsmodels to dependencies for CI" This reverts commit 7cf2ad0. * ordereddict for py2 * delimiter * ordereddict * tuple possible in hover_data dict * comment * py2 * black * debug * debug * doc, more tests * address review comments * short-circuit instead of bypass flag * be more permissive when accepting data in hover dict * remove print * update tutorial * add docstring to helper function * changelog Co-authored-by: Nicolas Kruchten <[email protected]>
1 parent 7fdba33 commit 5b0e8d3

File tree

5 files changed

+222
-8
lines changed

5 files changed

+222
-8
lines changed

Diff for: CHANGELOG.md

+8
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,14 @@
22
All notable changes to this project will be documented in this file.
33
This project adheres to [Semantic Versioning](http://semver.org/).
44

5+
## Dev version
6+
7+
### Added
8+
9+
- The `hover_data` parameter of `px` functions can now be a dictionary. This
10+
makes it possible to skip hover information for some arguments or to change
11+
the formatting of hover informatiom [#2377](https://github.com/plotly/plotly.py/pull/2377).
12+
513
## [4.6] - 2020-03-31
614

715
### Updated

Diff for: doc/python/hover-text-and-formatting.md

+49-4
Original file line numberDiff line numberDiff line change
@@ -123,7 +123,7 @@ fig.show()
123123

124124
### Customizing Hover text with Plotly Express
125125

126-
Plotly Express functions automatically add all the data being plotted (x, y, color etc) to the hover label. Many Plotly Express functions also support configurable hover text. The `hover_data` argument accepts a list of column names to be added to the hover tooltip. The `hover_name` property controls which column is displayed in bold as the tooltip title.
126+
Plotly Express functions automatically add all the data being plotted (x, y, color etc) to the hover label. Many Plotly Express functions also support configurable hover text. The `hover_data` argument accepts a list of column names to be added to the hover tooltip, or a dictionary for advanced formatting (see the next section). The `hover_name` property controls which column is displayed in bold as the tooltip title.
127127

128128
Here is an example that creates a scatter plot using Plotly Express with custom hover data and a custom hover name.
129129

@@ -138,15 +138,42 @@ fig = px.scatter(df_2007, x="gdpPercap", y="lifeExp", log_x=True,
138138
fig.show()
139139
```
140140

141+
### Disabling or customizing hover of columns in plotly express
142+
143+
`hover_data` can also be a dictionary. Its keys are existing columns of the `dataframe` argument, or new labels. For an existing column, the values can be
144+
* `False` to remove the column from the hover data (for example, if one wishes to remove the column of the `x` argument)
145+
* `True` to add a different column, with default formatting
146+
* a formatting string starting with `:` for numbers [d3-format's syntax](https://github.com/d3/d3-3.x-api-reference/blob/master/Formatting.md#d3_forma), and `|` for dates in [d3-time-format's syntax](https://github.com/d3/d3-3.x-api-reference/blob/master/Time-Formatting.md#format), for example `:.3f`, `|%a`.
147+
148+
It is also possible to pass new data as values of the `hover_data` dict, either as list-like data, or inside a tuple, which first element is one of the possible values described above for existing columns, and the second element correspond to the list-like data, for example `(True, [1, 2, 3])` or `(':.1f', [1.54, 2.345])`.
149+
150+
These different cases are illustrated in the following example.
151+
152+
```python
153+
import plotly.express as px
154+
import numpy as np
155+
df = px.data.iris()
156+
fig = px.scatter(df, x='petal_length', y='sepal_length', facet_col='species', color='species',
157+
hover_data={'species':False, # remove species from hover data
158+
'sepal_length':':.2f', # customize hover for column of y attribute
159+
'petal_width':True, # add other column, default formatting
160+
'sepal_width':':.2f', # add other column, customized formatting
161+
# data not in dataframe, default formatting
162+
'suppl_1': np.random.random(len(df)),
163+
# data not in dataframe, customized formatting
164+
'suppl_2': (':.3f', np.random.random(len(df)))
165+
})
166+
fig.update_layout(height=300)
167+
fig.show()
168+
```
169+
141170
### Customizing hover text with a hovertemplate
142171

143-
To customize the tooltip on your graph you can use [hovertemplate](https://plotly.com/python/reference/#pie-hovertemplate), which is a template string used for rendering the information that appear on hoverbox.
172+
To customize the tooltip on your graph you can use the [hovertemplate](https://plotly.com/python/reference/#pie-hovertemplate) attribute of `graph_objects` tracces, which is a template string used for rendering the information that appear on hoverbox.
144173
This template string can include `variables` in %{variable} format, `numbers` in [d3-format's syntax](https://github.com/d3/d3-3.x-api-reference/blob/master/Formatting.md#d3_forma), and `date` in [d3-time-format's syntax](https://github.com/d3/d3-3.x-api-reference/blob/master/Time-Formatting.md#format).
145174
Hovertemplate customize the tooltip text vs. [texttemplate](https://plotly.com/python/reference/#pie-texttemplate) which customizes the text that appears on your chart. <br>
146175
Set the horizontal alignment of the text within tooltip with [hoverlabel.align](https://plotly.com/python/reference/#layout-hoverlabel-align).
147176

148-
Plotly Express automatically sets the `hovertemplate`, but you can set it manually when using `graph_objects`.
149-
150177
```python
151178
import plotly.graph_objects as go
152179

@@ -187,6 +214,24 @@ fig = go.Figure(go.Pie(
187214
fig.show()
188215
```
189216

217+
### Modifying the hovertemplate of a plotly express figure
218+
219+
`plotly.express` automatically sets the hovertemplate but you can modify it using the `update_traces` method of the generated figure. It helps to print the hovertemplate generated by `plotly.express` in order to be able to modify it. One can also revert to the default hover information of traces by setting the hovertemplate to `None`.
220+
221+
```python
222+
import plotly.express as px
223+
224+
df_2007 = px.data.gapminder().query("year==2007")
225+
226+
fig = px.scatter(df_2007, x="gdpPercap", y="lifeExp", log_x=True, color='continent'
227+
)
228+
print("plotly express hovertemplate:", fig.data[0].hovertemplate)
229+
fig.update_traces(hovertemplate='GDP: %{x} <br>Life Expectany: %{y}') #
230+
fig.update_traces(hovertemplate=None, selector={'name':'Europe'}) # revert to default hover
231+
print("user_defined hovertemplate:", fig.data[0].hovertemplate)
232+
fig.show()
233+
```
234+
190235
### Advanced Hover Template
191236

192237
The following example shows how to format hover template. [Here](https://plotly.com/python/v3/hover-text-and-formatting/#dash-example) is an example to see how to format hovertemplate in Dash.

Diff for: packages/python/plotly/plotly/express/_core.py

+59-2
Original file line numberDiff line numberDiff line change
@@ -290,7 +290,10 @@ def make_trace_kwargs(args, trace_spec, trace_data, mapping_labels, sizeref):
290290
go.Histogram2d,
291291
go.Histogram2dContour,
292292
]:
293+
hover_is_dict = isinstance(attr_value, dict)
293294
for col in attr_value:
295+
if hover_is_dict and not attr_value[col]:
296+
continue
294297
try:
295298
position = args["custom_data"].index(col)
296299
except (ValueError, AttributeError, KeyError):
@@ -387,7 +390,20 @@ def make_trace_kwargs(args, trace_spec, trace_data, mapping_labels, sizeref):
387390
go.Parcoords,
388391
go.Parcats,
389392
]:
390-
hover_lines = [k + "=" + v for k, v in mapping_labels.items()]
393+
# Modify mapping_labels according to hover_data keys
394+
# if hover_data is a dict
395+
mapping_labels_copy = OrderedDict(mapping_labels)
396+
if args["hover_data"] and isinstance(args["hover_data"], dict):
397+
for k, v in mapping_labels.items():
398+
if k in args["hover_data"]:
399+
if args["hover_data"][k][0]:
400+
if isinstance(args["hover_data"][k][0], str):
401+
mapping_labels_copy[k] = v.replace(
402+
"}", "%s}" % args["hover_data"][k][0]
403+
)
404+
else:
405+
_ = mapping_labels_copy.pop(k)
406+
hover_lines = [k + "=" + v for k, v in mapping_labels_copy.items()]
391407
trace_patch["hovertemplate"] = hover_header + "<br>".join(hover_lines)
392408
trace_patch["hovertemplate"] += "<extra></extra>"
393409
return trace_patch, fit_results
@@ -869,6 +885,22 @@ def _get_reserved_col_names(args, attrables, array_attrables):
869885
return reserved_names
870886

871887

888+
def _isinstance_listlike(x):
889+
"""Returns True if x is an iterable which can be transformed into a pandas Series,
890+
False for the other types of possible values of a `hover_data` dict.
891+
A tuple of length 2 is a special case corresponding to a (format, data) tuple.
892+
"""
893+
if (
894+
isinstance(x, str)
895+
or (isinstance(x, tuple) and len(x) == 2)
896+
or isinstance(x, bool)
897+
or x is None
898+
):
899+
return False
900+
else:
901+
return True
902+
903+
872904
def build_dataframe(args, attrables, array_attrables):
873905
"""
874906
Constructs a dataframe and modifies `args` in-place.
@@ -890,7 +922,7 @@ def build_dataframe(args, attrables, array_attrables):
890922
for field in args:
891923
if field in array_attrables and args[field] is not None:
892924
args[field] = (
893-
dict(args[field])
925+
OrderedDict(args[field])
894926
if isinstance(args[field], dict)
895927
else list(args[field])
896928
)
@@ -919,6 +951,19 @@ def build_dataframe(args, attrables, array_attrables):
919951
else:
920952
df_output[df_input.columns] = df_input[df_input.columns]
921953

954+
# hover_data is a dict
955+
hover_data_is_dict = (
956+
"hover_data" in args
957+
and args["hover_data"]
958+
and isinstance(args["hover_data"], dict)
959+
)
960+
# If dict, convert all values of hover_data to tuples to simplify processing
961+
if hover_data_is_dict:
962+
for k in args["hover_data"]:
963+
if _isinstance_listlike(args["hover_data"][k]):
964+
args["hover_data"][k] = (True, args["hover_data"][k])
965+
if not isinstance(args["hover_data"][k], tuple):
966+
args["hover_data"][k] = (args["hover_data"][k], None)
922967
# Loop over possible arguments
923968
for field_name in attrables:
924969
# Massaging variables
@@ -954,6 +999,16 @@ def build_dataframe(args, attrables, array_attrables):
954999
if isinstance(argument, str) or isinstance(
9551000
argument, int
9561001
): # just a column name given as str or int
1002+
1003+
if (
1004+
field_name == "hover_data"
1005+
and hover_data_is_dict
1006+
and args["hover_data"][str(argument)][1] is not None
1007+
):
1008+
col_name = str(argument)
1009+
df_output[col_name] = args["hover_data"][col_name][1]
1010+
continue
1011+
9571012
if not df_provided:
9581013
raise ValueError(
9591014
"String or int arguments are only possible when a "
@@ -1029,6 +1084,8 @@ def build_dataframe(args, attrables, array_attrables):
10291084
# Finally, update argument with column name now that column exists
10301085
if field_name not in array_attrables:
10311086
args[field_name] = str(col_name)
1087+
elif isinstance(args[field_name], dict):
1088+
pass
10321089
else:
10331090
args[field_name][i] = str(col_name)
10341091

Diff for: packages/python/plotly/plotly/express/_doc.py

+9-2
Original file line numberDiff line numberDiff line change
@@ -180,8 +180,15 @@
180180
"Values from this column or array_like appear in bold in the hover tooltip.",
181181
],
182182
hover_data=[
183-
colref_list_type,
184-
colref_list_desc,
183+
"list of str or int, or Series or array-like, or dict",
184+
"Either a list of names of columns in `data_frame`, or pandas Series,",
185+
"or array_like objects",
186+
"or a dict with column names as keys, with values True (for default formatting)",
187+
"False (in order to remove this column from hover information),",
188+
"or a formatting string, for example ':.3f' or '|%a'",
189+
"or list-like data to appear in the hover tooltip",
190+
"or tuples with a bool or formatting string as first element,",
191+
"and list-like data to appear in hover as second element",
185192
"Values from these columns appear as extra data in the hover tooltip.",
186193
],
187194
custom_data=[
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
import plotly.express as px
2+
import numpy as np
3+
import pandas as pd
4+
import pytest
5+
import plotly.graph_objects as go
6+
from collections import OrderedDict # an OrderedDict is needed for Python 2
7+
8+
9+
def test_skip_hover():
10+
df = px.data.iris()
11+
fig = px.scatter(
12+
df,
13+
x="petal_length",
14+
y="petal_width",
15+
size="species_id",
16+
hover_data={"petal_length": None, "petal_width": None},
17+
)
18+
assert fig.data[0].hovertemplate == "species_id=%{marker.size}<extra></extra>"
19+
20+
21+
def test_composite_hover():
22+
df = px.data.tips()
23+
hover_dict = OrderedDict(
24+
{"day": False, "time": False, "sex": True, "total_bill": ":.1f"}
25+
)
26+
fig = px.scatter(
27+
df,
28+
x="tip",
29+
y="total_bill",
30+
color="day",
31+
facet_row="time",
32+
hover_data=hover_dict,
33+
)
34+
for el in ["tip", "total_bill", "sex"]:
35+
assert el in fig.data[0].hovertemplate
36+
for el in ["day", "time"]:
37+
assert el not in fig.data[0].hovertemplate
38+
assert ":.1f" in fig.data[0].hovertemplate
39+
40+
41+
def test_newdatain_hover_data():
42+
hover_dicts = [
43+
{"comment": ["a", "b", "c"]},
44+
{"comment": (1.234, 45.3455, 5666.234)},
45+
{"comment": [1.234, 45.3455, 5666.234]},
46+
{"comment": np.array([1.234, 45.3455, 5666.234])},
47+
{"comment": pd.Series([1.234, 45.3455, 5666.234])},
48+
]
49+
for hover_dict in hover_dicts:
50+
fig = px.scatter(x=[1, 2, 3], y=[3, 4, 5], hover_data=hover_dict)
51+
assert (
52+
fig.data[0].hovertemplate
53+
== "x=%{x}<br>y=%{y}<br>comment=%{customdata[0]}<extra></extra>"
54+
)
55+
fig = px.scatter(
56+
x=[1, 2, 3], y=[3, 4, 5], hover_data={"comment": (True, ["a", "b", "c"])}
57+
)
58+
assert (
59+
fig.data[0].hovertemplate
60+
== "x=%{x}<br>y=%{y}<br>comment=%{customdata[0]}<extra></extra>"
61+
)
62+
hover_dicts = [
63+
{"comment": (":.1f", (1.234, 45.3455, 5666.234))},
64+
{"comment": (":.1f", [1.234, 45.3455, 5666.234])},
65+
{"comment": (":.1f", np.array([1.234, 45.3455, 5666.234]))},
66+
{"comment": (":.1f", pd.Series([1.234, 45.3455, 5666.234]))},
67+
]
68+
for hover_dict in hover_dicts:
69+
fig = px.scatter(x=[1, 2, 3], y=[3, 4, 5], hover_data=hover_dict,)
70+
assert (
71+
fig.data[0].hovertemplate
72+
== "x=%{x}<br>y=%{y}<br>comment=%{customdata[0]:.1f}<extra></extra>"
73+
)
74+
75+
76+
def test_fail_wrong_column():
77+
with pytest.raises(ValueError):
78+
fig = px.scatter(
79+
{"a": [1, 2], "b": [3, 4], "c": [2, 1]},
80+
x="a",
81+
y="b",
82+
hover_data={"d": True},
83+
)
84+
with pytest.raises(ValueError):
85+
fig = px.scatter(
86+
{"a": [1, 2], "b": [3, 4], "c": [2, 1]},
87+
x="a",
88+
y="b",
89+
hover_data={"d": ":.1f"},
90+
)
91+
with pytest.raises(ValueError):
92+
fig = px.scatter(
93+
{"a": [1, 2], "b": [3, 4], "c": [2, 1]},
94+
x="a",
95+
y="b",
96+
hover_data={"d": (True, [3, 4, 5])},
97+
)

0 commit comments

Comments
 (0)