Skip to content

Commit

Permalink
API: Restore implicit converter registration (pandas-dev#18307)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
TomAugspurger committed Dec 8, 2017
1 parent f5fb09e commit 2d1b85a
Show file tree
Hide file tree
Showing 10 changed files with 485 additions and 185 deletions.
1 change: 0 additions & 1 deletion ci/check_imports.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@
'ipython',
'jinja2'
'lxml',
'matplotlib',
'numexpr',
'openpyxl',
'py',
Expand Down
11 changes: 11 additions & 0 deletions doc/source/api.rst
Original file line number Diff line number Diff line change
Expand Up @@ -2174,6 +2174,17 @@ Style Export and Import
Styler.export
Styler.use

Plotting
~~~~~~~~

.. currentmodule:: pandas

.. autosummary::
:toctree: generated/

plotting.register_matplotlib_converters
plotting.deregister_matplotlib_converters

.. currentmodule:: pandas

General utility functions
Expand Down
318 changes: 160 additions & 158 deletions doc/source/options.rst

Large diffs are not rendered by default.

35 changes: 32 additions & 3 deletions doc/source/whatsnew/v0.21.1.txt
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,36 @@ This is a minor release from 0.21.1 and includes a number of deprecations, new
features, enhancements, and performance improvements along with a large number
of bug fixes. We recommend that all users upgrade to this version.

.. _whatsnew_0211.special:

Restore Matplotlib datetime Converter Registration
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

Pandas implements some matplotlib converters for nicely formatting the axis
labels on plots with ``datetime`` or ``Period`` values. Prior to pandas 0.21.0,
these were implicitly registered with matplotlib, as a side effect of ``import
pandas``.

In pandas 0.21.0, we required users to explicitly register the
converter. This caused problems for some users who relied on those converters
being present for regular ``matplotlib.pyplot`` plotting methods, so we're
temporarily reverting that change; pandas will again register the converters on
import.

We've added a new option to control the converters:
``pd.options.plotting.matplotlib.register_converters``. By default, they are
registered. Toggling this to ``False`` removes pandas' formatters and restore
any converters we overwrote when registering them (:issue:`18301`).

We're working with the matplotlib developers to make this easier. We're trying
to balance user convenience (automatically registering the converters) with
import performance and best practices (importing pandas shouldn't have the side
effect of overwriting any custom converters you've already set). In the future
we hope to have most of the datetime formatting functionality in matplotlib,
with just the pandas-specific converters in pandas. We'll then gracefully
deprecate the automatic registration of converters in favor of users explicitly
registering them when they want them.

.. _whatsnew_0211.enhancements:

New features
Expand All @@ -30,9 +60,8 @@ Other Enhancements
Deprecations
~~~~~~~~~~~~

-
-
-
- ``pandas.tseries.register`` has been renamed to
:func:`pandas.plotting.register_matplotlib_converters`` (:issue:`18301`)

.. _whatsnew_0211.performance:

Expand Down
26 changes: 26 additions & 0 deletions pandas/core/config_init.py
Original file line number Diff line number Diff line change
Expand Up @@ -479,3 +479,29 @@ def use_inf_as_na_cb(key):
cf.register_option(
'engine', 'auto', parquet_engine_doc,
validator=is_one_of_factory(['auto', 'pyarrow', 'fastparquet']))

# --------
# Plotting
# ---------

register_converter_doc = """
: bool
Whether to register converters with matplotlib's units registry for
dates, times, datetimes, and Periods. Toggling to False will remove
the converters, restoring any converters that pandas overwrote.
"""


def register_converter_cb(key):
from pandas.plotting import register_matplotlib_converters
from pandas.plotting import deregister_matplotlib_converters

if cf.get_option(key):
register_matplotlib_converters()
else:
deregister_matplotlib_converters()


with cf.config_prefix("plotting.matplotlib"):
cf.register_option("register_converters", True, register_converter_doc,
validator=bool, cb=register_converter_cb)
7 changes: 7 additions & 0 deletions pandas/plotting/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,10 @@
from pandas.plotting._core import boxplot
from pandas.plotting._style import plot_params
from pandas.plotting._tools import table
try:
from pandas.plotting._converter import \
register as register_matplotlib_converters
from pandas.plotting._converter import \
deregister as deregister_matplotlib_converters
except ImportError:
pass
107 changes: 100 additions & 7 deletions pandas/plotting/_converter.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import warnings
from datetime import datetime, timedelta
import datetime as pydt
import numpy as np
Expand Down Expand Up @@ -44,14 +45,96 @@

MUSEC_PER_DAY = 1e6 * SEC_PER_DAY

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

def register():
units.registry[lib.Timestamp] = DatetimeConverter()
units.registry[Period] = PeriodConverter()
units.registry[pydt.datetime] = DatetimeConverter()
units.registry[pydt.date] = DatetimeConverter()
units.registry[pydt.time] = TimeConverter()
units.registry[np.datetime64] = DatetimeConverter()

def get_pairs():
pairs = [
(lib.Timestamp, DatetimeConverter),
(Period, PeriodConverter),
(pydt.datetime, DatetimeConverter),
(pydt.date, DatetimeConverter),
(pydt.time, TimeConverter),
(np.datetime64, DatetimeConverter),
]
return pairs


def register(explicit=True):
"""Register Pandas Formatters and Converters with matplotlib
This function modifies the global ``matplotlib.units.registry``
dictionary. Pandas adds custom converters for
* pd.Timestamp
* pd.Period
* np.datetime64
* datetime.datetime
* datetime.date
* datetime.time
See Also
--------
deregister_matplotlib_converter
"""
# Renamed in pandas.plotting.__init__
global _WARN

if explicit:
_WARN = False

pairs = get_pairs()
for type_, cls in pairs:
converter = cls()
if type_ in units.registry:
previous = units.registry[type_]
_mpl_units[type_] = previous
units.registry[type_] = converter


def deregister():
"""Remove pandas' formatters and converters
Removes the custom converters added by :func:`register`. This
attempts to set the state of the registry back to the state before
pandas registered its own units. Converters for pandas' own types like
Timestamp and Period are removed completely. Converters for types
pandas overwrites, like ``datetime.datetime``, are restored to their
original value.
See Also
--------
deregister_matplotlib_converters
"""
# Renamed in pandas.plotting.__init__
for type_, cls in get_pairs():
# We use type to catch our classes directly, no inheritance
if type(units.registry.get(type_)) is cls:
units.registry.pop(type_)

# restore the old keys
for unit, formatter in _mpl_units.items():
if type(formatter) not in {DatetimeConverter, PeriodConverter,
TimeConverter}:
# make it idempotent by excluding ours.
units.registry[unit] = formatter


def _check_implicitly_registered():
global _WARN

if _WARN:
msg = ("Using an implicitly registered datetime converter for a "
"matplotlib plotting method. The converter was registered "
"by pandas on import. Future versions of pandas will require "
"you to explicitly register matplotlib converters.\n\n"
"To register the converters:\n\t"
">>> from pandas.plotting import register_matplotlib_converters"
"\n\t"
">>> register_matplotlib_converters()")
warnings.warn(msg, FutureWarning)
_WARN = False


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

def get_locator(self, dmin, dmax):
'Pick the best locator based on a distance.'
_check_implicitly_registered()
delta = relativedelta(dmax, dmin)

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

def __call__(self):
# if no data have been set, this will tank with a ValueError
_check_implicitly_registered()
try:
dmin, dmax = self.viewlim_to_dt()
except ValueError:
Expand Down Expand Up @@ -914,6 +1000,8 @@ def _get_default_locs(self, vmin, vmax):
def __call__(self):
'Return the locations of the ticks.'
# axis calls Locator.set_axis inside set_m<xxxx>_formatter
_check_implicitly_registered()

vi = tuple(self.axis.get_view_interval())
if vi != self.plot_obj.view_interval:
self.plot_obj.date_axis_info = None
Expand Down Expand Up @@ -998,6 +1086,8 @@ def set_locs(self, locs):
'Sets the locations of the ticks'
# don't actually use the locs. This is just needed to work with
# matplotlib. Force to use vmin, vmax
_check_implicitly_registered()

self.locs = locs

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

def __call__(self, x, pos=0):
_check_implicitly_registered()

if self.formatdict is None:
return ''
else:
Expand Down Expand Up @@ -1039,6 +1131,7 @@ def format_timedelta_ticks(x, pos, n_decimals):
return s

def __call__(self, x, pos=0):
_check_implicitly_registered()
(vmin, vmax) = tuple(self.axis.get_view_interval())
n_decimals = int(np.ceil(np.log10(100 * 1e9 / (vmax - vmin))))
if n_decimals > 9:
Expand Down
28 changes: 14 additions & 14 deletions pandas/plotting/_core.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@

from pandas.util._decorators import cache_readonly
from pandas.core.base import PandasObject
from pandas.core.config import get_option
from pandas.core.dtypes.missing import isna, notna, remove_na_arraylike
from pandas.core.dtypes.common import (
is_list_like,
Expand Down Expand Up @@ -40,16 +41,13 @@
_get_xlim, _set_ticks_props,
format_date_labels)

_registered = False


def _setup():
# delay the import of matplotlib until nescessary
global _registered
if not _registered:
from pandas.plotting import _converter
_converter.register()
_registered = True
try:
from pandas.plotting import _converter
except ImportError:
pass
else:
if get_option('plotting.matplotlib.register_converters'):
_converter.register(explicit=True)


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

_setup()
_converter._WARN = False
self.data = data
self.by = by

Expand Down Expand Up @@ -2063,7 +2061,7 @@ def boxplot_frame(self, column=None, by=None, ax=None, fontsize=None, rot=0,
grid=True, figsize=None, layout=None,
return_type=None, **kwds):
import matplotlib.pyplot as plt
_setup()
_converter._WARN = False
ax = boxplot(self, column=column, by=by, ax=ax, fontsize=fontsize,
grid=grid, rot=rot, figsize=figsize, layout=layout,
return_type=return_type, **kwds)
Expand Down Expand Up @@ -2159,7 +2157,7 @@ def hist_frame(data, column=None, by=None, grid=True, xlabelsize=None,
kwds : other plotting keyword arguments
To be passed to hist function
"""
_setup()
_converter._WARN = False
if by is not None:
axes = grouped_hist(data, column=column, by=by, ax=ax, grid=grid,
figsize=figsize, sharex=sharex, sharey=sharey,
Expand Down Expand Up @@ -2293,6 +2291,8 @@ def grouped_hist(data, column=None, by=None, ax=None, bins=50, figsize=None,
-------
axes: collection of Matplotlib Axes
"""
_converter._WARN = False

def plot_group(group, ax):
ax.hist(group.dropna().values, bins=bins, **kwargs)

Expand Down Expand Up @@ -2356,7 +2356,7 @@ def boxplot_frame_groupby(grouped, subplots=True, column=None, fontsize=None,
>>> grouped = df.unstack(level='lvl1').groupby(level=0, axis=1)
>>> boxplot_frame_groupby(grouped, subplots=False)
"""
_setup()
_converter._WARN = False
if subplots is True:
naxes = len(grouped)
fig, axes = _subplots(naxes=naxes, squeeze=False,
Expand Down
Loading

0 comments on commit 2d1b85a

Please sign in to comment.