diff --git a/doc/source/whatsnew/v2.2.0.rst b/doc/source/whatsnew/v2.2.0.rst index d9ab0452c8334..3d228cd2bc609 100644 --- a/doc/source/whatsnew/v2.2.0.rst +++ b/doc/source/whatsnew/v2.2.0.rst @@ -882,6 +882,7 @@ Plotting ^^^^^^^^ - Bug in :meth:`DataFrame.plot.box` with ``vert=False`` and a Matplotlib ``Axes`` created with ``sharey=True`` (:issue:`54941`) - Bug in :meth:`DataFrame.plot.scatter` discarding string columns (:issue:`56142`) +- Bug in :meth:`DataFrame.plot` where bar and line plots are not aligned on the x-axis (:issue:`56611`) - Bug in :meth:`Series.plot` when reusing an ``ax`` object failing to raise when a ``how`` keyword is passed (:issue:`55953`) Groupby/resample/rolling diff --git a/pandas/plotting/_matplotlib/core.py b/pandas/plotting/_matplotlib/core.py index 6fa75ba5fb12d..56f01d1b1ee85 100644 --- a/pandas/plotting/_matplotlib/core.py +++ b/pandas/plotting/_matplotlib/core.py @@ -1804,6 +1804,13 @@ def _kind(self) -> Literal["bar", "barh"]: def orientation(self) -> PlottingOrientation: return "vertical" + @final + def _set_tick_pos(self, data) -> np.ndarray: + if self._is_series and is_integer_dtype(data.index): + return np.array(data.index) + else: + return np.arange(len(data)) + def __init__( self, data, @@ -1822,7 +1829,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) @@ -1835,6 +1841,8 @@ def __init__( MPLPlot.__init__(self, data, **kwargs) + self.tick_pos = self._set_tick_pos(data) + @cache_readonly def ax_pos(self) -> np.ndarray: return self.tick_pos - self.tickoffset diff --git a/pandas/tests/plotting/test_series.py b/pandas/tests/plotting/test_series.py index 2b2f2f3b84307..2f63ec137f2b2 100644 --- a/pandas/tests/plotting/test_series.py +++ b/pandas/tests/plotting/test_series.py @@ -919,10 +919,7 @@ def test_plot_order(self, data, index): ax = ser.plot(kind="bar") expected = ser.tolist() - result = [ - patch.get_bbox().ymax - for patch in sorted(ax.patches, key=lambda patch: patch.get_bbox().xmax) - ] + result = [patch.get_bbox().ymax for patch in ax.patches] assert expected == result def test_style_single_ok(self): @@ -977,6 +974,31 @@ def test_series_none_color(self): expected = _unpack_cycler(mpl.pyplot.rcParams)[:1] _check_colors(ax.get_lines(), linecolors=expected) + def test_bar_plot_x_axis(self): + df = DataFrame( + { + "bars": {-1: 0.5, 0: 1.0, 1: 3.0, 2: 3.5, 3: 1.5}, + "pct": {-1: 1.0, 0: 2.0, 1: 3.0, 2: 4.0, 3: 8.0}, + } + ) + ax_bar = df["bars"].plot(kind="bar") + df["pct"].plot(kind="line") + actual_bar_x = [ax.get_x() + ax.get_width() / 2.0 for ax in ax_bar.patches] + expected_x = [-1, 0, 1, 2, 3] + assert actual_bar_x == expected_x + + def test_non_numeric_bar_plot_x_axis(self): + df = DataFrame( + { + "bars": {"a": 0.5, "b": 1.0}, + "pct": {"a": 4.0, "b": 2.0}, + } + ) + ax_bar = df["bars"].plot(kind="bar") + actual_bar_x = [ax.get_x() + ax.get_width() / 2.0 for ax in ax_bar.patches] + expected_x = [0, 1] + assert actual_bar_x == expected_x + @pytest.mark.slow def test_plot_no_warning(self, ts): # GH 55138