diff --git a/doc/source/whatsnew/v3.0.0.rst b/doc/source/whatsnew/v3.0.0.rst index 184ca581902ee..5302912da31fa 100644 --- a/doc/source/whatsnew/v3.0.0.rst +++ b/doc/source/whatsnew/v3.0.0.rst @@ -763,6 +763,7 @@ Plotting - Bug in :meth:`DataFrame.plot.bar` with ``stacked=True`` where labels on stacked bars with zero-height segments were incorrectly positioned at the base instead of the label position of the previous segment (:issue:`59429`) - Bug in :meth:`DataFrame.plot.line` raising ``ValueError`` when set both color and a ``dict`` style (:issue:`59461`) - Bug in :meth:`DataFrame.plot` that causes a shift to the right when the frequency multiplier is greater than one. (:issue:`57587`) +- Bug in :meth:`Series.plot` preventing a line and bar from being aligned on the same plot (:issue:`61161`) - Bug in :meth:`Series.plot` preventing a line and scatter plot from being aligned (:issue:`61005`) - Bug in :meth:`Series.plot` with ``kind="pie"`` with :class:`ArrowDtype` (:issue:`59192`) diff --git a/pandas/plotting/_matplotlib/converter.py b/pandas/plotting/_matplotlib/converter.py index 4c00049075d03..774062e0f0412 100644 --- a/pandas/plotting/_matplotlib/converter.py +++ b/pandas/plotting/_matplotlib/converter.py @@ -225,16 +225,20 @@ def __call__(self, x, pos: int | None = 0) -> str: class PeriodConverter(mdates.DateConverter): @staticmethod def convert(values, units, axis): + if not hasattr(axis, "freq"): + raise TypeError("Axis must have `freq` set to convert to Periods") + return PeriodConverter.convert_from_freq(values, axis.freq) + + @staticmethod + def convert_from_freq(values, freq): if is_nested_list_like(values): - values = [PeriodConverter._convert_1d(v, units, axis) for v in values] + values = [PeriodConverter._convert_1d(v, freq) for v in values] else: - values = PeriodConverter._convert_1d(values, units, axis) + values = PeriodConverter._convert_1d(values, freq) return values @staticmethod - def _convert_1d(values, units, axis): - if not hasattr(axis, "freq"): - raise TypeError("Axis must have `freq` set to convert to Periods") + def _convert_1d(values, freq): valid_types = (str, datetime, Period, pydt.date, pydt.time, np.datetime64) with warnings.catch_warnings(): warnings.filterwarnings( @@ -248,17 +252,17 @@ def _convert_1d(values, units, axis): or is_integer(values) or is_float(values) ): - return get_datevalue(values, axis.freq) + return get_datevalue(values, freq) elif isinstance(values, PeriodIndex): - return values.asfreq(axis.freq).asi8 + return values.asfreq(freq).asi8 elif isinstance(values, Index): - return values.map(lambda x: get_datevalue(x, axis.freq)) + return values.map(lambda x: get_datevalue(x, freq)) elif lib.infer_dtype(values, skipna=False) == "period": # https://github.com/pandas-dev/pandas/issues/24304 # convert ndarray[period] -> PeriodIndex - return PeriodIndex(values, freq=axis.freq).asi8 + return PeriodIndex(values, freq=freq).asi8 elif isinstance(values, (list, tuple, np.ndarray, Index)): - return [get_datevalue(x, axis.freq) for x in values] + return [get_datevalue(x, freq) for x in values] return values diff --git a/pandas/plotting/_matplotlib/core.py b/pandas/plotting/_matplotlib/core.py index 24aa848de1b4c..2cf90a06cdc50 100644 --- a/pandas/plotting/_matplotlib/core.py +++ b/pandas/plotting/_matplotlib/core.py @@ -59,7 +59,10 @@ from pandas.io.formats.printing import pprint_thing from pandas.plotting._matplotlib import tools -from pandas.plotting._matplotlib.converter import register_pandas_matplotlib_converters +from pandas.plotting._matplotlib.converter import ( + PeriodConverter, + register_pandas_matplotlib_converters, +) from pandas.plotting._matplotlib.groupby import reconstruct_data_with_by from pandas.plotting._matplotlib.misc import unpack_single_str_list from pandas.plotting._matplotlib.style import get_standard_colors @@ -1858,7 +1861,6 @@ def __init__( self.bar_width = width self._align = align self._position = position - self.tick_pos = np.arange(len(data)) if is_list_like(bottom): bottom = np.array(bottom) @@ -1871,6 +1873,16 @@ def __init__( MPLPlot.__init__(self, data, **kwargs) + if self._is_ts_plot(): + self.tick_pos = np.array( + PeriodConverter.convert_from_freq( + self._get_xticks(), + data.index.freq, + ) + ) + else: + self.tick_pos = np.arange(len(data)) + @cache_readonly def ax_pos(self) -> np.ndarray: return self.tick_pos - self.tickoffset @@ -1900,6 +1912,7 @@ def lim_offset(self): # error: Signature of "_plot" incompatible with supertype "MPLPlot" @classmethod + @register_pandas_matplotlib_converters def _plot( # type: ignore[override] cls, ax: Axes, diff --git a/pandas/tests/plotting/test_series.py b/pandas/tests/plotting/test_series.py index c3b0219971446..da6c4db550a4e 100644 --- a/pandas/tests/plotting/test_series.py +++ b/pandas/tests/plotting/test_series.py @@ -971,3 +971,17 @@ def test_secondary_y_subplot_axis_labels(self): s1.plot(ax=ax2) assert len(ax.xaxis.get_minor_ticks()) == 0 assert len(ax.get_xticklabels()) > 0 + + def test_bar_line_plot(self): + """ + Test that bar and line plots with the same x values are superposed + """ + # GH61161 + index = period_range("2023", periods=3, freq="Y") + s = Series([1, 2, 3], index=index) + ax = plt.subplot() + s.plot(kind="bar", ax=ax) + bar_xticks = ax.get_xticks().tolist() + s.plot(kind="line", ax=ax, color="r") + line_xticks = ax.get_xticks()[: len(s)].tolist() + assert line_xticks == bar_xticks