diff --git a/pandas/plotting/_matplotlib/timeseries.py b/pandas/plotting/_matplotlib/timeseries.py index 98ac394f4e51e..c558e293973e8 100644 --- a/pandas/plotting/_matplotlib/timeseries.py +++ b/pandas/plotting/_matplotlib/timeseries.py @@ -18,6 +18,8 @@ OFFSET_TO_PERIOD_FREQSTR, FreqGroup, ) +from pandas.errors import Pandas4Warning +from pandas.util._exceptions import find_stack_level from pandas.core.dtypes.generic import ( ABCDatetimeIndex, @@ -74,6 +76,13 @@ def maybe_resample(series: Series, ax: Axes, kwargs: dict[str, Any]): series = series.to_period(freq=freq) if ax_freq is not None and freq != ax_freq: + warnings.warn( + "Plotting with mixed-frequency series is deprecated and " + "will raise in a future version. Align series frequencies " + "before plotting instead.", + Pandas4Warning, + stacklevel=find_stack_level(), + ) if is_superperiod(freq, ax_freq): # upsample input series = series.copy() # error: "Index" has no attribute "asfreq" @@ -293,7 +302,13 @@ def maybe_convert_index(ax: Axes, data: NDFrameT) -> NDFrameT: if isinstance(data.index, ABCDatetimeIndex): data = data.tz_localize(None).to_period(freq=freq_str) elif isinstance(data.index, ABCPeriodIndex): - data.index = data.index.asfreq(freq=freq_str, how="start") + # This will convert e.g. freq="60min" to freq="min", but will + # retain type(freq). It is not clear to @jbrockmendel why + # this is necessary as of 2025-09-24, but 18 tests fail + # without it. + new_freq = to_offset(freq_str, is_period=True) + assert type(new_freq) is type(data.index.freq) + data.index = data.index.asfreq(freq=new_freq, how="start") return data diff --git a/pandas/tests/plotting/test_datetimelike.py b/pandas/tests/plotting/test_datetimelike.py index 46894fbaa6d5b..042981d6c8e50 100644 --- a/pandas/tests/plotting/test_datetimelike.py +++ b/pandas/tests/plotting/test_datetimelike.py @@ -15,6 +15,7 @@ BaseOffset, to_offset, ) +from pandas.errors import Pandas4Warning from pandas.core.dtypes.dtypes import PeriodDtype @@ -628,7 +629,10 @@ def test_gap_upsample(self): idxh = date_range(low.index[0], low.index[-1], freq="12h") s = Series(np.random.default_rng(2).standard_normal(len(idxh)), idxh) - s.plot(secondary_y=True) + + msg = "Plotting with mixed-frequency series is deprecated" + with tm.assert_produces_warning(Pandas4Warning, match=msg): + s.plot(secondary_y=True) lines = ax.get_lines() assert len(lines) == 1 assert len(ax.right_ax.get_lines()) == 1 @@ -821,7 +825,9 @@ def test_mixed_freq_hf_first(self): low = Series(np.random.default_rng(2).standard_normal(len(idxl)), idxl) _, ax = mpl.pyplot.subplots() high.plot(ax=ax) - low.plot(ax=ax) + msg = "Plotting with mixed-frequency series is deprecated" + with tm.assert_produces_warning(Pandas4Warning, match=msg): + low.plot(ax=ax) for line in ax.get_lines(): assert PeriodIndex(data=line.get_xdata()).freq == "D" @@ -834,7 +840,9 @@ def test_mixed_freq_alignment(self): _, ax = mpl.pyplot.subplots() ax = ts.plot(ax=ax) - ts2.plot(style="r", ax=ax) + msg = "Plotting with mixed-frequency series is deprecated" + with tm.assert_produces_warning(Pandas4Warning, match=msg): + ts2.plot(style="r", ax=ax) assert ax.lines[0].get_xdata()[0] == ax.lines[1].get_xdata()[0] @@ -845,7 +853,9 @@ def test_mixed_freq_lf_first(self): low = Series(np.random.default_rng(2).standard_normal(len(idxl)), idxl) _, ax = mpl.pyplot.subplots() low.plot(legend=True, ax=ax) - high.plot(legend=True, ax=ax) + msg = "Plotting with mixed-frequency series is deprecated" + with tm.assert_produces_warning(Pandas4Warning, match=msg): + high.plot(legend=True, ax=ax) for line in ax.get_lines(): assert PeriodIndex(data=line.get_xdata()).freq == "D" leg = ax.get_legend() @@ -859,7 +869,9 @@ def test_mixed_freq_lf_first_hourly(self): low = Series(np.random.default_rng(2).standard_normal(len(idxl)), idxl) _, ax = mpl.pyplot.subplots() low.plot(ax=ax) - high.plot(ax=ax) + msg = "Plotting with mixed-frequency series is deprecated" + with tm.assert_produces_warning(Pandas4Warning, match=msg): + high.plot(ax=ax) for line in ax.get_lines(): assert PeriodIndex(data=line.get_xdata()).freq == "min" @@ -952,7 +964,9 @@ def test_to_weekly_resampling(self): low = Series(np.random.default_rng(2).standard_normal(len(idxl)), idxl) _, ax = mpl.pyplot.subplots() high.plot(ax=ax) - low.plot(ax=ax) + msg = "Plotting with mixed-frequency series is deprecated" + with tm.assert_produces_warning(Pandas4Warning, match=msg): + low.plot(ax=ax) for line in ax.get_lines(): assert PeriodIndex(data=line.get_xdata()).freq == idxh.freq @@ -963,7 +977,9 @@ def test_from_weekly_resampling(self): low = Series(np.random.default_rng(2).standard_normal(len(idxl)), idxl) _, ax = mpl.pyplot.subplots() low.plot(ax=ax) - high.plot(ax=ax) + msg = "Plotting with mixed-frequency series is deprecated" + with tm.assert_produces_warning(Pandas4Warning, match=msg): + high.plot(ax=ax) expected_h = idxh.to_period().asi8.astype(np.float64) expected_l = np.array( @@ -995,7 +1011,9 @@ def test_from_resampling_area_line_mixed(self, kind1, kind2): _, ax = mpl.pyplot.subplots() low.plot(kind=kind1, stacked=True, ax=ax) - high.plot(kind=kind2, stacked=True, ax=ax) + msg = "Plotting with mixed-frequency series is deprecated" + with tm.assert_produces_warning(Pandas4Warning, match=msg): + high.plot(kind=kind2, stacked=True, ax=ax) # check low dataframe result expected_x = np.array( @@ -1050,7 +1068,9 @@ def test_from_resampling_area_line_mixed_high_to_low(self, kind1, kind2): ) _, ax = mpl.pyplot.subplots() high.plot(kind=kind1, stacked=True, ax=ax) - low.plot(kind=kind2, stacked=True, ax=ax) + msg = "Plotting with mixed-frequency series is deprecated" + with tm.assert_produces_warning(Pandas4Warning, match=msg): + low.plot(kind=kind2, stacked=True, ax=ax) # check high dataframe result expected_x = idxh.to_period().asi8.astype(np.float64) @@ -1097,7 +1117,9 @@ def test_mixed_freq_second_millisecond(self): # high to low _, ax = mpl.pyplot.subplots() high.plot(ax=ax) - low.plot(ax=ax) + msg = "Plotting with mixed-frequency series is deprecated" + with tm.assert_produces_warning(Pandas4Warning, match=msg): + low.plot(ax=ax) assert len(ax.get_lines()) == 2 for line in ax.get_lines(): assert PeriodIndex(data=line.get_xdata()).freq == "ms" @@ -1111,7 +1133,9 @@ def test_mixed_freq_second_millisecond_low_to_high(self): # low to high _, ax = mpl.pyplot.subplots() low.plot(ax=ax) - high.plot(ax=ax) + msg = "Plotting with mixed-frequency series is deprecated" + with tm.assert_produces_warning(Pandas4Warning, match=msg): + high.plot(ax=ax) assert len(ax.get_lines()) == 2 for line in ax.get_lines(): assert PeriodIndex(data=line.get_xdata()).freq == "ms" @@ -1248,7 +1272,9 @@ def test_secondary_upsample(self): low = Series(np.random.default_rng(2).standard_normal(len(idxl)), idxl) _, ax = mpl.pyplot.subplots() low.plot(ax=ax) - ax = high.plot(secondary_y=True, ax=ax) + msg = "Plotting with mixed-frequency series is deprecated" + with tm.assert_produces_warning(Pandas4Warning, match=msg): + ax = high.plot(secondary_y=True, ax=ax) for line in ax.get_lines(): assert PeriodIndex(line.get_xdata()).freq == "D" assert hasattr(ax, "left_ax") @@ -1483,7 +1509,11 @@ def test_secondary_y_mixed_freq_ts_xlim(self): _, ax = mpl.pyplot.subplots() ts.plot(ax=ax) left_before, right_before = ax.get_xlim() - ts.resample("D").mean().plot(secondary_y=True, ax=ax) + + rs = ts.resample("D").mean() + msg = "Plotting with mixed-frequency series is deprecated" + with tm.assert_produces_warning(Pandas4Warning, match=msg): + rs.plot(secondary_y=True, ax=ax) left_after, right_after = ax.get_xlim() # a downsample should not have changed either limit