Skip to content

Commit 48c690a

Browse files
committed
API: Restore implicit converter registration (#18307)
* API: Restore implicit converter registration * Remove matplotlib from blacklist * fixup! Remove matplotlib from blacklist * Add option for toggling formatters * Remove move * Handle no matplotlib * Cleanup * Test no register * Restore original state * Added deregister * Doc, naming * Naming * Added deprecation * PEP8 * Fix typos * Rename it all * Missed one * Check version * No warnings by default * Update release notes * Test fixup - actually switch the default to not warn - We do overwrite matplotlib's formatters * Doc update * Fix deprecation message * Test added by default
1 parent 5251f74 commit 48c690a

File tree

10 files changed

+485
-185
lines changed

10 files changed

+485
-185
lines changed

ci/check_imports.py

-1
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@
99
'ipython',
1010
'jinja2'
1111
'lxml',
12-
'matplotlib',
1312
'numexpr',
1413
'openpyxl',
1514
'py',

doc/source/api.rst

+11
Original file line numberDiff line numberDiff line change
@@ -2174,6 +2174,17 @@ Style Export and Import
21742174
Styler.export
21752175
Styler.use
21762176

2177+
Plotting
2178+
~~~~~~~~
2179+
2180+
.. currentmodule:: pandas
2181+
2182+
.. autosummary::
2183+
:toctree: generated/
2184+
2185+
plotting.register_matplotlib_converters
2186+
plotting.deregister_matplotlib_converters
2187+
21772188
.. currentmodule:: pandas
21782189

21792190
General utility functions

doc/source/options.rst

+160-158
Large diffs are not rendered by default.

doc/source/whatsnew/v0.21.1.txt

+32-3
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,36 @@ This is a minor release from 0.21.1 and includes a number of deprecations, new
77
features, enhancements, and performance improvements along with a large number
88
of bug fixes. We recommend that all users upgrade to this version.
99

10+
.. _whatsnew_0211.special:
11+
12+
Restore Matplotlib datetime Converter Registration
13+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
14+
15+
Pandas implements some matplotlib converters for nicely formatting the axis
16+
labels on plots with ``datetime`` or ``Period`` values. Prior to pandas 0.21.0,
17+
these were implicitly registered with matplotlib, as a side effect of ``import
18+
pandas``.
19+
20+
In pandas 0.21.0, we required users to explicitly register the
21+
converter. This caused problems for some users who relied on those converters
22+
being present for regular ``matplotlib.pyplot`` plotting methods, so we're
23+
temporarily reverting that change; pandas will again register the converters on
24+
import.
25+
26+
We've added a new option to control the converters:
27+
``pd.options.plotting.matplotlib.register_converters``. By default, they are
28+
registered. Toggling this to ``False`` removes pandas' formatters and restore
29+
any converters we overwrote when registering them (:issue:`18301`).
30+
31+
We're working with the matplotlib developers to make this easier. We're trying
32+
to balance user convenience (automatically registering the converters) with
33+
import performance and best practices (importing pandas shouldn't have the side
34+
effect of overwriting any custom converters you've already set). In the future
35+
we hope to have most of the datetime formatting functionality in matplotlib,
36+
with just the pandas-specific converters in pandas. We'll then gracefully
37+
deprecate the automatic registration of converters in favor of users explicitly
38+
registering them when they want them.
39+
1040
.. _whatsnew_0211.enhancements:
1141

1242
New features
@@ -30,9 +60,8 @@ Other Enhancements
3060
Deprecations
3161
~~~~~~~~~~~~
3262

33-
-
34-
-
35-
-
63+
- ``pandas.tseries.register`` has been renamed to
64+
:func:`pandas.plotting.register_matplotlib_converters`` (:issue:`18301`)
3665

3766
.. _whatsnew_0211.performance:
3867

pandas/core/config_init.py

+26
Original file line numberDiff line numberDiff line change
@@ -479,3 +479,29 @@ def use_inf_as_na_cb(key):
479479
cf.register_option(
480480
'engine', 'auto', parquet_engine_doc,
481481
validator=is_one_of_factory(['auto', 'pyarrow', 'fastparquet']))
482+
483+
# --------
484+
# Plotting
485+
# ---------
486+
487+
register_converter_doc = """
488+
: bool
489+
Whether to register converters with matplotlib's units registry for
490+
dates, times, datetimes, and Periods. Toggling to False will remove
491+
the converters, restoring any converters that pandas overwrote.
492+
"""
493+
494+
495+
def register_converter_cb(key):
496+
from pandas.plotting import register_matplotlib_converters
497+
from pandas.plotting import deregister_matplotlib_converters
498+
499+
if cf.get_option(key):
500+
register_matplotlib_converters()
501+
else:
502+
deregister_matplotlib_converters()
503+
504+
505+
with cf.config_prefix("plotting.matplotlib"):
506+
cf.register_option("register_converters", True, register_converter_doc,
507+
validator=bool, cb=register_converter_cb)

pandas/plotting/__init__.py

+7
Original file line numberDiff line numberDiff line change
@@ -11,3 +11,10 @@
1111
from pandas.plotting._core import boxplot
1212
from pandas.plotting._style import plot_params
1313
from pandas.plotting._tools import table
14+
try:
15+
from pandas.plotting._converter import \
16+
register as register_matplotlib_converters
17+
from pandas.plotting._converter import \
18+
deregister as deregister_matplotlib_converters
19+
except ImportError:
20+
pass

pandas/plotting/_converter.py

+100-7
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import warnings
12
from datetime import datetime, timedelta
23
import datetime as pydt
34
import numpy as np
@@ -44,14 +45,96 @@
4445

4546
MUSEC_PER_DAY = 1e6 * SEC_PER_DAY
4647

48+
_WARN = True # Global for whether pandas has registered the units explicitly
49+
_mpl_units = {} # Cache for units overwritten by us
4750

48-
def register():
49-
units.registry[lib.Timestamp] = DatetimeConverter()
50-
units.registry[Period] = PeriodConverter()
51-
units.registry[pydt.datetime] = DatetimeConverter()
52-
units.registry[pydt.date] = DatetimeConverter()
53-
units.registry[pydt.time] = TimeConverter()
54-
units.registry[np.datetime64] = DatetimeConverter()
51+
52+
def get_pairs():
53+
pairs = [
54+
(lib.Timestamp, DatetimeConverter),
55+
(Period, PeriodConverter),
56+
(pydt.datetime, DatetimeConverter),
57+
(pydt.date, DatetimeConverter),
58+
(pydt.time, TimeConverter),
59+
(np.datetime64, DatetimeConverter),
60+
]
61+
return pairs
62+
63+
64+
def register(explicit=True):
65+
"""Register Pandas Formatters and Converters with matplotlib
66+
67+
This function modifies the global ``matplotlib.units.registry``
68+
dictionary. Pandas adds custom converters for
69+
70+
* pd.Timestamp
71+
* pd.Period
72+
* np.datetime64
73+
* datetime.datetime
74+
* datetime.date
75+
* datetime.time
76+
77+
See Also
78+
--------
79+
deregister_matplotlib_converter
80+
"""
81+
# Renamed in pandas.plotting.__init__
82+
global _WARN
83+
84+
if explicit:
85+
_WARN = False
86+
87+
pairs = get_pairs()
88+
for type_, cls in pairs:
89+
converter = cls()
90+
if type_ in units.registry:
91+
previous = units.registry[type_]
92+
_mpl_units[type_] = previous
93+
units.registry[type_] = converter
94+
95+
96+
def deregister():
97+
"""Remove pandas' formatters and converters
98+
99+
Removes the custom converters added by :func:`register`. This
100+
attempts to set the state of the registry back to the state before
101+
pandas registered its own units. Converters for pandas' own types like
102+
Timestamp and Period are removed completely. Converters for types
103+
pandas overwrites, like ``datetime.datetime``, are restored to their
104+
original value.
105+
106+
See Also
107+
--------
108+
deregister_matplotlib_converters
109+
"""
110+
# Renamed in pandas.plotting.__init__
111+
for type_, cls in get_pairs():
112+
# We use type to catch our classes directly, no inheritance
113+
if type(units.registry.get(type_)) is cls:
114+
units.registry.pop(type_)
115+
116+
# restore the old keys
117+
for unit, formatter in _mpl_units.items():
118+
if type(formatter) not in {DatetimeConverter, PeriodConverter,
119+
TimeConverter}:
120+
# make it idempotent by excluding ours.
121+
units.registry[unit] = formatter
122+
123+
124+
def _check_implicitly_registered():
125+
global _WARN
126+
127+
if _WARN:
128+
msg = ("Using an implicitly registered datetime converter for a "
129+
"matplotlib plotting method. The converter was registered "
130+
"by pandas on import. Future versions of pandas will require "
131+
"you to explicitly register matplotlib converters.\n\n"
132+
"To register the converters:\n\t"
133+
">>> from pandas.plotting import register_matplotlib_converters"
134+
"\n\t"
135+
">>> register_matplotlib_converters()")
136+
warnings.warn(msg, FutureWarning)
137+
_WARN = False
55138

56139

57140
def _to_ordinalf(tm):
@@ -189,6 +272,7 @@ class DatetimeConverter(dates.DateConverter):
189272
@staticmethod
190273
def convert(values, unit, axis):
191274
# values might be a 1-d array, or a list-like of arrays.
275+
_check_implicitly_registered()
192276
if is_nested_list_like(values):
193277
values = [DatetimeConverter._convert_1d(v, unit, axis)
194278
for v in values]
@@ -273,6 +357,7 @@ class PandasAutoDateLocator(dates.AutoDateLocator):
273357

274358
def get_locator(self, dmin, dmax):
275359
'Pick the best locator based on a distance.'
360+
_check_implicitly_registered()
276361
delta = relativedelta(dmax, dmin)
277362

278363
num_days = (delta.years * 12.0 + delta.months) * 31.0 + delta.days
@@ -314,6 +399,7 @@ def get_unit_generic(freq):
314399

315400
def __call__(self):
316401
# if no data have been set, this will tank with a ValueError
402+
_check_implicitly_registered()
317403
try:
318404
dmin, dmax = self.viewlim_to_dt()
319405
except ValueError:
@@ -914,6 +1000,8 @@ def _get_default_locs(self, vmin, vmax):
9141000
def __call__(self):
9151001
'Return the locations of the ticks.'
9161002
# axis calls Locator.set_axis inside set_m<xxxx>_formatter
1003+
_check_implicitly_registered()
1004+
9171005
vi = tuple(self.axis.get_view_interval())
9181006
if vi != self.plot_obj.view_interval:
9191007
self.plot_obj.date_axis_info = None
@@ -998,6 +1086,8 @@ def set_locs(self, locs):
9981086
'Sets the locations of the ticks'
9991087
# don't actually use the locs. This is just needed to work with
10001088
# matplotlib. Force to use vmin, vmax
1089+
_check_implicitly_registered()
1090+
10011091
self.locs = locs
10021092

10031093
(vmin, vmax) = vi = tuple(self.axis.get_view_interval())
@@ -1009,6 +1099,8 @@ def set_locs(self, locs):
10091099
self._set_default_format(vmin, vmax)
10101100

10111101
def __call__(self, x, pos=0):
1102+
_check_implicitly_registered()
1103+
10121104
if self.formatdict is None:
10131105
return ''
10141106
else:
@@ -1039,6 +1131,7 @@ def format_timedelta_ticks(x, pos, n_decimals):
10391131
return s
10401132

10411133
def __call__(self, x, pos=0):
1134+
_check_implicitly_registered()
10421135
(vmin, vmax) = tuple(self.axis.get_view_interval())
10431136
n_decimals = int(np.ceil(np.log10(100 * 1e9 / (vmax - vmin))))
10441137
if n_decimals > 9:

pandas/plotting/_core.py

+14-14
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111

1212
from pandas.util._decorators import cache_readonly
1313
from pandas.core.base import PandasObject
14+
from pandas.core.config import get_option
1415
from pandas.core.dtypes.missing import isna, notna, remove_na_arraylike
1516
from pandas.core.dtypes.common import (
1617
is_list_like,
@@ -40,16 +41,13 @@
4041
_get_xlim, _set_ticks_props,
4142
format_date_labels)
4243

43-
_registered = False
44-
45-
46-
def _setup():
47-
# delay the import of matplotlib until nescessary
48-
global _registered
49-
if not _registered:
50-
from pandas.plotting import _converter
51-
_converter.register()
52-
_registered = True
44+
try:
45+
from pandas.plotting import _converter
46+
except ImportError:
47+
pass
48+
else:
49+
if get_option('plotting.matplotlib.register_converters'):
50+
_converter.register(explicit=True)
5351

5452

5553
def _get_standard_kind(kind):
@@ -99,7 +97,7 @@ def __init__(self, data, kind=None, by=None, subplots=False, sharex=None,
9997
secondary_y=False, colormap=None,
10098
table=False, layout=None, **kwds):
10199

102-
_setup()
100+
_converter._WARN = False
103101
self.data = data
104102
self.by = by
105103

@@ -2063,7 +2061,7 @@ def boxplot_frame(self, column=None, by=None, ax=None, fontsize=None, rot=0,
20632061
grid=True, figsize=None, layout=None,
20642062
return_type=None, **kwds):
20652063
import matplotlib.pyplot as plt
2066-
_setup()
2064+
_converter._WARN = False
20672065
ax = boxplot(self, column=column, by=by, ax=ax, fontsize=fontsize,
20682066
grid=grid, rot=rot, figsize=figsize, layout=layout,
20692067
return_type=return_type, **kwds)
@@ -2159,7 +2157,7 @@ def hist_frame(data, column=None, by=None, grid=True, xlabelsize=None,
21592157
kwds : other plotting keyword arguments
21602158
To be passed to hist function
21612159
"""
2162-
_setup()
2160+
_converter._WARN = False
21632161
if by is not None:
21642162
axes = grouped_hist(data, column=column, by=by, ax=ax, grid=grid,
21652163
figsize=figsize, sharex=sharex, sharey=sharey,
@@ -2293,6 +2291,8 @@ def grouped_hist(data, column=None, by=None, ax=None, bins=50, figsize=None,
22932291
-------
22942292
axes: collection of Matplotlib Axes
22952293
"""
2294+
_converter._WARN = False
2295+
22962296
def plot_group(group, ax):
22972297
ax.hist(group.dropna().values, bins=bins, **kwargs)
22982298

@@ -2356,7 +2356,7 @@ def boxplot_frame_groupby(grouped, subplots=True, column=None, fontsize=None,
23562356
>>> grouped = df.unstack(level='lvl1').groupby(level=0, axis=1)
23572357
>>> boxplot_frame_groupby(grouped, subplots=False)
23582358
"""
2359-
_setup()
2359+
_converter._WARN = False
23602360
if subplots is True:
23612361
naxes = len(grouped)
23622362
fig, axes = _subplots(naxes=naxes, squeeze=False,

0 commit comments

Comments
 (0)