From 00ea10c158ac3d4087f6010f375084e9ddfcab99 Mon Sep 17 00:00:00 2001 From: Tom Augspurger Date: Thu, 23 Jul 2020 17:06:03 -0500 Subject: [PATCH] Matplotlib 3.3 compatibility fixups (#35393) --- ci/deps/azure-37-locale.yaml | 2 +- doc/source/whatsnew/v1.1.0.rst | 1 + pandas/plotting/_matplotlib/boxplot.py | 5 ++++ pandas/plotting/_matplotlib/compat.py | 1 + pandas/plotting/_matplotlib/converter.py | 34 +++++----------------- pandas/tests/plotting/test_converter.py | 6 ++-- pandas/tests/plotting/test_datetimelike.py | 14 +++++---- pandas/tests/plotting/test_frame.py | 1 + pandas/tests/plotting/test_series.py | 10 +++++-- 9 files changed, 34 insertions(+), 40 deletions(-) diff --git a/ci/deps/azure-37-locale.yaml b/ci/deps/azure-37-locale.yaml index 77aae791a47c1..4dbb6a5344976 100644 --- a/ci/deps/azure-37-locale.yaml +++ b/ci/deps/azure-37-locale.yaml @@ -18,7 +18,7 @@ dependencies: - ipython - jinja2 - lxml - - matplotlib <3.3.0 + - matplotlib>=3.3.0 - moto - nomkl - numexpr diff --git a/doc/source/whatsnew/v1.1.0.rst b/doc/source/whatsnew/v1.1.0.rst index 55e2a810e6fc3..b1ab54a608748 100644 --- a/doc/source/whatsnew/v1.1.0.rst +++ b/doc/source/whatsnew/v1.1.0.rst @@ -274,6 +274,7 @@ change, as ``fsspec`` will still bring in the same packages as before. Other enhancements ^^^^^^^^^^^^^^^^^^ +- Compatibility with matplotlib 3.3.0 (:issue:`34850`) - :meth:`IntegerArray.astype` now supports ``datetime64`` dtype (:issue:`32538`) - :class:`IntegerArray` now implements the ``sum`` operation (:issue:`33172`) - Added :class:`pandas.errors.InvalidIndexError` (:issue:`34570`). diff --git a/pandas/plotting/_matplotlib/boxplot.py b/pandas/plotting/_matplotlib/boxplot.py index 4b79bef41d025..53ef97bbe9a72 100644 --- a/pandas/plotting/_matplotlib/boxplot.py +++ b/pandas/plotting/_matplotlib/boxplot.py @@ -299,6 +299,11 @@ def plot_group(keys, values, ax): if fontsize is not None: ax.tick_params(axis="both", labelsize=fontsize) if kwds.get("vert", 1): + ticks = ax.get_xticks() + if len(ticks) != len(keys): + i, remainder = divmod(len(ticks), len(keys)) + assert remainder == 0, remainder + keys *= i ax.set_xticklabels(keys, rotation=rot) else: ax.set_yticklabels(keys, rotation=rot) diff --git a/pandas/plotting/_matplotlib/compat.py b/pandas/plotting/_matplotlib/compat.py index f2c5032112bc9..7f107f18eca25 100644 --- a/pandas/plotting/_matplotlib/compat.py +++ b/pandas/plotting/_matplotlib/compat.py @@ -21,3 +21,4 @@ def inner(): _mpl_ge_3_0_0 = _mpl_version("3.0.0", operator.ge) _mpl_ge_3_1_0 = _mpl_version("3.1.0", operator.ge) _mpl_ge_3_2_0 = _mpl_version("3.2.0", operator.ge) +_mpl_ge_3_3_0 = _mpl_version("3.3.0", operator.ge) diff --git a/pandas/plotting/_matplotlib/converter.py b/pandas/plotting/_matplotlib/converter.py index 05377e0c240b9..8f2080658e63e 100644 --- a/pandas/plotting/_matplotlib/converter.py +++ b/pandas/plotting/_matplotlib/converter.py @@ -16,7 +16,6 @@ from pandas._libs.tslibs.offsets import BaseOffset from pandas.core.dtypes.common import ( - is_datetime64_ns_dtype, is_float, is_float_dtype, is_integer, @@ -246,19 +245,6 @@ def get_datevalue(date, freq): raise ValueError(f"Unrecognizable date '{date}'") -def _dt_to_float_ordinal(dt): - """ - Convert :mod:`datetime` to the Gregorian date as UTC float days, - preserving hours, minutes, seconds and microseconds. Return value - is a :func:`float`. - """ - if isinstance(dt, (np.ndarray, Index, Series)) and is_datetime64_ns_dtype(dt): - base = dates.epoch2num(dt.asi8 / 1.0e9) - else: - base = dates.date2num(dt) - return base - - # Datetime Conversion class DatetimeConverter(dates.DateConverter): @staticmethod @@ -274,15 +260,11 @@ def convert(values, unit, axis): def _convert_1d(values, unit, axis): def try_parse(values): try: - return _dt_to_float_ordinal(tools.to_datetime(values)) + return dates.date2num(tools.to_datetime(values)) except Exception: return values - if isinstance(values, (datetime, pydt.date)): - return _dt_to_float_ordinal(values) - elif isinstance(values, np.datetime64): - return _dt_to_float_ordinal(Timestamp(values)) - elif isinstance(values, pydt.time): + if isinstance(values, (datetime, pydt.date, np.datetime64, pydt.time)): return dates.date2num(values) elif is_integer(values) or is_float(values): return values @@ -303,12 +285,10 @@ def try_parse(values): try: values = tools.to_datetime(values) - if isinstance(values, Index): - values = _dt_to_float_ordinal(values) - else: - values = [_dt_to_float_ordinal(x) for x in values] except Exception: - values = _dt_to_float_ordinal(values) + pass + + values = dates.date2num(values) return values @@ -411,8 +391,8 @@ def __call__(self): interval = self._get_interval() freq = f"{interval}L" tz = self.tz.tzname(None) - st = _from_ordinal(dates.date2num(dmin)) # strip tz - ed = _from_ordinal(dates.date2num(dmax)) + st = dmin.replace(tzinfo=None) + ed = dmin.replace(tzinfo=None) all_dates = date_range(start=st, end=ed, freq=freq, tz=tz).astype(object) try: diff --git a/pandas/tests/plotting/test_converter.py b/pandas/tests/plotting/test_converter.py index df2c9ecbd7a0a..b2eeb649276d5 100644 --- a/pandas/tests/plotting/test_converter.py +++ b/pandas/tests/plotting/test_converter.py @@ -27,6 +27,7 @@ pass pytest.importorskip("matplotlib.pyplot") +dates = pytest.importorskip("matplotlib.dates") def test_registry_mpl_resets(): @@ -146,7 +147,7 @@ def test_convert_accepts_unicode(self): def test_conversion(self): rs = self.dtc.convert(["2012-1-1"], None, None)[0] - xp = datetime(2012, 1, 1).toordinal() + xp = dates.date2num(datetime(2012, 1, 1)) assert rs == xp rs = self.dtc.convert("2012-1-1", None, None) @@ -155,9 +156,6 @@ def test_conversion(self): rs = self.dtc.convert(date(2012, 1, 1), None, None) assert rs == xp - rs = self.dtc.convert(datetime(2012, 1, 1).toordinal(), None, None) - assert rs == xp - rs = self.dtc.convert("2012-1-1", None, None) assert rs == xp diff --git a/pandas/tests/plotting/test_datetimelike.py b/pandas/tests/plotting/test_datetimelike.py index 201856669103a..ecf378d4fc04a 100644 --- a/pandas/tests/plotting/test_datetimelike.py +++ b/pandas/tests/plotting/test_datetimelike.py @@ -331,7 +331,7 @@ def test_freq_with_no_period_alias(self): bts = tm.makeTimeSeries(5).asfreq(freq) _, ax = self.plt.subplots() bts.plot(ax=ax) - assert ax.get_lines()[0].get_xydata()[0, 0] == bts.index[0].toordinal() + idx = ax.get_lines()[0].get_xdata() msg = "freq not specified and cannot be inferred" with pytest.raises(ValueError, match=msg): @@ -1279,6 +1279,8 @@ def test_mpl_nopandas(self): @pytest.mark.slow def test_irregular_ts_shared_ax_xlim(self): # GH 2960 + from pandas.plotting._matplotlib.converter import DatetimeConverter + ts = tm.makeTimeSeries()[:20] ts_irregular = ts[[1, 4, 5, 6, 8, 9, 10, 12, 13, 14, 15, 17, 18]] @@ -1289,8 +1291,8 @@ def test_irregular_ts_shared_ax_xlim(self): # check that axis limits are correct left, right = ax.get_xlim() - assert left <= ts_irregular.index.min().toordinal() - assert right >= ts_irregular.index.max().toordinal() + assert left <= DatetimeConverter.convert(ts_irregular.index.min(), "", ax) + assert right >= DatetimeConverter.convert(ts_irregular.index.max(), "", ax) @pytest.mark.slow def test_secondary_y_non_ts_xlim(self): @@ -1345,6 +1347,8 @@ def test_secondary_y_mixed_freq_ts_xlim(self): @pytest.mark.slow def test_secondary_y_irregular_ts_xlim(self): # GH 3490 - irregular-timeseries with secondary y + from pandas.plotting._matplotlib.converter import DatetimeConverter + ts = tm.makeTimeSeries()[:20] ts_irregular = ts[[1, 4, 5, 6, 8, 9, 10, 12, 13, 14, 15, 17, 18]] @@ -1356,8 +1360,8 @@ def test_secondary_y_irregular_ts_xlim(self): ts_irregular[:5].plot(ax=ax) left, right = ax.get_xlim() - assert left <= ts_irregular.index.min().toordinal() - assert right >= ts_irregular.index.max().toordinal() + assert left <= DatetimeConverter.convert(ts_irregular.index.min(), "", ax) + assert right >= DatetimeConverter.convert(ts_irregular.index.max(), "", ax) def test_plot_outofbounds_datetime(self): # 2579 - checking this does not raise diff --git a/pandas/tests/plotting/test_frame.py b/pandas/tests/plotting/test_frame.py index 3d85e79b15c4c..317a994bd9a32 100644 --- a/pandas/tests/plotting/test_frame.py +++ b/pandas/tests/plotting/test_frame.py @@ -1563,6 +1563,7 @@ def test_boxplot(self): ax.xaxis.get_ticklocs(), np.arange(1, len(numeric_cols) + 1) ) assert len(ax.lines) == self.bp_n_objects * len(numeric_cols) + tm.close() axes = series.plot.box(rot=40) self._check_ticks_props(axes, xrot=40, yrot=0) diff --git a/pandas/tests/plotting/test_series.py b/pandas/tests/plotting/test_series.py index 316ca6ce91af7..151bb3bed7207 100644 --- a/pandas/tests/plotting/test_series.py +++ b/pandas/tests/plotting/test_series.py @@ -274,12 +274,14 @@ def test_rotation(self): self._check_ticks_props(axes, xrot=30) def test_irregular_datetime(self): + from pandas.plotting._matplotlib.converter import DatetimeConverter + rng = date_range("1/1/2000", "3/1/2000") rng = rng[[0, 1, 2, 3, 5, 9, 10, 11, 12]] ser = Series(randn(len(rng)), rng) _, ax = self.plt.subplots() ax = ser.plot(ax=ax) - xp = datetime(1999, 1, 1).toordinal() + xp = DatetimeConverter.convert(datetime(1999, 1, 1), "", ax) ax.set_xlim("1/1/1999", "1/1/2001") assert xp == ax.get_xlim()[0] @@ -684,11 +686,13 @@ def test_kind_both_ways(self): kinds = ( plotting.PlotAccessor._common_kinds + plotting.PlotAccessor._series_kinds ) - _, ax = self.plt.subplots() for kind in kinds: - + _, ax = self.plt.subplots() s.plot(kind=kind, ax=ax) + self.plt.close() + _, ax = self.plt.subplots() getattr(s.plot, kind)() + self.plt.close() @pytest.mark.slow def test_invalid_plot_data(self):