Skip to content

Commit 51bad7f

Browse files
committed
Merge remote-tracking branch 'origin/master' into figure_level_validate
2 parents 9990cd1 + e778c6b commit 51bad7f

File tree

10 files changed

+270
-28
lines changed

10 files changed

+270
-28
lines changed

Diff for: .circleci/create_conda_optional_env.sh

+1-1
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ if [ ! -d $HOME/miniconda/envs/circle_optional ]; then
1616
# Create environment
1717
# PYTHON_VERSION=2.7 or 3.5
1818
$HOME/miniconda/bin/conda create -n circle_optional --yes python=$PYTHON_VERSION \
19-
requests nbformat six retrying psutil pandas decorator pytest mock nose poppler xarray scikit-image ipython jupyter ipykernel ipywidgets
19+
requests nbformat six retrying psutil pandas decorator pytest mock nose poppler xarray scikit-image ipython jupyter ipykernel ipywidgets statsmodels
2020

2121
# Install orca into environment
2222
$HOME/miniconda/bin/conda install --yes -n circle_optional -c plotly plotly-orca==1.3.1

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: doc/python/imshow.md

+2-2
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ jupyter:
66
extension: .md
77
format_name: markdown
88
format_version: '1.2'
9-
jupytext_version: 1.3.1
9+
jupytext_version: 1.4.2
1010
kernelspec:
1111
display_name: Python 3
1212
language: python
@@ -20,7 +20,7 @@ jupyter:
2020
name: python
2121
nbconvert_exporter: python
2222
pygments_lexer: ipython3
23-
version: 3.6.8
23+
version: 3.7.7
2424
plotly:
2525
description: How to display image data in Python with Plotly.
2626
display_as: scientific

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

+84-16
Original file line numberDiff line numberDiff line change
@@ -241,18 +241,25 @@ def make_trace_kwargs(args, trace_spec, trace_data, mapping_labels, sizeref):
241241
sorted_trace_data = trace_data.sort_values(by=args["x"])
242242
y = sorted_trace_data[args["y"]]
243243
x = sorted_trace_data[args["x"]]
244-
trace_patch["x"] = x
245244

246245
if x.dtype.type == np.datetime64:
247246
x = x.astype(int) / 10 ** 9 # convert to unix epoch seconds
248247

249248
if attr_value == "lowess":
250-
trendline = sm.nonparametric.lowess(y, x)
249+
# missing ='drop' is the default value for lowess but not for OLS (None)
250+
# we force it here in case statsmodels change their defaults
251+
trendline = sm.nonparametric.lowess(y, x, missing="drop")
252+
trace_patch["x"] = trendline[:, 0]
251253
trace_patch["y"] = trendline[:, 1]
252254
hover_header = "<b>LOWESS trendline</b><br><br>"
253255
elif attr_value == "ols":
254-
fit_results = sm.OLS(y.values, sm.add_constant(x.values)).fit()
256+
fit_results = sm.OLS(
257+
y.values, sm.add_constant(x.values), missing="drop"
258+
).fit()
255259
trace_patch["y"] = fit_results.predict()
260+
trace_patch["x"] = x[
261+
np.logical_not(np.logical_or(np.isnan(y), np.isnan(x)))
262+
]
256263
hover_header = "<b>OLS trendline</b><br>"
257264
hover_header += "%s = %g * %s + %g<br>" % (
258265
args["y"],
@@ -290,7 +297,10 @@ def make_trace_kwargs(args, trace_spec, trace_data, mapping_labels, sizeref):
290297
go.Histogram2d,
291298
go.Histogram2dContour,
292299
]:
300+
hover_is_dict = isinstance(attr_value, dict)
293301
for col in attr_value:
302+
if hover_is_dict and not attr_value[col]:
303+
continue
294304
try:
295305
position = args["custom_data"].index(col)
296306
except (ValueError, AttributeError, KeyError):
@@ -387,7 +397,20 @@ def make_trace_kwargs(args, trace_spec, trace_data, mapping_labels, sizeref):
387397
go.Parcoords,
388398
go.Parcats,
389399
]:
390-
hover_lines = [k + "=" + v for k, v in mapping_labels.items()]
400+
# Modify mapping_labels according to hover_data keys
401+
# if hover_data is a dict
402+
mapping_labels_copy = OrderedDict(mapping_labels)
403+
if args["hover_data"] and isinstance(args["hover_data"], dict):
404+
for k, v in mapping_labels.items():
405+
if k in args["hover_data"]:
406+
if args["hover_data"][k][0]:
407+
if isinstance(args["hover_data"][k][0], str):
408+
mapping_labels_copy[k] = v.replace(
409+
"}", "%s}" % args["hover_data"][k][0]
410+
)
411+
else:
412+
_ = mapping_labels_copy.pop(k)
413+
hover_lines = [k + "=" + v for k, v in mapping_labels_copy.items()]
391414
trace_patch["hovertemplate"] = hover_header + "<br>".join(hover_lines)
392415
trace_patch["hovertemplate"] += "<extra></extra>"
393416
return trace_patch, fit_results
@@ -535,9 +558,9 @@ def configure_cartesian_axes(args, fig, orders):
535558
def configure_ternary_axes(args, fig, orders):
536559
fig.update_layout(
537560
ternary=dict(
538-
aaxis=dict(title=get_label(args, args["a"])),
539-
baxis=dict(title=get_label(args, args["b"])),
540-
caxis=dict(title=get_label(args, args["c"])),
561+
aaxis=dict(title_text=get_label(args, args["a"])),
562+
baxis=dict(title_text=get_label(args, args["b"])),
563+
caxis=dict(title_text=get_label(args, args["c"])),
541564
)
542565
)
543566

@@ -572,9 +595,9 @@ def configure_polar_axes(args, fig, orders):
572595
def configure_3d_axes(args, fig, orders):
573596
layout = dict(
574597
scene=dict(
575-
xaxis=dict(title=get_label(args, args["x"])),
576-
yaxis=dict(title=get_label(args, args["y"])),
577-
zaxis=dict(title=get_label(args, args["z"])),
598+
xaxis=dict(title_text=get_label(args, args["x"])),
599+
yaxis=dict(title_text=get_label(args, args["y"])),
600+
zaxis=dict(title_text=get_label(args, args["z"])),
578601
)
579602
)
580603

@@ -869,6 +892,22 @@ def _get_reserved_col_names(args, attrables, array_attrables):
869892
return reserved_names
870893

871894

895+
def _isinstance_listlike(x):
896+
"""Returns True if x is an iterable which can be transformed into a pandas Series,
897+
False for the other types of possible values of a `hover_data` dict.
898+
A tuple of length 2 is a special case corresponding to a (format, data) tuple.
899+
"""
900+
if (
901+
isinstance(x, str)
902+
or (isinstance(x, tuple) and len(x) == 2)
903+
or isinstance(x, bool)
904+
or x is None
905+
):
906+
return False
907+
else:
908+
return True
909+
910+
872911
def build_dataframe(args, attrables, array_attrables):
873912
"""
874913
Constructs a dataframe and modifies `args` in-place.
@@ -890,7 +929,7 @@ def build_dataframe(args, attrables, array_attrables):
890929
for field in args:
891930
if field in array_attrables and args[field] is not None:
892931
args[field] = (
893-
dict(args[field])
932+
OrderedDict(args[field])
894933
if isinstance(args[field], dict)
895934
else list(args[field])
896935
)
@@ -919,6 +958,19 @@ def build_dataframe(args, attrables, array_attrables):
919958
else:
920959
df_output[df_input.columns] = df_input[df_input.columns]
921960

961+
# hover_data is a dict
962+
hover_data_is_dict = (
963+
"hover_data" in args
964+
and args["hover_data"]
965+
and isinstance(args["hover_data"], dict)
966+
)
967+
# If dict, convert all values of hover_data to tuples to simplify processing
968+
if hover_data_is_dict:
969+
for k in args["hover_data"]:
970+
if _isinstance_listlike(args["hover_data"][k]):
971+
args["hover_data"][k] = (True, args["hover_data"][k])
972+
if not isinstance(args["hover_data"][k], tuple):
973+
args["hover_data"][k] = (args["hover_data"][k], None)
922974
# Loop over possible arguments
923975
for field_name in attrables:
924976
# Massaging variables
@@ -954,6 +1006,16 @@ def build_dataframe(args, attrables, array_attrables):
9541006
if isinstance(argument, str) or isinstance(
9551007
argument, int
9561008
): # just a column name given as str or int
1009+
1010+
if (
1011+
field_name == "hover_data"
1012+
and hover_data_is_dict
1013+
and args["hover_data"][str(argument)][1] is not None
1014+
):
1015+
col_name = str(argument)
1016+
df_output[col_name] = args["hover_data"][col_name][1]
1017+
continue
1018+
9571019
if not df_provided:
9581020
raise ValueError(
9591021
"String or int arguments are only possible when a "
@@ -1029,6 +1091,8 @@ def build_dataframe(args, attrables, array_attrables):
10291091
# Finally, update argument with column name now that column exists
10301092
if field_name not in array_attrables:
10311093
args[field_name] = str(col_name)
1094+
elif isinstance(args[field_name], dict):
1095+
pass
10321096
else:
10331097
args[field_name][i] = str(col_name)
10341098

@@ -1554,15 +1618,19 @@ def make_figure(args, constructor, trace_patch={}, layout_patch={}):
15541618
cmid=args["color_continuous_midpoint"],
15551619
cmin=range_color[0],
15561620
cmax=range_color[1],
1557-
colorbar=dict(title=get_decorated_label(args, args[colorvar], colorvar)),
1621+
colorbar=dict(
1622+
title_text=get_decorated_label(args, args[colorvar], colorvar)
1623+
),
15581624
)
1559-
for v in ["title", "height", "width"]:
1625+
for v in ["height", "width"]:
15601626
if args[v]:
15611627
layout_patch[v] = args[v]
15621628
layout_patch["legend"] = dict(tracegroupgap=0)
15631629
if trace_name_labels:
1564-
layout_patch["legend"]["title"] = ", ".join(trace_name_labels)
1565-
if "title" not in layout_patch and args["template"].layout.margin.t is None:
1630+
layout_patch["legend"]["title_text"] = ", ".join(trace_name_labels)
1631+
if args["title"]:
1632+
layout_patch["title_text"] = args["title"]
1633+
elif args["template"].layout.margin.t is None:
15661634
layout_patch["margin"] = {"t": 60}
15671635
if (
15681636
"size" in args
@@ -1592,7 +1660,7 @@ def make_figure(args, constructor, trace_patch={}, layout_patch={}):
15921660

15931661
# Add traces, layout and frames to figure
15941662
fig.add_traces(frame_list[0]["data"] if len(frame_list) > 0 else [])
1595-
fig.layout.update(layout_patch)
1663+
fig.update_layout(layout_patch)
15961664
if "template" in args and args["template"] is not None:
15971665
fig.update_layout(template=args["template"], overwrite=True)
15981666
fig.frames = frame_list if len(frames) > 1 else []

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=[

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

+5-3
Original file line numberDiff line numberDiff line change
@@ -244,7 +244,7 @@ def imshow(
244244
cmax=range_color[1],
245245
)
246246
if labels["color"]:
247-
layout["coloraxis1"]["colorbar"] = dict(title=labels["color"])
247+
layout["coloraxis1"]["colorbar"] = dict(title_text=labels["color"])
248248

249249
# For 2D+RGB data, use Image trace
250250
elif img.ndim == 3 and img.shape[-1] in [3, 4]:
@@ -262,10 +262,12 @@ def imshow(
262262
)
263263

264264
layout_patch = dict()
265-
for attr_name in ["title", "height", "width"]:
265+
for attr_name in ["height", "width"]:
266266
if args[attr_name]:
267267
layout_patch[attr_name] = args[attr_name]
268-
if "title" not in layout_patch and args["template"].layout.margin.t is None:
268+
if args["title"]:
269+
layout_patch["title_text"] = args["title"]
270+
elif args["template"].layout.margin.t is None:
269271
layout_patch["margin"] = {"t": 60}
270272
fig = go.Figure(data=trace, layout=layout)
271273
fig.update_layout(layout_patch)

0 commit comments

Comments
 (0)