diff --git a/doc/source/whatsnew/v1.2.0.rst b/doc/source/whatsnew/v1.2.0.rst index b93f98ab0274f..bc5229d4b4296 100644 --- a/doc/source/whatsnew/v1.2.0.rst +++ b/doc/source/whatsnew/v1.2.0.rst @@ -668,6 +668,7 @@ Plotting - Bug in :meth:`DataFrame.plot` was rotating xticklabels when ``subplots=True``, even if the x-axis wasn't an irregular time series (:issue:`29460`) - Bug in :meth:`DataFrame.plot` where a marker letter in the ``style`` keyword sometimes causes a ``ValueError`` (:issue:`21003`) +- Bug in :func:`DataFrame.plot.bar` and :func:`Series.plot.bar`. Ticks position were assigned by value order instead of using the actual value for numeric, or a smart ordering for string. (:issue:`26186` and :issue:`11465`) - Twinned axes were losing their tick labels which should only happen to all but the last row or column of 'externally' shared axes (:issue:`33819`) - Bug in :meth:`Series.plot` and :meth:`DataFrame.plot` was throwing :exc:`ValueError` with a :class:`Series` or :class:`DataFrame` indexed by a :class:`TimedeltaIndex` with a fixed frequency when x-axis lower limit was greater than upper limit (:issue:`37454`) diff --git a/pandas/plotting/_matplotlib/core.py b/pandas/plotting/_matplotlib/core.py index 97fa3f11e9dfb..c01cfb9a8b487 100644 --- a/pandas/plotting/_matplotlib/core.py +++ b/pandas/plotting/_matplotlib/core.py @@ -1359,7 +1359,6 @@ def __init__(self, data, **kwargs): self.bar_width = kwargs.pop("width", 0.5) pos = kwargs.pop("position", 0.5) kwargs.setdefault("align", "center") - self.tick_pos = np.arange(len(data)) self.bottom = kwargs.pop("bottom", 0) self.left = kwargs.pop("left", 0) @@ -1382,7 +1381,16 @@ def __init__(self, data, **kwargs): self.tickoffset = self.bar_width * pos self.lim_offset = 0 - self.ax_pos = self.tick_pos - self.tickoffset + if isinstance(self.data.index, ABCMultiIndex): + if kwargs["ax"] is not None and kwargs["ax"].has_data(): + warnings.warn( + "Redrawing a bar plot with a MultiIndex is not supported " + + "and may lead to inconsistent label positions.", + UserWarning, + ) + self.ax_index = np.arange(len(data)) + else: + self.ax_index = self.data.index def _args_adjust(self): if is_list_like(self.bottom): @@ -1409,6 +1417,15 @@ def _make_plot(self): for i, (label, y) in enumerate(self._iter_data(fillna=0)): ax = self._get_ax(i) + + if self.orientation == "vertical": + ax.xaxis.update_units(self.ax_index) + self.tick_pos = ax.convert_xunits(self.ax_index).astype(np.int) + elif self.orientation == "horizontal": + ax.yaxis.update_units(self.ax_index) + self.tick_pos = ax.convert_yunits(self.ax_index).astype(np.int) + self.ax_pos = self.tick_pos - self.tickoffset + kwds = self.kwds.copy() if self._is_series: kwds["color"] = colors @@ -1480,8 +1497,8 @@ def _post_plot_logic(self, ax: "Axes", data): str_index = [pprint_thing(key) for key in range(data.shape[0])] name = self._get_index_name() - s_edge = self.ax_pos[0] - 0.25 + self.lim_offset - e_edge = self.ax_pos[-1] + 0.25 + self.bar_width + self.lim_offset + s_edge = self.ax_pos.min() - 0.25 + self.lim_offset + e_edge = self.ax_pos.max() + 0.25 + self.bar_width + self.lim_offset self._decorate_ticks(ax, name, str_index, s_edge, e_edge) diff --git a/pandas/tests/plotting/frame/test_frame.py b/pandas/tests/plotting/frame/test_frame.py index 56ac7a477adbb..77a4c4a8faf5e 100644 --- a/pandas/tests/plotting/frame/test_frame.py +++ b/pandas/tests/plotting/frame/test_frame.py @@ -2190,6 +2190,81 @@ def test_xlabel_ylabel_dataframe_plane_plot(self, kind, xlabel, ylabel): assert ax.get_xlabel() == (xcol if xlabel is None else xlabel) assert ax.get_ylabel() == (ycol if ylabel is None else ylabel) + @pytest.mark.slow + @pytest.mark.parametrize("method", ["bar", "barh"]) + def test_bar_ticklabel_consistence(self, method): + # Draw two consecutiv bar plot with consistent ticklabels + # The labels positions should not move between two drawing on the same axis + # GH: 26186 + def get_main_axis(ax): + if method == "barh": + return ax.yaxis + elif method == "bar": + return ax.xaxis + + # Plot the first bar plot + data = {"A": 0, "B": 3, "C": -4} + df = DataFrame.from_dict(data, orient="index", columns=["Value"]) + ax = getattr(df.plot, method)() + ax.get_figure().canvas.draw() + + # Retrieve the label positions for the first drawing + xticklabels = [t.get_text() for t in get_main_axis(ax).get_ticklabels()] + label_positions_1 = dict(zip(xticklabels, get_main_axis(ax).get_ticklocs())) + + # Modify the dataframe order and values and plot on same axis + df = df.sort_values("Value") * -2 + ax = getattr(df.plot, method)(ax=ax, color="red") + ax.get_figure().canvas.draw() + + # Retrieve the label positions for the second drawing + xticklabels = [t.get_text() for t in get_main_axis(ax).get_ticklabels()] + label_positions_2 = dict(zip(xticklabels, get_main_axis(ax).get_ticklocs())) + + # Assert that the label positions did not change between the plotting + assert label_positions_1 == label_positions_2 + + def test_bar_numeric(self): + # Bar plot with numeric index have tick location values equal to index + # values + # GH: 11465 + df = DataFrame(np.random.rand(10), index=np.arange(10, 20)) + ax = df.plot.bar() + ticklocs = ax.xaxis.get_ticklocs() + expected = np.arange(10, 20, dtype=np.int64) + tm.assert_numpy_array_equal(ticklocs, expected) + + def test_bar_multiindex(self): + # Test from pandas/doc/source/user_guide/visualization.rst + # at section Plotting With Error Bars + # Related to issue GH: 26186 + + ix3 = pd.MultiIndex.from_arrays( + [ + ["a", "a", "a", "a", "b", "b", "b", "b"], + ["foo", "foo", "bar", "bar", "foo", "foo", "bar", "bar"], + ], + names=["letter", "word"], + ) + + df3 = DataFrame( + {"data1": [3, 2, 4, 3, 2, 4, 3, 2], "data2": [6, 5, 7, 5, 4, 5, 6, 5]}, + index=ix3, + ) + + # Group by index labels and take the means and standard deviations + # for each group + gp3 = df3.groupby(level=("letter", "word")) + means = gp3.mean() + errors = gp3.std() + + # No assertion we just ensure that we can plot a MultiIndex bar plot + # and are getting a UserWarning if redrawing + with tm.assert_produces_warning(None): + ax = means.plot.bar(yerr=errors, capsize=4) + with tm.assert_produces_warning(UserWarning): + means.plot.bar(yerr=errors, capsize=4, ax=ax) + def _generate_4_axes_via_gridspec(): import matplotlib as mpl