Skip to content

Commit 015fc62

Browse files
committed
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.
1 parent 8d2818e commit 015fc62

File tree

3 files changed

+183
-44
lines changed

3 files changed

+183
-44
lines changed

doc/source/whatsnew/v0.16.1.txt

+8
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,14 @@ API changes
3131

3232

3333

34+
- When passing in an ax to ``df.plot( ..., ax=ax)``, the `sharex` kwarg will now default to `False`.
35+
The result is that the visibility of xlabels and xticklabels will not anymore be changed. You
36+
have to do that by yourself for the right axes in your figure or set ``sharex=True`` explicitly
37+
(but this changes the visible for all axes in the figure, not only the one which is passed in!).
38+
If pandas creates the subplots itself (e.g. no passed in `ax` kwarg), then the
39+
default is still ``sharex=True`` and the visibility changes are applied.
40+
41+
3442

3543
- Add support for separating years and quarters using dashes, for
3644
example 2014-Q1. (:issue:`9688`)

pandas/tests/test_graphics.py

+92-1
Original file line numberDiff line numberDiff line change
@@ -1000,8 +1000,14 @@ def test_plot(self):
10001000
_check_plot_works(df.plot, xticks=[1, 5, 10])
10011001
_check_plot_works(df.plot, ylim=(-100, 100), xlim=(-100, 100))
10021002

1003-
axes = _check_plot_works(df.plot, subplots=True, title='blah')
1003+
_check_plot_works(df.plot, subplots=True, title='blah')
1004+
# We have to redo it here because _check_plot_works does two plots, once without an ax
1005+
# kwarg and once with an ax kwarg and the new sharex behaviour does not remove the
1006+
# visibility of the latter axis (as ax is present).
1007+
# see: https://github.com/pydata/pandas/issues/9737
1008+
axes = df.plot(subplots=True, title='blah')
10041009
self._check_axes_shape(axes, axes_num=3, layout=(3, 1))
1010+
#axes[0].figure.savefig("test.png")
10051011
for ax in axes[:2]:
10061012
self._check_visible(ax.xaxis) # xaxis must be visible for grid
10071013
self._check_visible(ax.get_xticklabels(), visible=False)
@@ -3138,6 +3144,78 @@ def _check_errorbar_color(containers, expected, has_err='has_xerr'):
31383144
self._check_has_errorbars(ax, xerr=0, yerr=1)
31393145
_check_errorbar_color(ax.containers, 'green', has_err='has_yerr')
31403146

3147+
def test_sharex_and_ax(self):
3148+
# https://github.com/pydata/pandas/issues/9737
3149+
# using gridspec, the axis in fig.get_axis() are sorted differently than pandas expected
3150+
# them, so make sure that only the right ones are removed
3151+
import matplotlib.pyplot as plt
3152+
plt.close('all')
3153+
gs, axes = _generate_4_axes_via_gridspec()
3154+
3155+
df = DataFrame({"a":[1,2,3,4,5,6], "b":[1,2,3,4,5,6]})
3156+
3157+
for ax in axes:
3158+
df.plot(x="a", y="b", title="title", ax=ax, sharex=True)
3159+
3160+
gs.tight_layout(plt.gcf())
3161+
for ax in plt.gcf().get_axes():
3162+
for label in ax.get_xticklabels():
3163+
self.assertEqual(label.get_visible(), ax.is_last_row(),
3164+
"x ticklabel has wrong visiblity")
3165+
self.assertEqual(ax.xaxis.get_label().get_visible(), ax.is_last_row(),
3166+
"x label has wrong visiblity")
3167+
3168+
plt.close('all')
3169+
gs, axes = _generate_4_axes_via_gridspec()
3170+
# without sharex, no labels should be touched!
3171+
for ax in axes:
3172+
df.plot(x="a", y="b", title="title", ax=ax)
3173+
3174+
gs.tight_layout(plt.gcf())
3175+
for ax in plt.gcf().get_axes():
3176+
for label in ax.get_xticklabels():
3177+
self.assertTrue(label.get_visible(), "x ticklabel is invisible but shouldn't")
3178+
self.assertTrue(ax.xaxis.get_label().get_visible(),
3179+
"x label is invisible but shouldn't")
3180+
3181+
3182+
def test_sharey_and_ax(self):
3183+
# https://github.com/pydata/pandas/issues/9737
3184+
# using gridspec, the axis in fig.get_axis() are sorted differently than pandas expected
3185+
# them, so make sure that only the right ones are removed
3186+
import matplotlib.pyplot as plt
3187+
3188+
plt.close('all')
3189+
gs, axes = _generate_4_axes_via_gridspec()
3190+
3191+
df = DataFrame({"a":[1,2,3,4,5,6], "b":[1,2,3,4,5,6]})
3192+
3193+
for ax in axes:
3194+
df.plot(x="a", y="b", title="title", ax=ax, sharey=True)
3195+
3196+
gs.tight_layout(plt.gcf())
3197+
for ax in plt.gcf().get_axes():
3198+
for label in ax.get_yticklabels():
3199+
self.assertEqual(label.get_visible(), ax.is_first_col(),
3200+
"y ticklabel has wrong visiblity")
3201+
self.assertEqual(ax.yaxis.get_label().get_visible(), ax.is_first_col(),
3202+
"y label has wrong visiblity")
3203+
3204+
plt.close('all')
3205+
gs, axes = _generate_4_axes_via_gridspec()
3206+
3207+
# without sharex, no labels should be touched!
3208+
for ax in axes:
3209+
df.plot(x="a", y="b", title="title", ax=ax)
3210+
3211+
gs.tight_layout(plt.gcf())
3212+
for ax in plt.gcf().get_axes():
3213+
for label in ax.get_yticklabels():
3214+
self.assertTrue(label.get_visible(), "y ticklabel is invisible but shouldn't")
3215+
self.assertTrue(ax.yaxis.get_label().get_visible(),
3216+
"y label is invisible but shouldn't")
3217+
3218+
31413219

31423220
@tm.mplskip
31433221
class TestDataFrameGroupByPlots(TestPlotBase):
@@ -3612,6 +3690,19 @@ def _check_plot_works(f, *args, **kwargs):
36123690

36133691
return ret
36143692

3693+
def _generate_4_axes_via_gridspec():
3694+
import matplotlib.pyplot as plt
3695+
import matplotlib as mpl
3696+
import matplotlib.gridspec
3697+
3698+
gs = mpl.gridspec.GridSpec(2, 2)
3699+
ax_tl = plt.subplot(gs[0,0])
3700+
ax_ll = plt.subplot(gs[1,0])
3701+
ax_tr = plt.subplot(gs[0,1])
3702+
ax_lr = plt.subplot(gs[1,1])
3703+
3704+
return gs, [ax_tl, ax_ll, ax_tr, ax_lr]
3705+
36153706

36163707
def curpath():
36173708
pth, _ = os.path.split(os.path.abspath(__file__))

pandas/tools/plotting.py

+83-43
Original file line numberDiff line numberDiff line change
@@ -769,7 +769,7 @@ class MPLPlot(object):
769769
_attr_defaults = {'logy': False, 'logx': False, 'loglog': False,
770770
'mark_right': True, 'stacked': False}
771771

772-
def __init__(self, data, kind=None, by=None, subplots=False, sharex=True,
772+
def __init__(self, data, kind=None, by=None, subplots=False, sharex=None,
773773
sharey=False, use_index=True,
774774
figsize=None, grid=None, legend=True, rot=None,
775775
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,
786786
self.sort_columns = sort_columns
787787

788788
self.subplots = subplots
789-
self.sharex = sharex
789+
790+
if sharex is None:
791+
if ax is None:
792+
self.sharex = True
793+
else:
794+
# if we get an axis, the users should do the visibility setting...
795+
self.sharex = False
796+
else:
797+
self.sharex = sharex
798+
790799
self.sharey = sharey
791800
self.figsize = figsize
792801
self.layout = layout
@@ -2350,10 +2359,14 @@ def _plot(data, x=None, y=None, subplots=False,
23502359
df_ax = """ax : matplotlib axes object, default None
23512360
subplots : boolean, default False
23522361
Make separate subplots for each column
2353-
sharex : boolean, default True
2354-
In case subplots=True, share x axis
2362+
sharex : boolean, default True if ax is None else False
2363+
In case subplots=True, share x axis and set some x axis labels to
2364+
invisible; defaults to True if ax is None otherwise False if an ax
2365+
is passed in; Be aware, that passing in both an ax and sharex=True
2366+
will alter all x axis labels for all axis in a figure!
23552367
sharey : boolean, default False
2356-
In case subplots=True, share y axis
2368+
In case subplots=True, share y axis and set some y axis labels to
2369+
invisible
23572370
layout : tuple (optional)
23582371
(rows, columns) for the layout of subplots"""
23592372
series_ax = """ax : matplotlib axes object
@@ -2465,7 +2478,7 @@ def _plot(data, x=None, y=None, subplots=False,
24652478

24662479
@Appender(_shared_docs['plot'] % _shared_doc_df_kwargs)
24672480
def plot_frame(data, x=None, y=None, kind='line', ax=None, # Dataframe unique
2468-
subplots=False, sharex=True, sharey=False, layout=None, # Dataframe unique
2481+
subplots=False, sharex=None, sharey=False, layout=None, # Dataframe unique
24692482
figsize=None, use_index=True, title=None, grid=None,
24702483
legend=True, style=None, logx=False, logy=False, loglog=False,
24712484
xticks=None, yticks=None, xlim=None, ylim=None,
@@ -2730,8 +2743,14 @@ def hist_frame(data, column=None, by=None, grid=True, xlabelsize=None,
27302743
yrot : float, default None
27312744
rotation of y axis labels
27322745
ax : matplotlib axes object, default None
2733-
sharex : bool, if True, the X axis will be shared amongst all subplots.
2734-
sharey : bool, if True, the Y axis will be shared amongst all subplots.
2746+
sharex : boolean, default True if ax is None else False
2747+
In case subplots=True, share x axis and set some x axis labels to
2748+
invisible; defaults to True if ax is None otherwise False if an ax
2749+
is passed in; Be aware, that passing in both an ax and sharex=True
2750+
will alter all x axis labels for all subplots in a figure!
2751+
sharey : boolean, default False
2752+
In case subplots=True, share y axis and set some y axis labels to
2753+
invisible
27352754
figsize : tuple
27362755
The size of the figure to create in inches by default
27372756
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,
31293148
Keyword arguments:
31303149
31313150
naxes : int
3132-
Number of required axes. Exceeded axes are set invisible. Default is nrows * ncols.
3151+
Number of required axes. Exceeded axes are set invisible. Default is
3152+
nrows * ncols.
31333153
31343154
sharex : bool
31353155
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,
32563276
ax = fig.add_subplot(nrows, ncols, i + 1, **kwds)
32573277
axarr[i] = ax
32583278

3259-
_handle_shared_axes(axarr, nplots, naxes, nrows, ncols, sharex, sharey)
3260-
32613279
if naxes != nplots:
32623280
for ax in axarr[naxes:]:
32633281
ax.set_visible(False)
32643282

3283+
_handle_shared_axes(axarr, nplots, naxes, nrows, ncols, sharex, sharey)
3284+
32653285
if squeeze:
32663286
# Reshape the array to have the final desired dimension (nrow,ncol),
32673287
# 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,
32763296

32773297
return fig, axes
32783298

3299+
def _remove_xlabels_from_axis(ax):
3300+
for label in ax.get_xticklabels():
3301+
label.set_visible(False)
3302+
try:
3303+
# set_visible will not be effective if
3304+
# minor axis has NullLocator and NullFormattor (default)
3305+
import matplotlib.ticker as ticker
3306+
3307+
if isinstance(ax.xaxis.get_minor_locator(), ticker.NullLocator):
3308+
ax.xaxis.set_minor_locator(ticker.AutoLocator())
3309+
if isinstance(ax.xaxis.get_minor_formatter(), ticker.NullFormatter):
3310+
ax.xaxis.set_minor_formatter(ticker.FormatStrFormatter(''))
3311+
for label in ax.get_xticklabels(minor=True):
3312+
label.set_visible(False)
3313+
except Exception: # pragma no cover
3314+
pass
3315+
ax.xaxis.get_label().set_visible(False)
3316+
3317+
def _remove_ylables_from_axis(ax):
3318+
for label in ax.get_yticklabels():
3319+
label.set_visible(False)
3320+
try:
3321+
import matplotlib.ticker as ticker
3322+
if isinstance(ax.yaxis.get_minor_locator(), ticker.NullLocator):
3323+
ax.yaxis.set_minor_locator(ticker.AutoLocator())
3324+
if isinstance(ax.yaxis.get_minor_formatter(), ticker.NullFormatter):
3325+
ax.yaxis.set_minor_formatter(ticker.FormatStrFormatter(''))
3326+
for label in ax.get_yticklabels(minor=True):
3327+
label.set_visible(False)
3328+
except Exception: # pragma no cover
3329+
pass
3330+
ax.yaxis.get_label().set_visible(False)
32793331

32803332
def _handle_shared_axes(axarr, nplots, naxes, nrows, ncols, sharex, sharey):
32813333
if nplots > 1:
32823334

3335+
# first find out the ax layout, so that we can correctly handle 'gaps"
3336+
layout = np.zeros((nrows+1,ncols+1), dtype=np.bool)
3337+
for ax in axarr:
3338+
layout[ax.rowNum, ax.colNum] = ax.get_visible()
3339+
32833340
if sharex and nrows > 1:
3284-
for ax in axarr[:naxes][:-ncols]: # only bottom row
3285-
for label in ax.get_xticklabels():
3286-
label.set_visible(False)
3287-
try:
3288-
# set_visible will not be effective if
3289-
# minor axis has NullLocator and NullFormattor (default)
3290-
import matplotlib.ticker as ticker
3291-
3292-
if isinstance(ax.xaxis.get_minor_locator(), ticker.NullLocator):
3293-
ax.xaxis.set_minor_locator(ticker.AutoLocator())
3294-
if isinstance(ax.xaxis.get_minor_formatter(), ticker.NullFormatter):
3295-
ax.xaxis.set_minor_formatter(ticker.FormatStrFormatter(''))
3296-
for label in ax.get_xticklabels(minor=True):
3297-
label.set_visible(False)
3298-
except Exception: # pragma no cover
3299-
pass
3300-
ax.xaxis.get_label().set_visible(False)
3341+
for ax in axarr:
3342+
# only the last row of subplots should get x labels -> all other off
3343+
# layout handles the case that the subplot is the last in the column,
3344+
# because below is no subplot/gap.
3345+
if not layout[ax.rowNum+1, ax.colNum]:
3346+
continue
3347+
_remove_xlabels_from_axis(ax)
33013348
if sharey and ncols > 1:
3302-
for i, ax in enumerate(axarr):
3303-
if (i % ncols) != 0: # only first column
3304-
for label in ax.get_yticklabels():
3305-
label.set_visible(False)
3306-
try:
3307-
import matplotlib.ticker as ticker
3308-
if isinstance(ax.yaxis.get_minor_locator(), ticker.NullLocator):
3309-
ax.yaxis.set_minor_locator(ticker.AutoLocator())
3310-
if isinstance(ax.yaxis.get_minor_formatter(), ticker.NullFormatter):
3311-
ax.yaxis.set_minor_formatter(ticker.FormatStrFormatter(''))
3312-
for label in ax.get_yticklabels(minor=True):
3313-
label.set_visible(False)
3314-
except Exception: # pragma no cover
3315-
pass
3316-
ax.yaxis.get_label().set_visible(False)
3349+
for ax in axarr:
3350+
# only the first column should get y labels -> set all other to off
3351+
# as we only have labels in teh first column and we always have a subplot there,
3352+
# we can skip the layout test
3353+
if ax.is_first_col():
3354+
continue
3355+
_remove_ylables_from_axis(ax)
3356+
33173357

33183358

33193359
def _flatten(axes):

0 commit comments

Comments
 (0)