Skip to content

Commit 24b8f1e

Browse files
API: Restore implicit converter registration (pandas-dev#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 3e506a3 commit 24b8f1e

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
@@ -2373,6 +2373,17 @@ Style Export and Import
23732373
Styler.use
23742374
Styler.to_excel
23752375

2376+
Plotting
2377+
~~~~~~~~
2378+
2379+
.. currentmodule:: pandas
2380+
2381+
.. autosummary::
2382+
:toctree: generated/
2383+
2384+
plotting.register_matplotlib_converters
2385+
plotting.deregister_matplotlib_converters
2386+
23762387
.. currentmodule:: pandas
23772388

23782389
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
@@ -480,3 +480,29 @@ def use_inf_as_na_cb(key):
480480
cf.register_option(
481481
'engine', 'auto', parquet_engine_doc,
482482
validator=is_one_of_factory(['auto', 'pyarrow', 'fastparquet']))
483+
484+
# --------
485+
# Plotting
486+
# ---------
487+
488+
register_converter_doc = """
489+
: bool
490+
Whether to register converters with matplotlib's units registry for
491+
dates, times, datetimes, and Periods. Toggling to False will remove
492+
the converters, restoring any converters that pandas overwrote.
493+
"""
494+
495+
496+
def register_converter_cb(key):
497+
from pandas.plotting import register_matplotlib_converters
498+
from pandas.plotting import deregister_matplotlib_converters
499+
500+
if cf.get_option(key):
501+
register_matplotlib_converters()
502+
else:
503+
deregister_matplotlib_converters()
504+
505+
506+
with cf.config_prefix("plotting.matplotlib"):
507+
cf.register_option("register_converters", True, register_converter_doc,
508+
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
@@ -45,14 +46,96 @@
4546

4647
MUSEC_PER_DAY = 1e6 * SEC_PER_DAY
4748

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

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

57140

58141
def _to_ordinalf(tm):
@@ -190,6 +273,7 @@ class DatetimeConverter(dates.DateConverter):
190273
@staticmethod
191274
def convert(values, unit, axis):
192275
# values might be a 1-d array, or a list-like of arrays.
276+
_check_implicitly_registered()
193277
if is_nested_list_like(values):
194278
values = [DatetimeConverter._convert_1d(v, unit, axis)
195279
for v in values]
@@ -274,6 +358,7 @@ class PandasAutoDateLocator(dates.AutoDateLocator):
274358

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

279364
num_days = (delta.years * 12.0 + delta.months) * 31.0 + delta.days
@@ -315,6 +400,7 @@ def get_unit_generic(freq):
315400

316401
def __call__(self):
317402
# if no data have been set, this will tank with a ValueError
403+
_check_implicitly_registered()
318404
try:
319405
dmin, dmax = self.viewlim_to_dt()
320406
except ValueError:
@@ -918,6 +1004,8 @@ def _get_default_locs(self, vmin, vmax):
9181004
def __call__(self):
9191005
'Return the locations of the ticks.'
9201006
# axis calls Locator.set_axis inside set_m<xxxx>_formatter
1007+
_check_implicitly_registered()
1008+
9211009
vi = tuple(self.axis.get_view_interval())
9221010
if vi != self.plot_obj.view_interval:
9231011
self.plot_obj.date_axis_info = None
@@ -1002,6 +1090,8 @@ def set_locs(self, locs):
10021090
'Sets the locations of the ticks'
10031091
# don't actually use the locs. This is just needed to work with
10041092
# matplotlib. Force to use vmin, vmax
1093+
_check_implicitly_registered()
1094+
10051095
self.locs = locs
10061096

10071097
(vmin, vmax) = vi = tuple(self.axis.get_view_interval())
@@ -1013,6 +1103,8 @@ def set_locs(self, locs):
10131103
self._set_default_format(vmin, vmax)
10141104

10151105
def __call__(self, x, pos=0):
1106+
_check_implicitly_registered()
1107+
10161108
if self.formatdict is None:
10171109
return ''
10181110
else:
@@ -1043,6 +1135,7 @@ def format_timedelta_ticks(x, pos, n_decimals):
10431135
return s
10441136

10451137
def __call__(self, x, pos=0):
1138+
_check_implicitly_registered()
10461139
(vmin, vmax) = tuple(self.axis.get_view_interval())
10471140
n_decimals = int(np.ceil(np.log10(100 * 1e9 / (vmax - vmin))))
10481141
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

@@ -2064,7 +2062,7 @@ def boxplot_frame(self, column=None, by=None, ax=None, fontsize=None, rot=0,
20642062
grid=True, figsize=None, layout=None,
20652063
return_type=None, **kwds):
20662064
import matplotlib.pyplot as plt
2067-
_setup()
2065+
_converter._WARN = False
20682066
ax = boxplot(self, column=column, by=by, ax=ax, fontsize=fontsize,
20692067
grid=grid, rot=rot, figsize=figsize, layout=layout,
20702068
return_type=return_type, **kwds)
@@ -2160,7 +2158,7 @@ def hist_frame(data, column=None, by=None, grid=True, xlabelsize=None,
21602158
`**kwds` : other plotting keyword arguments
21612159
To be passed to hist function
21622160
"""
2163-
_setup()
2161+
_converter._WARN = False
21642162
if by is not None:
21652163
axes = grouped_hist(data, column=column, by=by, ax=ax, grid=grid,
21662164
figsize=figsize, sharex=sharex, sharey=sharey,
@@ -2294,6 +2292,8 @@ def grouped_hist(data, column=None, by=None, ax=None, bins=50, figsize=None,
22942292
-------
22952293
axes: collection of Matplotlib Axes
22962294
"""
2295+
_converter._WARN = False
2296+
22972297
def plot_group(group, ax):
22982298
ax.hist(group.dropna().values, bins=bins, **kwargs)
22992299

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

0 commit comments

Comments
 (0)