From 015fc62dad34e1293728f4ae0cf438f798f5f1de Mon Sep 17 00:00:00 2001 From: Jan Schulz Date: Fri, 27 Mar 2015 11:18:33 +0100 Subject: [PATCH] Fix sharex/sharey behaviour together with passing in an axis This commit fixes two things: 1.) Wrong x label visibility when using gridspec generated axis When using gridspec, plt.gcf().get_axes() was in the wrong order for the code handling setting the right axes label to invisible. Also handle cases where no subplot is in the last row of a column. 2.) Don't change ax label visibility when an axis is passed in Before, when passing in one ax object of a figure with multiple subplots, the plot call would change the visibility of the x labels, so that only the subplots in the last row would have xticklabels and xlabels. This would happen even if the rest of the subplots would not be plotted with pandas. Now, when passing in an ax to `df.plot( ..., ax=ax)`, the `sharex` kwarg will default to `False` and the visibility of xlabels and xticklabels will not anymore be changed. If you still want that, you can either do that by yourself for the right axes in your figure or explicitly set `sharex=True`. Be aware, that this changes the visible for all axes in the figure, not only the one which is passed in! If pandas creates the subplots itself (e.g. no passed in `ax` kwarg), then the default is still `sharex=True` and the visibility changes are applied. Also fix some old unittests which---together with the quirks of `_check_plot_works`, which plots the plot twice, once without an ax kwarg and once with an ax kwarg---triggered test failures as the new behaviour of `ax together without an explicit sharex` would not remove the visibility of some xlabels. Update the docstrings to explain the new sharex behaviour and also add a warning regarding the changing of all subplots' x axis labels, even for subplots which were not created by pandas. --- doc/source/whatsnew/v0.16.1.txt | 8 ++ pandas/tests/test_graphics.py | 93 ++++++++++++++++++++++- pandas/tools/plotting.py | 126 +++++++++++++++++++++----------- 3 files changed, 183 insertions(+), 44 deletions(-) diff --git a/doc/source/whatsnew/v0.16.1.txt b/doc/source/whatsnew/v0.16.1.txt index 3c3742c968642..c6ace8c23e064 100644 --- a/doc/source/whatsnew/v0.16.1.txt +++ b/doc/source/whatsnew/v0.16.1.txt @@ -31,6 +31,14 @@ API changes +- When passing in an ax to ``df.plot( ..., ax=ax)``, the `sharex` kwarg will now default to `False`. + The result is that the visibility of xlabels and xticklabels will not anymore be changed. You + have to do that by yourself for the right axes in your figure or set ``sharex=True`` explicitly + (but this changes the visible for all axes in the figure, not only the one which is passed in!). + If pandas creates the subplots itself (e.g. no passed in `ax` kwarg), then the + default is still ``sharex=True`` and the visibility changes are applied. + + - Add support for separating years and quarters using dashes, for example 2014-Q1. (:issue:`9688`) diff --git a/pandas/tests/test_graphics.py b/pandas/tests/test_graphics.py index 1cb11179b2430..0a9df941a6045 100644 --- a/pandas/tests/test_graphics.py +++ b/pandas/tests/test_graphics.py @@ -1000,8 +1000,14 @@ def test_plot(self): _check_plot_works(df.plot, xticks=[1, 5, 10]) _check_plot_works(df.plot, ylim=(-100, 100), xlim=(-100, 100)) - axes = _check_plot_works(df.plot, subplots=True, title='blah') + _check_plot_works(df.plot, subplots=True, title='blah') + # We have to redo it here because _check_plot_works does two plots, once without an ax + # kwarg and once with an ax kwarg and the new sharex behaviour does not remove the + # visibility of the latter axis (as ax is present). + # see: https://github.com/pydata/pandas/issues/9737 + axes = df.plot(subplots=True, title='blah') self._check_axes_shape(axes, axes_num=3, layout=(3, 1)) + #axes[0].figure.savefig("test.png") for ax in axes[:2]: self._check_visible(ax.xaxis) # xaxis must be visible for grid self._check_visible(ax.get_xticklabels(), visible=False) @@ -3138,6 +3144,78 @@ def _check_errorbar_color(containers, expected, has_err='has_xerr'): self._check_has_errorbars(ax, xerr=0, yerr=1) _check_errorbar_color(ax.containers, 'green', has_err='has_yerr') + def test_sharex_and_ax(self): + # https://github.com/pydata/pandas/issues/9737 + # using gridspec, the axis in fig.get_axis() are sorted differently than pandas expected + # them, so make sure that only the right ones are removed + import matplotlib.pyplot as plt + plt.close('all') + gs, axes = _generate_4_axes_via_gridspec() + + df = DataFrame({"a":[1,2,3,4,5,6], "b":[1,2,3,4,5,6]}) + + for ax in axes: + df.plot(x="a", y="b", title="title", ax=ax, sharex=True) + + gs.tight_layout(plt.gcf()) + for ax in plt.gcf().get_axes(): + for label in ax.get_xticklabels(): + self.assertEqual(label.get_visible(), ax.is_last_row(), + "x ticklabel has wrong visiblity") + self.assertEqual(ax.xaxis.get_label().get_visible(), ax.is_last_row(), + "x label has wrong visiblity") + + plt.close('all') + gs, axes = _generate_4_axes_via_gridspec() + # without sharex, no labels should be touched! + for ax in axes: + df.plot(x="a", y="b", title="title", ax=ax) + + gs.tight_layout(plt.gcf()) + for ax in plt.gcf().get_axes(): + for label in ax.get_xticklabels(): + self.assertTrue(label.get_visible(), "x ticklabel is invisible but shouldn't") + self.assertTrue(ax.xaxis.get_label().get_visible(), + "x label is invisible but shouldn't") + + + def test_sharey_and_ax(self): + # https://github.com/pydata/pandas/issues/9737 + # using gridspec, the axis in fig.get_axis() are sorted differently than pandas expected + # them, so make sure that only the right ones are removed + import matplotlib.pyplot as plt + + plt.close('all') + gs, axes = _generate_4_axes_via_gridspec() + + df = DataFrame({"a":[1,2,3,4,5,6], "b":[1,2,3,4,5,6]}) + + for ax in axes: + df.plot(x="a", y="b", title="title", ax=ax, sharey=True) + + gs.tight_layout(plt.gcf()) + for ax in plt.gcf().get_axes(): + for label in ax.get_yticklabels(): + self.assertEqual(label.get_visible(), ax.is_first_col(), + "y ticklabel has wrong visiblity") + self.assertEqual(ax.yaxis.get_label().get_visible(), ax.is_first_col(), + "y label has wrong visiblity") + + plt.close('all') + gs, axes = _generate_4_axes_via_gridspec() + + # without sharex, no labels should be touched! + for ax in axes: + df.plot(x="a", y="b", title="title", ax=ax) + + gs.tight_layout(plt.gcf()) + for ax in plt.gcf().get_axes(): + for label in ax.get_yticklabels(): + self.assertTrue(label.get_visible(), "y ticklabel is invisible but shouldn't") + self.assertTrue(ax.yaxis.get_label().get_visible(), + "y label is invisible but shouldn't") + + @tm.mplskip class TestDataFrameGroupByPlots(TestPlotBase): @@ -3612,6 +3690,19 @@ def _check_plot_works(f, *args, **kwargs): return ret +def _generate_4_axes_via_gridspec(): + import matplotlib.pyplot as plt + import matplotlib as mpl + import matplotlib.gridspec + + gs = mpl.gridspec.GridSpec(2, 2) + ax_tl = plt.subplot(gs[0,0]) + ax_ll = plt.subplot(gs[1,0]) + ax_tr = plt.subplot(gs[0,1]) + ax_lr = plt.subplot(gs[1,1]) + + return gs, [ax_tl, ax_ll, ax_tr, ax_lr] + def curpath(): pth, _ = os.path.split(os.path.abspath(__file__)) diff --git a/pandas/tools/plotting.py b/pandas/tools/plotting.py index cf9c890823f8f..ce000ffc3e012 100644 --- a/pandas/tools/plotting.py +++ b/pandas/tools/plotting.py @@ -769,7 +769,7 @@ class MPLPlot(object): _attr_defaults = {'logy': False, 'logx': False, 'loglog': False, 'mark_right': True, 'stacked': False} - def __init__(self, data, kind=None, by=None, subplots=False, sharex=True, + def __init__(self, data, kind=None, by=None, subplots=False, sharex=None, sharey=False, use_index=True, figsize=None, grid=None, legend=True, rot=None, ax=None, fig=None, title=None, xlim=None, ylim=None, @@ -786,7 +786,16 @@ def __init__(self, data, kind=None, by=None, subplots=False, sharex=True, self.sort_columns = sort_columns self.subplots = subplots - self.sharex = sharex + + if sharex is None: + if ax is None: + self.sharex = True + else: + # if we get an axis, the users should do the visibility setting... + self.sharex = False + else: + self.sharex = sharex + self.sharey = sharey self.figsize = figsize self.layout = layout @@ -2350,10 +2359,14 @@ def _plot(data, x=None, y=None, subplots=False, df_ax = """ax : matplotlib axes object, default None subplots : boolean, default False Make separate subplots for each column - sharex : boolean, default True - In case subplots=True, share x axis + sharex : boolean, default True if ax is None else False + In case subplots=True, share x axis and set some x axis labels to + invisible; defaults to True if ax is None otherwise False if an ax + is passed in; Be aware, that passing in both an ax and sharex=True + will alter all x axis labels for all axis in a figure! sharey : boolean, default False - In case subplots=True, share y axis + In case subplots=True, share y axis and set some y axis labels to + invisible layout : tuple (optional) (rows, columns) for the layout of subplots""" series_ax = """ax : matplotlib axes object @@ -2465,7 +2478,7 @@ def _plot(data, x=None, y=None, subplots=False, @Appender(_shared_docs['plot'] % _shared_doc_df_kwargs) def plot_frame(data, x=None, y=None, kind='line', ax=None, # Dataframe unique - subplots=False, sharex=True, sharey=False, layout=None, # Dataframe unique + subplots=False, sharex=None, sharey=False, layout=None, # Dataframe unique figsize=None, use_index=True, title=None, grid=None, legend=True, style=None, logx=False, logy=False, loglog=False, xticks=None, yticks=None, xlim=None, ylim=None, @@ -2730,8 +2743,14 @@ def hist_frame(data, column=None, by=None, grid=True, xlabelsize=None, yrot : float, default None rotation of y axis labels ax : matplotlib axes object, default None - sharex : bool, if True, the X axis will be shared amongst all subplots. - sharey : bool, if True, the Y axis will be shared amongst all subplots. + sharex : boolean, default True if ax is None else False + In case subplots=True, share x axis and set some x axis labels to + invisible; defaults to True if ax is None otherwise False if an ax + is passed in; Be aware, that passing in both an ax and sharex=True + will alter all x axis labels for all subplots in a figure! + sharey : boolean, default False + In case subplots=True, share y axis and set some y axis labels to + invisible figsize : tuple The size of the figure to create in inches by default layout: (optional) a tuple (rows, columns) for the layout of the histograms @@ -3129,7 +3148,8 @@ def _subplots(naxes=None, sharex=False, sharey=False, squeeze=True, Keyword arguments: naxes : int - Number of required axes. Exceeded axes are set invisible. Default is nrows * ncols. + Number of required axes. Exceeded axes are set invisible. Default is + nrows * ncols. sharex : bool If True, the X axis will be shared amongst all subplots. @@ -3256,12 +3276,12 @@ def _subplots(naxes=None, sharex=False, sharey=False, squeeze=True, ax = fig.add_subplot(nrows, ncols, i + 1, **kwds) axarr[i] = ax - _handle_shared_axes(axarr, nplots, naxes, nrows, ncols, sharex, sharey) - if naxes != nplots: for ax in axarr[naxes:]: ax.set_visible(False) + _handle_shared_axes(axarr, nplots, naxes, nrows, ncols, sharex, sharey) + if squeeze: # Reshape the array to have the final desired dimension (nrow,ncol), # though discarding unneeded dimensions that equal 1. If we only have @@ -3276,44 +3296,64 @@ def _subplots(naxes=None, sharex=False, sharey=False, squeeze=True, return fig, axes +def _remove_xlabels_from_axis(ax): + for label in ax.get_xticklabels(): + label.set_visible(False) + try: + # set_visible will not be effective if + # minor axis has NullLocator and NullFormattor (default) + import matplotlib.ticker as ticker + + if isinstance(ax.xaxis.get_minor_locator(), ticker.NullLocator): + ax.xaxis.set_minor_locator(ticker.AutoLocator()) + if isinstance(ax.xaxis.get_minor_formatter(), ticker.NullFormatter): + ax.xaxis.set_minor_formatter(ticker.FormatStrFormatter('')) + for label in ax.get_xticklabels(minor=True): + label.set_visible(False) + except Exception: # pragma no cover + pass + ax.xaxis.get_label().set_visible(False) + +def _remove_ylables_from_axis(ax): + for label in ax.get_yticklabels(): + label.set_visible(False) + try: + import matplotlib.ticker as ticker + if isinstance(ax.yaxis.get_minor_locator(), ticker.NullLocator): + ax.yaxis.set_minor_locator(ticker.AutoLocator()) + if isinstance(ax.yaxis.get_minor_formatter(), ticker.NullFormatter): + ax.yaxis.set_minor_formatter(ticker.FormatStrFormatter('')) + for label in ax.get_yticklabels(minor=True): + label.set_visible(False) + except Exception: # pragma no cover + pass + ax.yaxis.get_label().set_visible(False) def _handle_shared_axes(axarr, nplots, naxes, nrows, ncols, sharex, sharey): if nplots > 1: + # first find out the ax layout, so that we can correctly handle 'gaps" + layout = np.zeros((nrows+1,ncols+1), dtype=np.bool) + for ax in axarr: + layout[ax.rowNum, ax.colNum] = ax.get_visible() + if sharex and nrows > 1: - for ax in axarr[:naxes][:-ncols]: # only bottom row - for label in ax.get_xticklabels(): - label.set_visible(False) - try: - # set_visible will not be effective if - # minor axis has NullLocator and NullFormattor (default) - import matplotlib.ticker as ticker - - if isinstance(ax.xaxis.get_minor_locator(), ticker.NullLocator): - ax.xaxis.set_minor_locator(ticker.AutoLocator()) - if isinstance(ax.xaxis.get_minor_formatter(), ticker.NullFormatter): - ax.xaxis.set_minor_formatter(ticker.FormatStrFormatter('')) - for label in ax.get_xticklabels(minor=True): - label.set_visible(False) - except Exception: # pragma no cover - pass - ax.xaxis.get_label().set_visible(False) + for ax in axarr: + # only the last row of subplots should get x labels -> all other off + # layout handles the case that the subplot is the last in the column, + # because below is no subplot/gap. + if not layout[ax.rowNum+1, ax.colNum]: + continue + _remove_xlabels_from_axis(ax) if sharey and ncols > 1: - for i, ax in enumerate(axarr): - if (i % ncols) != 0: # only first column - for label in ax.get_yticklabels(): - label.set_visible(False) - try: - import matplotlib.ticker as ticker - if isinstance(ax.yaxis.get_minor_locator(), ticker.NullLocator): - ax.yaxis.set_minor_locator(ticker.AutoLocator()) - if isinstance(ax.yaxis.get_minor_formatter(), ticker.NullFormatter): - ax.yaxis.set_minor_formatter(ticker.FormatStrFormatter('')) - for label in ax.get_yticklabels(minor=True): - label.set_visible(False) - except Exception: # pragma no cover - pass - ax.yaxis.get_label().set_visible(False) + for ax in axarr: + # only the first column should get y labels -> set all other to off + # as we only have labels in teh first column and we always have a subplot there, + # we can skip the layout test + if ax.is_first_col(): + continue + _remove_ylables_from_axis(ax) + def _flatten(axes):