Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Hover format or skip #2377

Merged
merged 23 commits into from
Apr 21, 2020
Merged
Show file tree
Hide file tree
Changes from 22 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
53 changes: 49 additions & 4 deletions doc/python/hover-text-and-formatting.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand All @@ -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
nicolaskruchten marked this conversation as resolved.
Show resolved Hide resolved

`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. <br>
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

Expand Down Expand Up @@ -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} <br>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.
Expand Down
61 changes: 59 additions & 2 deletions packages/python/plotly/plotly/express/_core.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down Expand Up @@ -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)
nicolaskruchten marked this conversation as resolved.
Show resolved Hide resolved
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 + "<br>".join(hover_lines)
trace_patch["hovertemplate"] += "<extra></extra>"
return trace_patch, fit_results
Expand Down Expand Up @@ -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.
Expand All @@ -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])
)
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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 "
Expand Down Expand Up @@ -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)

Expand Down
11 changes: 9 additions & 2 deletions packages/python/plotly/plotly/express/_doc.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)",
nicolaskruchten marked this conversation as resolved.
Show resolved Hide resolved
"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=[
Expand Down
Original file line number Diff line number Diff line change
@@ -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}<extra></extra>"


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}<br>y=%{y}<br>comment=%{customdata[0]}<extra></extra>"
)
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}<br>y=%{y}<br>comment=%{customdata[0]}<extra></extra>"
)
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}<br>y=%{y}<br>comment=%{customdata[0]:.1f}<extra></extra>"
)


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])},
nicolaskruchten marked this conversation as resolved.
Show resolved Hide resolved
)