From f98033b73b41c1d516d2b7925b4dc043e6b0fa96 Mon Sep 17 00:00:00 2001 From: Francesco Bianco Morghet <36233478+francibm97@users.noreply.github.com> Date: Tue, 13 Oct 2020 21:09:02 +0200 Subject: [PATCH 1/4] BUG: Fixes #36918 boxplots with matplotlib --- pandas/plotting/_matplotlib/boxplot.py | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/pandas/plotting/_matplotlib/boxplot.py b/pandas/plotting/_matplotlib/boxplot.py index 8ceba22b1f7a4..bb0a8d0316ad5 100644 --- a/pandas/plotting/_matplotlib/boxplot.py +++ b/pandas/plotting/_matplotlib/boxplot.py @@ -246,6 +246,7 @@ def boxplot( ): import matplotlib.pyplot as plt + from matplotlib.ticker import FixedFormatter # validate return_type: if return_type not in BoxPlot._valid_return_types: @@ -302,15 +303,13 @@ def plot_group(keys, values, ax: "Axes"): bp = ax.boxplot(values, **kwds) 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) + axis = ax.xaxis else: - ax.set_yticklabels(keys, rotation=rot) + axis = ax.yaxis + axis.set_major_formatter(FixedFormatter(keys)) + ax.tick_params(axis=axis.axis_name, which="major", rotation=rot) maybe_color_bp(bp, **kwds) # Return axes in multiplot case, maybe revisit later # 985 From dd5df89fa45ea74bb533791a859cdd83284cf060 Mon Sep 17 00:00:00 2001 From: Francesco Bianco Morghet <36233478+francibm97@users.noreply.github.com> Date: Thu, 15 Oct 2020 21:22:26 +0200 Subject: [PATCH 2/4] Added test --- pandas/tests/plotting/test_boxplot_method.py | 34 ++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/pandas/tests/plotting/test_boxplot_method.py b/pandas/tests/plotting/test_boxplot_method.py index 0a096acc9fa6d..bc6da1241fbf3 100644 --- a/pandas/tests/plotting/test_boxplot_method.py +++ b/pandas/tests/plotting/test_boxplot_method.py @@ -218,6 +218,40 @@ def test_specified_props_kwd(self, props, expected): assert result[expected][0].get_color() == "C1" + @pytest.mark.parametrize("vert", [(True), (False)]) + def test_boxplot_shared_axis(self, vert): + # GH 37107 + df1 = DataFrame(np.random.random((100, 5)), columns=["A", "B", "C", "D", "E"]) + df2 = DataFrame(np.random.random((100, 5)), columns=["A", "B", "C", "D", "E"]) + + # Two rows if shared axis is y, two rows if shared axis is y. + # This is done so that the shared axes are actually separated + # and get_ticklabels returns a non empty list on each ax object + nrows, ncols, sharex, sharey = ( + (1, 2, True, False) if vert else (2, 1, False, True) + ) + fig, axes = self.plt.subplots( + nrows=nrows, ncols=ncols, sharex=sharex, sharey=sharey + ) + df1.boxplot(ax=axes[0], vert=vert, fontsize=10, rot=10) + df2.boxplot(ax=axes[1], vert=vert, fontsize=10, rot=10) + + # In order for the ticklabels to be placed, the plot has to be drawn + fig.canvas.draw() + + for ax in axes: + axis = ax.xaxis if vert else ax.yaxis + labels = [x.get_text() for x in axis.get_ticklabels(which="major")] + if vert: + self._check_ticks_props(ax, xlabelsize=10, xrot=10) + else: + self._check_ticks_props(ax, ylabelsize=10, yrot=10) + + # Matplotlib returns 10 ticklabels, 5 of which are empty + assert len(labels) % 5 == 0 + assert len(labels) // (len(labels) // 5) == 5 + assert labels[:5] == ["A", "B", "C", "D", "E"] + @td.skip_if_no_mpl class TestDataFrameGroupByPlots(TestPlotBase): From bc1d31e3219dfa09ac443ccb8311dfef7b554556 Mon Sep 17 00:00:00 2001 From: Francesco Bianco Morghet <36233478+francibm97@users.noreply.github.com> Date: Thu, 15 Oct 2020 21:22:58 +0200 Subject: [PATCH 3/4] Added whatsnew --- doc/source/whatsnew/v1.2.0.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/doc/source/whatsnew/v1.2.0.rst b/doc/source/whatsnew/v1.2.0.rst index f77a55b95d567..edb85fb5ac58c 100644 --- a/doc/source/whatsnew/v1.2.0.rst +++ b/doc/source/whatsnew/v1.2.0.rst @@ -437,6 +437,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`) - 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:`DataFrame.boxplot` was raising ``ValueError`` when plotting with ``vert=True`` on a subplot with shared axes (:issue:`36918`) Groupby/resample/rolling ^^^^^^^^^^^^^^^^^^^^^^^^ From 1bbe662a91fafcce1f25552133cbb9806cf71dd5 Mon Sep 17 00:00:00 2001 From: Francesco Bianco Morghet <36233478+francibm97@users.noreply.github.com> Date: Fri, 16 Oct 2020 19:39:57 +0200 Subject: [PATCH 4/4] Update fix --- doc/source/whatsnew/v1.2.0.rst | 2 +- pandas/plotting/_matplotlib/boxplot.py | 4 +- pandas/tests/plotting/test_boxplot_method.py | 42 +++++++------------- 3 files changed, 19 insertions(+), 29 deletions(-) diff --git a/doc/source/whatsnew/v1.2.0.rst b/doc/source/whatsnew/v1.2.0.rst index edb85fb5ac58c..62c90e32fad2d 100644 --- a/doc/source/whatsnew/v1.2.0.rst +++ b/doc/source/whatsnew/v1.2.0.rst @@ -437,7 +437,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`) - 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:`DataFrame.boxplot` was raising ``ValueError`` when plotting with ``vert=True`` on a subplot with shared axes (:issue:`36918`) +- Bug in :meth:`DataFrame.boxplot` was raising ``ValueError`` when plotting with ``vert=False`` on a subplot with shared axes (:issue:`36918`) Groupby/resample/rolling ^^^^^^^^^^^^^^^^^^^^^^^^ diff --git a/pandas/plotting/_matplotlib/boxplot.py b/pandas/plotting/_matplotlib/boxplot.py index bb0a8d0316ad5..906413bd6c764 100644 --- a/pandas/plotting/_matplotlib/boxplot.py +++ b/pandas/plotting/_matplotlib/boxplot.py @@ -246,7 +246,7 @@ def boxplot( ): import matplotlib.pyplot as plt - from matplotlib.ticker import FixedFormatter + from matplotlib.ticker import FixedFormatter, FixedLocator # validate return_type: if return_type not in BoxPlot._valid_return_types: @@ -308,6 +308,8 @@ def plot_group(keys, values, ax: "Axes"): axis = ax.xaxis else: axis = ax.yaxis + positions = kwds.get("positions", list(range(1, 1 + len(values)))) + axis.set_major_locator(FixedLocator(positions)) axis.set_major_formatter(FixedFormatter(keys)) ax.tick_params(axis=axis.axis_name, which="major", rotation=rot) maybe_color_bp(bp, **kwds) diff --git a/pandas/tests/plotting/test_boxplot_method.py b/pandas/tests/plotting/test_boxplot_method.py index bc6da1241fbf3..85f52675769a1 100644 --- a/pandas/tests/plotting/test_boxplot_method.py +++ b/pandas/tests/plotting/test_boxplot_method.py @@ -219,38 +219,26 @@ def test_specified_props_kwd(self, props, expected): assert result[expected][0].get_color() == "C1" @pytest.mark.parametrize("vert", [(True), (False)]) - def test_boxplot_shared_axis(self, vert): + def test_boxplot_multiple_times_same_axis(self, vert): # GH 37107 - df1 = DataFrame(np.random.random((100, 5)), columns=["A", "B", "C", "D", "E"]) - df2 = DataFrame(np.random.random((100, 5)), columns=["A", "B", "C", "D", "E"]) - - # Two rows if shared axis is y, two rows if shared axis is y. - # This is done so that the shared axes are actually separated - # and get_ticklabels returns a non empty list on each ax object - nrows, ncols, sharex, sharey = ( - (1, 2, True, False) if vert else (2, 1, False, True) - ) - fig, axes = self.plt.subplots( - nrows=nrows, ncols=ncols, sharex=sharex, sharey=sharey - ) - df1.boxplot(ax=axes[0], vert=vert, fontsize=10, rot=10) - df2.boxplot(ax=axes[1], vert=vert, fontsize=10, rot=10) + df = DataFrame(np.random.random((100, 5)), columns=["A", "B", "C", "D", "E"]) + + fig, ax = self.plt.subplots(nrows=1, ncols=1) + df.boxplot(ax=ax, vert=vert, fontsize=10, rot=10) + df.boxplot(ax=ax, vert=vert, fontsize=10, rot=10) # In order for the ticklabels to be placed, the plot has to be drawn fig.canvas.draw() - for ax in axes: - axis = ax.xaxis if vert else ax.yaxis - labels = [x.get_text() for x in axis.get_ticklabels(which="major")] - if vert: - self._check_ticks_props(ax, xlabelsize=10, xrot=10) - else: - self._check_ticks_props(ax, ylabelsize=10, yrot=10) - - # Matplotlib returns 10 ticklabels, 5 of which are empty - assert len(labels) % 5 == 0 - assert len(labels) // (len(labels) // 5) == 5 - assert labels[:5] == ["A", "B", "C", "D", "E"] + if vert: + self._check_ticks_props(ax, xlabelsize=10, xrot=10) + else: + self._check_ticks_props(ax, ylabelsize=10, yrot=10) + + axis = ax.xaxis if vert else ax.yaxis + self._check_text_labels( + axis.get_ticklabels(which="major"), ["A", "B", "C", "D", "E"] + ) @td.skip_if_no_mpl