Skip to content

API: Change matplotlib formatter registration #28722

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

Merged
merged 15 commits into from
Oct 25, 2019
Merged
2 changes: 1 addition & 1 deletion ci/code_checks.sh
Original file line number Diff line number Diff line change
Expand Up @@ -208,7 +208,7 @@ import sys
import pandas

blacklist = {'bs4', 'gcsfs', 'html5lib', 'http', 'ipython', 'jinja2', 'hypothesis',
'lxml', 'numexpr', 'openpyxl', 'py', 'pytest', 's3fs', 'scipy',
'lxml', 'matplotlib', 'numexpr', 'openpyxl', 'py', 'pytest', 's3fs', 'scipy',
'tables', 'urllib.request', 'xlrd', 'xlsxwriter', 'xlwt'}

# GH#28227 for some of these check for top-level modules, while others are
Expand Down
15 changes: 15 additions & 0 deletions doc/source/user_guide/visualization.rst
Original file line number Diff line number Diff line change
Expand Up @@ -1190,6 +1190,21 @@ with "(right)" in the legend. To turn off the automatic marking, use the

plt.close('all')

.. _plotting.formatters:

Custom formatters for timeseries plots
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

.. versionchanged:: 1.0.0

Pandas provides custom formatters for timeseries plots. These change the
formatting of the axis labels for dates and times. By default,
the custom formatters are applied only to plots created by pandas with
:meth:`DataFrame.plot` or :meth:`Series.plot`. To have them apply to all
plots, including those made by matplotlib, set the option
``pd.options.plotting.matplotlib.register_converters = True`` or use
:meth:`pandas.plotting.register_matplotlib_converters`.

Suppressing tick resolution adjustment
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

Expand Down
27 changes: 26 additions & 1 deletion doc/source/whatsnew/v1.0.0.rst
Original file line number Diff line number Diff line change
Expand Up @@ -174,7 +174,6 @@ Backwards incompatible API changes

pd.arrays.IntervalArray.from_tuples([(0, 1), (2, 3)])


.. _whatsnew_1000.api.other:

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


.. _whatsnew_1000.api.documentation:

Documentation Improvements
Expand Down Expand Up @@ -220,6 +224,27 @@ with migrating existing code.
Removal of prior version deprecations/changes
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

.. _whatsnew_1000.matplotlib_units:

**Matplotlib unit registration**

Previously, pandas would register converters with matplotlib as a side effect of importing pandas (:issue:`18720`).
This changed the output of plots made via matplotlib plots after pandas was imported, even if you were using
matplotlib directly rather than rather than :meth:`~DataFrame.plot`.

To use pandas formatters with a matplotlib plot, specify

.. code-block:: python

>>> import pandas as pd
>>> pd.options.plotting.matplotlib.register_converters = True

Note that plots created by :meth:`DataFrame.plot` and :meth:`Series.plot` *do* register the converters
automatically. The only behavior change is when plotting a date-like object via ``matplotlib.pyplot.plot``
or ``matplotlib.Axes.plot``. See :ref:`plotting.formatters` for more.

**Other removals**

- Removed the previously deprecated :meth:`Series.get_value`, :meth:`Series.set_value`, :meth:`DataFrame.get_value`, :meth:`DataFrame.set_value` (:issue:`17739`)
- Changed the the default value of `inplace` in :meth:`DataFrame.set_index` and :meth:`Series.set_axis`. It now defaults to False (:issue:`27600`)
- :meth:`pandas.Series.str.cat` now defaults to aligning ``others``, using ``join='left'`` (:issue:`27611`)
Expand Down
6 changes: 3 additions & 3 deletions pandas/core/config_init.py
Original file line number Diff line number Diff line change
Expand Up @@ -599,7 +599,7 @@ def register_plotting_backend_cb(key):


register_converter_doc = """
: bool
: bool or 'auto'.
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.
Expand All @@ -619,8 +619,8 @@ def register_converter_cb(key):
with cf.config_prefix("plotting.matplotlib"):
cf.register_option(
"register_converters",
True,
"auto",
register_converter_doc,
validator=bool,
validator=is_one_of_factory(["auto", True, False]),
cb=register_converter_cb,
)
6 changes: 0 additions & 6 deletions pandas/plotting/_core.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,19 +3,13 @@

from pandas._config import get_option

from pandas.compat._optional import import_optional_dependency
from pandas.util._decorators import Appender

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

from pandas.core.base import PandasObject

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


def hist_series(
self,
Expand Down
5 changes: 0 additions & 5 deletions pandas/plotting/_matplotlib/__init__.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
from pandas._config import get_option

from pandas.plotting._matplotlib.boxplot import (
BoxPlot,
boxplot,
Expand Down Expand Up @@ -42,9 +40,6 @@
"hexbin": HexBinPlot,
}

if get_option("plotting.matplotlib.register_converters"):
register(explicit=False)


def plot(data, kind, **kwargs):
# Importing pyplot at the top of the file (before the converters are
Expand Down
3 changes: 0 additions & 3 deletions pandas/plotting/_matplotlib/boxplot.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@
import pandas as pd

from pandas.io.formats.printing import pprint_thing
from pandas.plotting._matplotlib import converter
from pandas.plotting._matplotlib.core import LinePlot, MPLPlot
from pandas.plotting._matplotlib.style import _get_standard_colors
from pandas.plotting._matplotlib.tools import _flatten, _subplots
Expand Down Expand Up @@ -364,7 +363,6 @@ def boxplot_frame(
):
import matplotlib.pyplot as plt

converter._WARN = False # no warning for pandas plots
ax = boxplot(
self,
column=column,
Expand Down Expand Up @@ -396,7 +394,6 @@ def boxplot_frame_groupby(
sharey=True,
**kwds
):
converter._WARN = False # no warning for pandas plots
if subplots is True:
naxes = len(grouped)
fig, axes = _subplots(
Expand Down
69 changes: 37 additions & 32 deletions pandas/plotting/_matplotlib/converter.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import contextlib
import datetime as pydt
from datetime import datetime, timedelta
import warnings
import functools

from dateutil.relativedelta import relativedelta
import matplotlib.dates as dates
Expand All @@ -23,6 +24,7 @@
)
from pandas.core.dtypes.generic import ABCSeries

from pandas import get_option
import pandas.core.common as com
from pandas.core.index import Index
from pandas.core.indexes.datetimes import date_range
Expand All @@ -39,7 +41,6 @@

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


Expand All @@ -55,13 +56,42 @@ def get_pairs():
return pairs


def register(explicit=True):
# Renamed in pandas.plotting.__init__
global _WARN
def register_pandas_matplotlib_converters(func):
"""
Decorator applying pandas_converters.
"""

@functools.wraps(func)
def wrapper(*args, **kwargs):
with pandas_converters():
return func(*args, **kwargs)

if explicit:
_WARN = False
return wrapper


@contextlib.contextmanager
def pandas_converters():
"""
Context manager registering pandas' converters for a plot.

See Also
--------
register_pandas_matplotlib_converters : Decorator that applies this.
"""
value = get_option("plotting.matplotlib.register_converters")

if value:
# register for True or "auto"
register()
try:
yield
finally:
if value == "auto":
# only deregister for "auto"
deregister()


def register():
pairs = get_pairs()
for type_, cls in pairs:
# Cache previous converter if present
Expand All @@ -86,24 +116,6 @@ def deregister():
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):
tot_sec = tm.hour * 3600 + tm.minute * 60 + tm.second + float(tm.microsecond / 1e6)
return tot_sec
Expand Down Expand Up @@ -253,7 +265,6 @@ 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]
else:
Expand Down Expand Up @@ -330,7 +341,6 @@ def __init__(self, locator, tz=None, defaultfmt="%Y-%m-%d"):
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 @@ -372,7 +382,6 @@ 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 @@ -990,7 +999,6 @@ 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:
Expand Down Expand Up @@ -1075,7 +1083,6 @@ 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

Expand All @@ -1088,7 +1095,6 @@ 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 ""
Expand Down Expand Up @@ -1120,7 +1126,6 @@ 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
9 changes: 2 additions & 7 deletions pandas/plotting/_matplotlib/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,6 @@

import numpy as np

from pandas._config import get_option

from pandas.errors import AbstractMethodError
from pandas.util._decorators import cache_readonly

Expand All @@ -28,8 +26,8 @@
import pandas.core.common as com

from pandas.io.formats.printing import pprint_thing
from pandas.plotting._matplotlib import converter
from pandas.plotting._matplotlib.compat import _mpl_ge_3_0_0
from pandas.plotting._matplotlib.converter import register_pandas_matplotlib_converters
from pandas.plotting._matplotlib.style import _get_standard_colors
from pandas.plotting._matplotlib.tools import (
_flatten,
Expand All @@ -41,9 +39,6 @@
table,
)

if get_option("plotting.matplotlib.register_converters"):
converter.register(explicit=False)


class MPLPlot:
"""
Expand Down Expand Up @@ -112,7 +107,6 @@ def __init__(

import matplotlib.pyplot as plt

converter._WARN = False # no warning for pandas plots
self.data = data
self.by = by

Expand Down Expand Up @@ -648,6 +642,7 @@ def _get_xticks(self, convert_period=False):
return x

@classmethod
@register_pandas_matplotlib_converters
def _plot(cls, ax, x, y, style=None, is_errorbar=False, **kwds):
mask = isna(y)
if mask.any():
Expand Down
3 changes: 0 additions & 3 deletions pandas/plotting/_matplotlib/hist.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@
import pandas.core.common as com

from pandas.io.formats.printing import pprint_thing
from pandas.plotting._matplotlib import converter
from pandas.plotting._matplotlib.core import LinePlot, MPLPlot
from pandas.plotting._matplotlib.tools import _flatten, _set_ticks_props, _subplots

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

converter._WARN = False # no warning for pandas plots
xrot = xrot or rot

fig, axes = _grouped_plot(
Expand Down Expand Up @@ -363,7 +361,6 @@ def hist_frame(
bins=10,
**kwds
):
converter._WARN = False # no warning for pandas plots
if by is not None:
axes = _grouped_hist(
data,
Expand Down
Loading