Skip to content

Commit c5074c5

Browse files
TomAugspurgerNico Cernek
authored and
Nico Cernek
committed
API: Change matplotlib formatter registration (pandas-dev#28722)
1 parent b645ce5 commit c5074c5

File tree

13 files changed

+96
-98
lines changed

13 files changed

+96
-98
lines changed

ci/code_checks.sh

+1-1
Original file line numberDiff line numberDiff line change
@@ -208,7 +208,7 @@ import sys
208208
import pandas
209209
210210
blacklist = {'bs4', 'gcsfs', 'html5lib', 'http', 'ipython', 'jinja2', 'hypothesis',
211-
'lxml', 'numexpr', 'openpyxl', 'py', 'pytest', 's3fs', 'scipy',
211+
'lxml', 'matplotlib', 'numexpr', 'openpyxl', 'py', 'pytest', 's3fs', 'scipy',
212212
'tables', 'urllib.request', 'xlrd', 'xlsxwriter', 'xlwt'}
213213
214214
# GH#28227 for some of these check for top-level modules, while others are

doc/source/user_guide/visualization.rst

+15
Original file line numberDiff line numberDiff line change
@@ -1190,6 +1190,21 @@ with "(right)" in the legend. To turn off the automatic marking, use the
11901190
11911191
plt.close('all')
11921192
1193+
.. _plotting.formatters:
1194+
1195+
Custom formatters for timeseries plots
1196+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
1197+
1198+
.. versionchanged:: 1.0.0
1199+
1200+
Pandas provides custom formatters for timeseries plots. These change the
1201+
formatting of the axis labels for dates and times. By default,
1202+
the custom formatters are applied only to plots created by pandas with
1203+
:meth:`DataFrame.plot` or :meth:`Series.plot`. To have them apply to all
1204+
plots, including those made by matplotlib, set the option
1205+
``pd.options.plotting.matplotlib.register_converters = True`` or use
1206+
:meth:`pandas.plotting.register_matplotlib_converters`.
1207+
11931208
Suppressing tick resolution adjustment
11941209
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
11951210

doc/source/whatsnew/v1.0.0.rst

+26
Original file line numberDiff line numberDiff line change
@@ -213,8 +213,13 @@ Other API changes
213213
- In order to improve tab-completion, Pandas does not include most deprecated attributes when introspecting a pandas object using ``dir`` (e.g. ``dir(df)``).
214214
To see which attributes are excluded, see an object's ``_deprecations`` attribute, for example ``pd.DataFrame._deprecations`` (:issue:`28805`).
215215
- The returned dtype of ::func:`pd.unique` now matches the input dtype. (:issue:`27874`)
216+
- Changed the default configuration value for ``options.matplotlib.register_converters`` from ``True`` to ``"auto"`` (:issue:`18720`).
217+
Now, pandas custom formatters will only be applied to plots created by pandas, through :meth:`~DataFrame.plot`.
218+
Previously, pandas' formatters would be applied to all plots created *after* a :meth:`~DataFrame.plot`.
219+
See :ref:`units registration <whatsnew_1000.matplotlib_units>` for more.
216220
-
217221

222+
218223
.. _whatsnew_1000.api.documentation:
219224

220225
Documentation Improvements
@@ -247,6 +252,27 @@ with migrating existing code.
247252
Removal of prior version deprecations/changes
248253
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
249254

255+
.. _whatsnew_1000.matplotlib_units:
256+
257+
**Matplotlib unit registration**
258+
259+
Previously, pandas would register converters with matplotlib as a side effect of importing pandas (:issue:`18720`).
260+
This changed the output of plots made via matplotlib plots after pandas was imported, even if you were using
261+
matplotlib directly rather than rather than :meth:`~DataFrame.plot`.
262+
263+
To use pandas formatters with a matplotlib plot, specify
264+
265+
.. code-block:: python
266+
267+
>>> import pandas as pd
268+
>>> pd.options.plotting.matplotlib.register_converters = True
269+
270+
Note that plots created by :meth:`DataFrame.plot` and :meth:`Series.plot` *do* register the converters
271+
automatically. The only behavior change is when plotting a date-like object via ``matplotlib.pyplot.plot``
272+
or ``matplotlib.Axes.plot``. See :ref:`plotting.formatters` for more.
273+
274+
**Other removals**
275+
250276
- Removed the previously deprecated :meth:`Series.get_value`, :meth:`Series.set_value`, :meth:`DataFrame.get_value`, :meth:`DataFrame.set_value` (:issue:`17739`)
251277
- Changed the the default value of `inplace` in :meth:`DataFrame.set_index` and :meth:`Series.set_axis`. It now defaults to False (:issue:`27600`)
252278
- :meth:`pandas.Series.str.cat` now defaults to aligning ``others``, using ``join='left'`` (:issue:`27611`)

pandas/core/config_init.py

+3-3
Original file line numberDiff line numberDiff line change
@@ -599,7 +599,7 @@ def register_plotting_backend_cb(key):
599599

600600

601601
register_converter_doc = """
602-
: bool
602+
: bool or 'auto'.
603603
Whether to register converters with matplotlib's units registry for
604604
dates, times, datetimes, and Periods. Toggling to False will remove
605605
the converters, restoring any converters that pandas overwrote.
@@ -619,8 +619,8 @@ def register_converter_cb(key):
619619
with cf.config_prefix("plotting.matplotlib"):
620620
cf.register_option(
621621
"register_converters",
622-
True,
622+
"auto",
623623
register_converter_doc,
624-
validator=bool,
624+
validator=is_one_of_factory(["auto", True, False]),
625625
cb=register_converter_cb,
626626
)

pandas/plotting/_core.py

-6
Original file line numberDiff line numberDiff line change
@@ -3,19 +3,13 @@
33

44
from pandas._config import get_option
55

6-
from pandas.compat._optional import import_optional_dependency
76
from pandas.util._decorators import Appender
87

98
from pandas.core.dtypes.common import is_integer, is_list_like
109
from pandas.core.dtypes.generic import ABCDataFrame, ABCSeries
1110

1211
from pandas.core.base import PandasObject
1312

14-
# Trigger matplotlib import, which implicitly registers our
15-
# converts. Implicit registration is deprecated, and when enforced
16-
# we can lazily import matplotlib.
17-
import_optional_dependency("pandas.plotting._matplotlib", raise_on_missing=False)
18-
1913

2014
def hist_series(
2115
self,

pandas/plotting/_matplotlib/__init__.py

-5
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,3 @@
1-
from pandas._config import get_option
2-
31
from pandas.plotting._matplotlib.boxplot import (
42
BoxPlot,
53
boxplot,
@@ -42,9 +40,6 @@
4240
"hexbin": HexBinPlot,
4341
}
4442

45-
if get_option("plotting.matplotlib.register_converters"):
46-
register(explicit=False)
47-
4843

4944
def plot(data, kind, **kwargs):
5045
# Importing pyplot at the top of the file (before the converters are

pandas/plotting/_matplotlib/boxplot.py

-3
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@
1111
import pandas as pd
1212

1313
from pandas.io.formats.printing import pprint_thing
14-
from pandas.plotting._matplotlib import converter
1514
from pandas.plotting._matplotlib.core import LinePlot, MPLPlot
1615
from pandas.plotting._matplotlib.style import _get_standard_colors
1716
from pandas.plotting._matplotlib.tools import _flatten, _subplots
@@ -364,7 +363,6 @@ def boxplot_frame(
364363
):
365364
import matplotlib.pyplot as plt
366365

367-
converter._WARN = False # no warning for pandas plots
368366
ax = boxplot(
369367
self,
370368
column=column,
@@ -396,7 +394,6 @@ def boxplot_frame_groupby(
396394
sharey=True,
397395
**kwds
398396
):
399-
converter._WARN = False # no warning for pandas plots
400397
if subplots is True:
401398
naxes = len(grouped)
402399
fig, axes = _subplots(

pandas/plotting/_matplotlib/converter.py

+37-32
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
1+
import contextlib
12
import datetime as pydt
23
from datetime import datetime, timedelta
3-
import warnings
4+
import functools
45

56
from dateutil.relativedelta import relativedelta
67
import matplotlib.dates as dates
@@ -23,6 +24,7 @@
2324
)
2425
from pandas.core.dtypes.generic import ABCSeries
2526

27+
from pandas import get_option
2628
import pandas.core.common as com
2729
from pandas.core.index import Index
2830
from pandas.core.indexes.datetimes import date_range
@@ -39,7 +41,6 @@
3941

4042
MUSEC_PER_DAY = 1e6 * SEC_PER_DAY
4143

42-
_WARN = True # Global for whether pandas has registered the units explicitly
4344
_mpl_units = {} # Cache for units overwritten by us
4445

4546

@@ -55,13 +56,42 @@ def get_pairs():
5556
return pairs
5657

5758

58-
def register(explicit=True):
59-
# Renamed in pandas.plotting.__init__
60-
global _WARN
59+
def register_pandas_matplotlib_converters(func):
60+
"""
61+
Decorator applying pandas_converters.
62+
"""
63+
64+
@functools.wraps(func)
65+
def wrapper(*args, **kwargs):
66+
with pandas_converters():
67+
return func(*args, **kwargs)
6168

62-
if explicit:
63-
_WARN = False
69+
return wrapper
6470

71+
72+
@contextlib.contextmanager
73+
def pandas_converters():
74+
"""
75+
Context manager registering pandas' converters for a plot.
76+
77+
See Also
78+
--------
79+
register_pandas_matplotlib_converters : Decorator that applies this.
80+
"""
81+
value = get_option("plotting.matplotlib.register_converters")
82+
83+
if value:
84+
# register for True or "auto"
85+
register()
86+
try:
87+
yield
88+
finally:
89+
if value == "auto":
90+
# only deregister for "auto"
91+
deregister()
92+
93+
94+
def register():
6595
pairs = get_pairs()
6696
for type_, cls in pairs:
6797
# Cache previous converter if present
@@ -86,24 +116,6 @@ def deregister():
86116
units.registry[unit] = formatter
87117

88118

89-
def _check_implicitly_registered():
90-
global _WARN
91-
92-
if _WARN:
93-
msg = (
94-
"Using an implicitly registered datetime converter for a "
95-
"matplotlib plotting method. The converter was registered "
96-
"by pandas on import. Future versions of pandas will require "
97-
"you to explicitly register matplotlib converters.\n\n"
98-
"To register the converters:\n\t"
99-
">>> from pandas.plotting import register_matplotlib_converters"
100-
"\n\t"
101-
">>> register_matplotlib_converters()"
102-
)
103-
warnings.warn(msg, FutureWarning)
104-
_WARN = False
105-
106-
107119
def _to_ordinalf(tm):
108120
tot_sec = tm.hour * 3600 + tm.minute * 60 + tm.second + float(tm.microsecond / 1e6)
109121
return tot_sec
@@ -253,7 +265,6 @@ class DatetimeConverter(dates.DateConverter):
253265
@staticmethod
254266
def convert(values, unit, axis):
255267
# values might be a 1-d array, or a list-like of arrays.
256-
_check_implicitly_registered()
257268
if is_nested_list_like(values):
258269
values = [DatetimeConverter._convert_1d(v, unit, axis) for v in values]
259270
else:
@@ -330,7 +341,6 @@ def __init__(self, locator, tz=None, defaultfmt="%Y-%m-%d"):
330341
class PandasAutoDateLocator(dates.AutoDateLocator):
331342
def get_locator(self, dmin, dmax):
332343
"""Pick the best locator based on a distance."""
333-
_check_implicitly_registered()
334344
delta = relativedelta(dmax, dmin)
335345

336346
num_days = (delta.years * 12.0 + delta.months) * 31.0 + delta.days
@@ -372,7 +382,6 @@ def get_unit_generic(freq):
372382

373383
def __call__(self):
374384
# if no data have been set, this will tank with a ValueError
375-
_check_implicitly_registered()
376385
try:
377386
dmin, dmax = self.viewlim_to_dt()
378387
except ValueError:
@@ -990,7 +999,6 @@ def _get_default_locs(self, vmin, vmax):
990999
def __call__(self):
9911000
"Return the locations of the ticks."
9921001
# axis calls Locator.set_axis inside set_m<xxxx>_formatter
993-
_check_implicitly_registered()
9941002

9951003
vi = tuple(self.axis.get_view_interval())
9961004
if vi != self.plot_obj.view_interval:
@@ -1075,7 +1083,6 @@ def set_locs(self, locs):
10751083
"Sets the locations of the ticks"
10761084
# don't actually use the locs. This is just needed to work with
10771085
# matplotlib. Force to use vmin, vmax
1078-
_check_implicitly_registered()
10791086

10801087
self.locs = locs
10811088

@@ -1088,7 +1095,6 @@ def set_locs(self, locs):
10881095
self._set_default_format(vmin, vmax)
10891096

10901097
def __call__(self, x, pos=0):
1091-
_check_implicitly_registered()
10921098

10931099
if self.formatdict is None:
10941100
return ""
@@ -1120,7 +1126,6 @@ def format_timedelta_ticks(x, pos, n_decimals):
11201126
return s
11211127

11221128
def __call__(self, x, pos=0):
1123-
_check_implicitly_registered()
11241129
(vmin, vmax) = tuple(self.axis.get_view_interval())
11251130
n_decimals = int(np.ceil(np.log10(100 * 1e9 / (vmax - vmin))))
11261131
if n_decimals > 9:

pandas/plotting/_matplotlib/core.py

+2-7
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,6 @@
44

55
import numpy as np
66

7-
from pandas._config import get_option
8-
97
from pandas.errors import AbstractMethodError
108
from pandas.util._decorators import cache_readonly
119

@@ -28,8 +26,8 @@
2826
import pandas.core.common as com
2927

3028
from pandas.io.formats.printing import pprint_thing
31-
from pandas.plotting._matplotlib import converter
3229
from pandas.plotting._matplotlib.compat import _mpl_ge_3_0_0
30+
from pandas.plotting._matplotlib.converter import register_pandas_matplotlib_converters
3331
from pandas.plotting._matplotlib.style import _get_standard_colors
3432
from pandas.plotting._matplotlib.tools import (
3533
_flatten,
@@ -41,9 +39,6 @@
4139
table,
4240
)
4341

44-
if get_option("plotting.matplotlib.register_converters"):
45-
converter.register(explicit=False)
46-
4742

4843
class MPLPlot:
4944
"""
@@ -112,7 +107,6 @@ def __init__(
112107

113108
import matplotlib.pyplot as plt
114109

115-
converter._WARN = False # no warning for pandas plots
116110
self.data = data
117111
self.by = by
118112

@@ -648,6 +642,7 @@ def _get_xticks(self, convert_period=False):
648642
return x
649643

650644
@classmethod
645+
@register_pandas_matplotlib_converters
651646
def _plot(cls, ax, x, y, style=None, is_errorbar=False, **kwds):
652647
mask = isna(y)
653648
if mask.any():

pandas/plotting/_matplotlib/hist.py

-3
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@
99
import pandas.core.common as com
1010

1111
from pandas.io.formats.printing import pprint_thing
12-
from pandas.plotting._matplotlib import converter
1312
from pandas.plotting._matplotlib.core import LinePlot, MPLPlot
1413
from pandas.plotting._matplotlib.tools import _flatten, _set_ticks_props, _subplots
1514

@@ -255,7 +254,6 @@ def _grouped_hist(
255254
def plot_group(group, ax):
256255
ax.hist(group.dropna().values, bins=bins, **kwargs)
257256

258-
converter._WARN = False # no warning for pandas plots
259257
xrot = xrot or rot
260258

261259
fig, axes = _grouped_plot(
@@ -363,7 +361,6 @@ def hist_frame(
363361
bins=10,
364362
**kwds
365363
):
366-
converter._WARN = False # no warning for pandas plots
367364
if by is not None:
368365
axes = _grouped_hist(
369366
data,

0 commit comments

Comments
 (0)