Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

API: Restore implicit converter registration #18307

Merged
merged 26 commits into from
Dec 7, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 @@ -2375,6 +2375,17 @@ Style Export and Import
Styler.use
Styler.to_excel

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 @@ -480,3 +480,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 @@ -45,14 +46,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 @@ -190,6 +273,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 @@ -274,6 +358,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 @@ -315,6 +400,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 @@ -917,6 +1003,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 @@ -1001,6 +1089,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 @@ -1012,6 +1102,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 @@ -1042,6 +1134,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 @@ -2064,7 +2062,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 @@ -2160,7 +2158,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 @@ -2294,6 +2292,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 @@ -2358,7 +2358,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()
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this one doesn't need to be replaced with _converter.WARN = False ?

_converter._WARN = False
if subplots is True:
naxes = len(grouped)
fig, axes = _subplots(naxes=naxes, squeeze=False,
Expand Down
Loading