diff --git a/CHANGELOG.md b/CHANGELOG.md
index b2b05a9094f..ab4811baae3 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,6 +2,14 @@
All notable changes to this project will be documented in this file.
This project adheres to [Semantic Versioning](http://semver.org/).
+## Dev version
+
+### Added
+
+- The `hover_data` parameter of `px` functions can now be a dictionary. This
+ makes it possible to skip hover information for some arguments or to change
+ the formatting of hover informatiom [#2377](https://github.com/plotly/plotly.py/pull/2377).
+
## [4.6] - 2020-03-31
### Updated
diff --git a/doc/python/hover-text-and-formatting.md b/doc/python/hover-text-and-formatting.md
index 9331a4831ee..eb5957cb71a 100644
--- a/doc/python/hover-text-and-formatting.md
+++ b/doc/python/hover-text-and-formatting.md
@@ -123,7 +123,7 @@ fig.show()
### Customizing Hover text with Plotly Express
-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.
+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.
Here is an example that creates a scatter plot using Plotly Express with custom hover data and a custom hover name.
@@ -138,15 +138,42 @@ fig = px.scatter(df_2007, x="gdpPercap", y="lifeExp", log_x=True,
fig.show()
```
+### Disabling or customizing hover of columns in plotly express
+
+`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
+* `False` to remove the column from the hover data (for example, if one wishes to remove the column of the `x` argument)
+* `True` to add a different column, with default formatting
+* 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`.
+
+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])`.
+
+These different cases are illustrated in the following example.
+
+```python
+import plotly.express as px
+import numpy as np
+df = px.data.iris()
+fig = px.scatter(df, x='petal_length', y='sepal_length', facet_col='species', color='species',
+ hover_data={'species':False, # remove species from hover data
+ 'sepal_length':':.2f', # customize hover for column of y attribute
+ 'petal_width':True, # add other column, default formatting
+ 'sepal_width':':.2f', # add other column, customized formatting
+ # data not in dataframe, default formatting
+ 'suppl_1': np.random.random(len(df)),
+ # data not in dataframe, customized formatting
+ 'suppl_2': (':.3f', np.random.random(len(df)))
+ })
+fig.update_layout(height=300)
+fig.show()
+```
+
### Customizing hover text with a hovertemplate
-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.
+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.
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).
Hovertemplate customize the tooltip text vs. [texttemplate](https://plotly.com/python/reference/#pie-texttemplate) which customizes the text that appears on your chart.
Set the horizontal alignment of the text within tooltip with [hoverlabel.align](https://plotly.com/python/reference/#layout-hoverlabel-align).
-Plotly Express automatically sets the `hovertemplate`, but you can set it manually when using `graph_objects`.
-
```python
import plotly.graph_objects as go
@@ -187,6 +214,24 @@ fig = go.Figure(go.Pie(
fig.show()
```
+### Modifying the hovertemplate of a plotly express figure
+
+`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`.
+
+```python
+import plotly.express as px
+
+df_2007 = px.data.gapminder().query("year==2007")
+
+fig = px.scatter(df_2007, x="gdpPercap", y="lifeExp", log_x=True, color='continent'
+ )
+print("plotly express hovertemplate:", fig.data[0].hovertemplate)
+fig.update_traces(hovertemplate='GDP: %{x}
Life Expectany: %{y}') #
+fig.update_traces(hovertemplate=None, selector={'name':'Europe'}) # revert to default hover
+print("user_defined hovertemplate:", fig.data[0].hovertemplate)
+fig.show()
+```
+
### Advanced Hover Template
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 --git a/packages/python/plotly/plotly/express/_core.py b/packages/python/plotly/plotly/express/_core.py
index 613920d05fa..89b4ffce5e3 100644
--- a/packages/python/plotly/plotly/express/_core.py
+++ b/packages/python/plotly/plotly/express/_core.py
@@ -290,7 +290,10 @@ def make_trace_kwargs(args, trace_spec, trace_data, mapping_labels, sizeref):
go.Histogram2d,
go.Histogram2dContour,
]:
+ hover_is_dict = isinstance(attr_value, dict)
for col in attr_value:
+ if hover_is_dict and not attr_value[col]:
+ continue
try:
position = args["custom_data"].index(col)
except (ValueError, AttributeError, KeyError):
@@ -387,7 +390,20 @@ def make_trace_kwargs(args, trace_spec, trace_data, mapping_labels, sizeref):
go.Parcoords,
go.Parcats,
]:
- hover_lines = [k + "=" + v for k, v in mapping_labels.items()]
+ # Modify mapping_labels according to hover_data keys
+ # if hover_data is a dict
+ mapping_labels_copy = OrderedDict(mapping_labels)
+ if args["hover_data"] and isinstance(args["hover_data"], dict):
+ for k, v in mapping_labels.items():
+ if k in args["hover_data"]:
+ if args["hover_data"][k][0]:
+ if isinstance(args["hover_data"][k][0], str):
+ mapping_labels_copy[k] = v.replace(
+ "}", "%s}" % args["hover_data"][k][0]
+ )
+ else:
+ _ = mapping_labels_copy.pop(k)
+ hover_lines = [k + "=" + v for k, v in mapping_labels_copy.items()]
trace_patch["hovertemplate"] = hover_header + "
".join(hover_lines)
trace_patch["hovertemplate"] += ""
return trace_patch, fit_results
@@ -869,6 +885,22 @@ def _get_reserved_col_names(args, attrables, array_attrables):
return reserved_names
+def _isinstance_listlike(x):
+ """Returns True if x is an iterable which can be transformed into a pandas Series,
+ False for the other types of possible values of a `hover_data` dict.
+ A tuple of length 2 is a special case corresponding to a (format, data) tuple.
+ """
+ if (
+ isinstance(x, str)
+ or (isinstance(x, tuple) and len(x) == 2)
+ or isinstance(x, bool)
+ or x is None
+ ):
+ return False
+ else:
+ return True
+
+
def build_dataframe(args, attrables, array_attrables):
"""
Constructs a dataframe and modifies `args` in-place.
@@ -890,7 +922,7 @@ def build_dataframe(args, attrables, array_attrables):
for field in args:
if field in array_attrables and args[field] is not None:
args[field] = (
- dict(args[field])
+ OrderedDict(args[field])
if isinstance(args[field], dict)
else list(args[field])
)
@@ -919,6 +951,19 @@ def build_dataframe(args, attrables, array_attrables):
else:
df_output[df_input.columns] = df_input[df_input.columns]
+ # hover_data is a dict
+ hover_data_is_dict = (
+ "hover_data" in args
+ and args["hover_data"]
+ and isinstance(args["hover_data"], dict)
+ )
+ # If dict, convert all values of hover_data to tuples to simplify processing
+ if hover_data_is_dict:
+ for k in args["hover_data"]:
+ if _isinstance_listlike(args["hover_data"][k]):
+ args["hover_data"][k] = (True, args["hover_data"][k])
+ if not isinstance(args["hover_data"][k], tuple):
+ args["hover_data"][k] = (args["hover_data"][k], None)
# Loop over possible arguments
for field_name in attrables:
# Massaging variables
@@ -954,6 +999,16 @@ def build_dataframe(args, attrables, array_attrables):
if isinstance(argument, str) or isinstance(
argument, int
): # just a column name given as str or int
+
+ if (
+ field_name == "hover_data"
+ and hover_data_is_dict
+ and args["hover_data"][str(argument)][1] is not None
+ ):
+ col_name = str(argument)
+ df_output[col_name] = args["hover_data"][col_name][1]
+ continue
+
if not df_provided:
raise ValueError(
"String or int arguments are only possible when a "
@@ -1029,6 +1084,8 @@ def build_dataframe(args, attrables, array_attrables):
# Finally, update argument with column name now that column exists
if field_name not in array_attrables:
args[field_name] = str(col_name)
+ elif isinstance(args[field_name], dict):
+ pass
else:
args[field_name][i] = str(col_name)
diff --git a/packages/python/plotly/plotly/express/_doc.py b/packages/python/plotly/plotly/express/_doc.py
index 4f17af74756..05a5b214cad 100644
--- a/packages/python/plotly/plotly/express/_doc.py
+++ b/packages/python/plotly/plotly/express/_doc.py
@@ -180,8 +180,15 @@
"Values from this column or array_like appear in bold in the hover tooltip.",
],
hover_data=[
- colref_list_type,
- colref_list_desc,
+ "list of str or int, or Series or array-like, or dict",
+ "Either a list of names of columns in `data_frame`, or pandas Series,",
+ "or array_like objects",
+ "or a dict with column names as keys, with values True (for default formatting)",
+ "False (in order to remove this column from hover information),",
+ "or a formatting string, for example ':.3f' or '|%a'",
+ "or list-like data to appear in the hover tooltip",
+ "or tuples with a bool or formatting string as first element,",
+ "and list-like data to appear in hover as second element",
"Values from these columns appear as extra data in the hover tooltip.",
],
custom_data=[
diff --git a/packages/python/plotly/plotly/tests/test_core/test_px/test_px_hover.py b/packages/python/plotly/plotly/tests/test_core/test_px/test_px_hover.py
new file mode 100644
index 00000000000..509f48d1991
--- /dev/null
+++ b/packages/python/plotly/plotly/tests/test_core/test_px/test_px_hover.py
@@ -0,0 +1,97 @@
+import plotly.express as px
+import numpy as np
+import pandas as pd
+import pytest
+import plotly.graph_objects as go
+from collections import OrderedDict # an OrderedDict is needed for Python 2
+
+
+def test_skip_hover():
+ df = px.data.iris()
+ fig = px.scatter(
+ df,
+ x="petal_length",
+ y="petal_width",
+ size="species_id",
+ hover_data={"petal_length": None, "petal_width": None},
+ )
+ assert fig.data[0].hovertemplate == "species_id=%{marker.size}"
+
+
+def test_composite_hover():
+ df = px.data.tips()
+ hover_dict = OrderedDict(
+ {"day": False, "time": False, "sex": True, "total_bill": ":.1f"}
+ )
+ fig = px.scatter(
+ df,
+ x="tip",
+ y="total_bill",
+ color="day",
+ facet_row="time",
+ hover_data=hover_dict,
+ )
+ for el in ["tip", "total_bill", "sex"]:
+ assert el in fig.data[0].hovertemplate
+ for el in ["day", "time"]:
+ assert el not in fig.data[0].hovertemplate
+ assert ":.1f" in fig.data[0].hovertemplate
+
+
+def test_newdatain_hover_data():
+ hover_dicts = [
+ {"comment": ["a", "b", "c"]},
+ {"comment": (1.234, 45.3455, 5666.234)},
+ {"comment": [1.234, 45.3455, 5666.234]},
+ {"comment": np.array([1.234, 45.3455, 5666.234])},
+ {"comment": pd.Series([1.234, 45.3455, 5666.234])},
+ ]
+ for hover_dict in hover_dicts:
+ fig = px.scatter(x=[1, 2, 3], y=[3, 4, 5], hover_data=hover_dict)
+ assert (
+ fig.data[0].hovertemplate
+ == "x=%{x}
y=%{y}
comment=%{customdata[0]}"
+ )
+ fig = px.scatter(
+ x=[1, 2, 3], y=[3, 4, 5], hover_data={"comment": (True, ["a", "b", "c"])}
+ )
+ assert (
+ fig.data[0].hovertemplate
+ == "x=%{x}
y=%{y}
comment=%{customdata[0]}"
+ )
+ hover_dicts = [
+ {"comment": (":.1f", (1.234, 45.3455, 5666.234))},
+ {"comment": (":.1f", [1.234, 45.3455, 5666.234])},
+ {"comment": (":.1f", np.array([1.234, 45.3455, 5666.234]))},
+ {"comment": (":.1f", pd.Series([1.234, 45.3455, 5666.234]))},
+ ]
+ for hover_dict in hover_dicts:
+ fig = px.scatter(x=[1, 2, 3], y=[3, 4, 5], hover_data=hover_dict,)
+ assert (
+ fig.data[0].hovertemplate
+ == "x=%{x}
y=%{y}
comment=%{customdata[0]:.1f}"
+ )
+
+
+def test_fail_wrong_column():
+ with pytest.raises(ValueError):
+ fig = px.scatter(
+ {"a": [1, 2], "b": [3, 4], "c": [2, 1]},
+ x="a",
+ y="b",
+ hover_data={"d": True},
+ )
+ with pytest.raises(ValueError):
+ fig = px.scatter(
+ {"a": [1, 2], "b": [3, 4], "c": [2, 1]},
+ x="a",
+ y="b",
+ hover_data={"d": ":.1f"},
+ )
+ with pytest.raises(ValueError):
+ fig = px.scatter(
+ {"a": [1, 2], "b": [3, 4], "c": [2, 1]},
+ x="a",
+ y="b",
+ hover_data={"d": (True, [3, 4, 5])},
+ )