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):