Skip to content

Commit 1404ce9

Browse files
committed
ENH: plot functions accept multiple axes and layout kw
1 parent dd635ed commit 1404ce9

File tree

4 files changed

+288
-108
lines changed

4 files changed

+288
-108
lines changed

doc/source/v0.15.0.txt

+3
Original file line numberDiff line numberDiff line change
@@ -303,6 +303,9 @@ Enhancements
303303
~~~~~~~~~~~~
304304
- Added support for bool, uint8, uint16 and uint32 datatypes in ``to_stata`` (:issue:`7097`, :issue:`7365`)
305305

306+
- Added ``layout`` keyword to ``DataFrame.plot`` (:issue:`6667`)
307+
- Allow to pass multiple axes to ``DataFrame.plot``, ``hist`` and ``boxplot`` (:issue:`5353`, :issue:`6970`, :issue:`7069`)
308+
306309

307310
- ``PeriodIndex`` supports ``resolution`` as the same as ``DatetimeIndex`` (:issue:`7708`)
308311
- ``pandas.tseries.holiday`` has added support for additional holidays and ways to observe holidays (:issue:`7070`)

doc/source/visualization.rst

+38-7
Original file line numberDiff line numberDiff line change
@@ -946,10 +946,41 @@ with the ``subplots`` keyword:
946946
@savefig frame_plot_subplots.png
947947
df.plot(subplots=True, figsize=(6, 6));
948948
949-
Targeting Different Subplots
950-
~~~~~~~~~~~~~~~~~~~~~~~~~~~~
949+
Using Layout and Targetting Multiple Axes
950+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
951951

952-
You can pass an ``ax`` argument to :meth:`Series.plot` to plot on a particular axis:
952+
The layout of subplots can be specified by ``layout`` keyword. It can accept
953+
``(rows, columns)``. The ``layout`` keyword can be used in
954+
``hist`` and ``boxplot`` also. If input is invalid, ``ValueError`` will be raised.
955+
956+
The number of axes which can be contained by rows x columns specified by ``layout`` must be
957+
larger than the number of required subplots. If layout can contain more axes than required,
958+
blank axes are not drawn.
959+
960+
.. ipython:: python
961+
962+
@savefig frame_plot_subplots_layout.png
963+
df.plot(subplots=True, layout=(2, 3), figsize=(6, 6));
964+
965+
Also, you can pass multiple axes created beforehand as list-like via ``ax`` keyword.
966+
This allows to use more complicated layout.
967+
The passed axes must be the same number as the subplots being drawn.
968+
969+
When multiple axes are passed via ``ax`` keyword, ``layout``, ``sharex`` and ``sharey`` keywords are ignored.
970+
These must be configured when creating axes.
971+
972+
.. ipython:: python
973+
974+
fig, axes = plt.subplots(4, 4, figsize=(6, 6));
975+
plt.adjust_subplots(wspace=0.5, hspace=0.5);
976+
target1 = [axes[0][0], axes[1][1], axes[2][2], axes[3][3]]
977+
target2 = [axes[3][0], axes[2][1], axes[1][2], axes[0][3]]
978+
979+
df.plot(subplots=True, ax=target1, legend=False);
980+
@savefig frame_plot_subplots_multi_ax.png
981+
(-df).plot(subplots=True, ax=target2, legend=False);
982+
983+
Another option is passing an ``ax`` argument to :meth:`Series.plot` to plot on a particular axis:
953984

954985
.. ipython:: python
955986
:suppress:
@@ -964,12 +995,12 @@ You can pass an ``ax`` argument to :meth:`Series.plot` to plot on a particular a
964995
.. ipython:: python
965996
966997
fig, axes = plt.subplots(nrows=2, ncols=2)
967-
df['A'].plot(ax=axes[0,0]); axes[0,0].set_title('A')
968-
df['B'].plot(ax=axes[0,1]); axes[0,1].set_title('B')
969-
df['C'].plot(ax=axes[1,0]); axes[1,0].set_title('C')
998+
df['A'].plot(ax=axes[0,0]); axes[0,0].set_title('A');
999+
df['B'].plot(ax=axes[0,1]); axes[0,1].set_title('B');
1000+
df['C'].plot(ax=axes[1,0]); axes[1,0].set_title('C');
9701001
9711002
@savefig series_plot_multi.png
972-
df['D'].plot(ax=axes[1,1]); axes[1,1].set_title('D')
1003+
df['D'].plot(ax=axes[1,1]); axes[1,1].set_title('D');
9731004
9741005
.. ipython:: python
9751006
:suppress:

pandas/tests/test_graphics.py

+134-8
Original file line numberDiff line numberDiff line change
@@ -670,7 +670,7 @@ def test_hist_layout_with_by(self):
670670
axes = _check_plot_works(df.height.hist, by=df.classroom, layout=(2, 2))
671671
self._check_axes_shape(axes, axes_num=3, layout=(2, 2))
672672

673-
axes = _check_plot_works(df.height.hist, by=df.category, layout=(4, 2), figsize=(12, 7))
673+
axes = df.height.hist(by=df.category, layout=(4, 2), figsize=(12, 7))
674674
self._check_axes_shape(axes, axes_num=4, layout=(4, 2), figsize=(12, 7))
675675

676676
@slow
@@ -1071,6 +1071,7 @@ def test_subplots(self):
10711071
for kind in ['bar', 'barh', 'line', 'area']:
10721072
axes = df.plot(kind=kind, subplots=True, sharex=True, legend=True)
10731073
self._check_axes_shape(axes, axes_num=3, layout=(3, 1))
1074+
self.assertEqual(axes.shape, (3, ))
10741075

10751076
for ax, column in zip(axes, df.columns):
10761077
self._check_legend_labels(ax, labels=[com.pprint_thing(column)])
@@ -1133,6 +1134,77 @@ def test_subplots_timeseries(self):
11331134
self._check_visible(ax.get_yticklabels())
11341135
self._check_ticks_props(ax, xlabelsize=7, xrot=45)
11351136

1137+
def test_subplots_layout(self):
1138+
# GH 6667
1139+
df = DataFrame(np.random.rand(10, 3),
1140+
index=list(string.ascii_letters[:10]))
1141+
1142+
axes = df.plot(subplots=True, layout=(2, 2))
1143+
self._check_axes_shape(axes, axes_num=3, layout=(2, 2))
1144+
self.assertEqual(axes.shape, (2, 2))
1145+
1146+
axes = df.plot(subplots=True, layout=(1, 4))
1147+
self._check_axes_shape(axes, axes_num=3, layout=(1, 4))
1148+
self.assertEqual(axes.shape, (1, 4))
1149+
1150+
with tm.assertRaises(ValueError):
1151+
axes = df.plot(subplots=True, layout=(1, 1))
1152+
1153+
# single column
1154+
df = DataFrame(np.random.rand(10, 1),
1155+
index=list(string.ascii_letters[:10]))
1156+
axes = df.plot(subplots=True)
1157+
self._check_axes_shape(axes, axes_num=1, layout=(1, 1))
1158+
self.assertEqual(axes.shape, (1, ))
1159+
1160+
axes = df.plot(subplots=True, layout=(3, 3))
1161+
self._check_axes_shape(axes, axes_num=1, layout=(3, 3))
1162+
self.assertEqual(axes.shape, (3, 3))
1163+
1164+
@slow
1165+
def test_subplots_multiple_axes(self):
1166+
# GH 5353, 6970, GH 7069
1167+
fig, axes = self.plt.subplots(2, 3)
1168+
df = DataFrame(np.random.rand(10, 3),
1169+
index=list(string.ascii_letters[:10]))
1170+
1171+
returned = df.plot(subplots=True, ax=axes[0])
1172+
self._check_axes_shape(returned, axes_num=3, layout=(1, 3))
1173+
self.assertEqual(returned.shape, (3, ))
1174+
self.assertIs(returned[0].figure, fig)
1175+
# draw on second row
1176+
returned = df.plot(subplots=True, ax=axes[1])
1177+
self._check_axes_shape(returned, axes_num=3, layout=(1, 3))
1178+
self.assertEqual(returned.shape, (3, ))
1179+
self.assertIs(returned[0].figure, fig)
1180+
self._check_axes_shape(axes, axes_num=6, layout=(2, 3))
1181+
tm.close()
1182+
1183+
with tm.assertRaises(ValueError):
1184+
fig, axes = self.plt.subplots(2, 3)
1185+
# pass different number of axes from required
1186+
df.plot(subplots=True, ax=axes)
1187+
1188+
# pass 2-dim axes and invalid layout
1189+
# invalid lauout should not affect to input and return value
1190+
# (show warning is tested in
1191+
# TestDataFrameGroupByPlots.test_grouped_box_multiple_axes
1192+
fig, axes = self.plt.subplots(2, 2)
1193+
df = DataFrame(np.random.rand(10, 4),
1194+
index=list(string.ascii_letters[:10]))
1195+
1196+
returned = df.plot(subplots=True, ax=axes, layout=(2, 1))
1197+
self._check_axes_shape(returned, axes_num=4, layout=(2, 2))
1198+
self.assertEqual(returned.shape, (4, ))
1199+
1200+
# single column
1201+
fig, axes = self.plt.subplots(1, 1)
1202+
df = DataFrame(np.random.rand(10, 1),
1203+
index=list(string.ascii_letters[:10]))
1204+
axes = df.plot(subplots=True, ax=[axes])
1205+
self._check_axes_shape(axes, axes_num=1, layout=(1, 1))
1206+
self.assertEqual(axes.shape, (1, ))
1207+
11361208
def test_negative_log(self):
11371209
df = - DataFrame(rand(6, 4),
11381210
index=list(string.ascii_letters[:6]),
@@ -1718,15 +1790,15 @@ def test_hist_df_coord(self):
17181790
normal_df = DataFrame({'A': np.repeat(np.array([1, 2, 3, 4, 5]),
17191791
np.array([10, 9, 8, 7, 6])),
17201792
'B': np.repeat(np.array([1, 2, 3, 4, 5]),
1721-
np.array([8, 8, 8, 8, 8])),
1793+
np.array([8, 8, 8, 8, 8])),
17221794
'C': np.repeat(np.array([1, 2, 3, 4, 5]),
17231795
np.array([6, 7, 8, 9, 10]))},
17241796
columns=['A', 'B', 'C'])
17251797

17261798
nan_df = DataFrame({'A': np.repeat(np.array([np.nan, 1, 2, 3, 4, 5]),
17271799
np.array([3, 10, 9, 8, 7, 6])),
17281800
'B': np.repeat(np.array([1, np.nan, 2, 3, 4, 5]),
1729-
np.array([8, 3, 8, 8, 8, 8])),
1801+
np.array([8, 3, 8, 8, 8, 8])),
17301802
'C': np.repeat(np.array([1, 2, 3, np.nan, 4, 5]),
17311803
np.array([6, 7, 8, 3, 9, 10]))},
17321804
columns=['A', 'B', 'C'])
@@ -2712,6 +2784,41 @@ def test_grouped_box_layout(self):
27122784
return_type='dict')
27132785
self._check_axes_shape(self.plt.gcf().axes, axes_num=3, layout=(1, 4))
27142786

2787+
@slow
2788+
def test_grouped_box_multiple_axes(self):
2789+
# GH 6970, GH 7069
2790+
df = self.hist_df
2791+
2792+
# check warning to ignore sharex / sharey
2793+
# this check should be done in the first function which
2794+
# passes multiple axes to plot, hist or boxplot
2795+
# location should be changed if other test is added
2796+
# which has earlier alphabetical order
2797+
with tm.assert_produces_warning(UserWarning):
2798+
fig, axes = self.plt.subplots(2, 2)
2799+
df.groupby('category').boxplot(column='height', return_type='axes', ax=axes)
2800+
self._check_axes_shape(self.plt.gcf().axes, axes_num=4, layout=(2, 2))
2801+
2802+
fig, axes = self.plt.subplots(2, 3)
2803+
returned = df.boxplot(column=['height', 'weight', 'category'], by='gender',
2804+
return_type='axes', ax=axes[0])
2805+
returned = np.array(returned.values())
2806+
self._check_axes_shape(returned, axes_num=3, layout=(1, 3))
2807+
self.assert_numpy_array_equal(returned, axes[0])
2808+
self.assertIs(returned[0].figure, fig)
2809+
# draw on second row
2810+
returned = df.groupby('classroom').boxplot(column=['height', 'weight', 'category'],
2811+
return_type='axes', ax=axes[1])
2812+
returned = np.array(returned.values())
2813+
self._check_axes_shape(returned, axes_num=3, layout=(1, 3))
2814+
self.assert_numpy_array_equal(returned, axes[1])
2815+
self.assertIs(returned[0].figure, fig)
2816+
2817+
with tm.assertRaises(ValueError):
2818+
fig, axes = self.plt.subplots(2, 3)
2819+
# pass different number of axes from required
2820+
axes = df.groupby('classroom').boxplot(ax=axes)
2821+
27152822
@slow
27162823
def test_grouped_hist_layout(self):
27172824

@@ -2724,12 +2831,12 @@ def test_grouped_hist_layout(self):
27242831
axes = _check_plot_works(df.hist, column='height', by=df.gender, layout=(2, 1))
27252832
self._check_axes_shape(axes, axes_num=2, layout=(2, 1))
27262833

2727-
axes = _check_plot_works(df.hist, column='height', by=df.category, layout=(4, 1))
2834+
axes = df.hist(column='height', by=df.category, layout=(4, 1))
27282835
self._check_axes_shape(axes, axes_num=4, layout=(4, 1))
27292836

2730-
axes = _check_plot_works(df.hist, column='height', by=df.category,
2731-
layout=(4, 2), figsize=(12, 8))
2837+
axes = df.hist(column='height', by=df.category, layout=(4, 2), figsize=(12, 8))
27322838
self._check_axes_shape(axes, axes_num=4, layout=(4, 2), figsize=(12, 8))
2839+
tm.close()
27332840

27342841
# GH 6769
27352842
axes = _check_plot_works(df.hist, column='height', by='classroom', layout=(2, 2))
@@ -2739,13 +2846,32 @@ def test_grouped_hist_layout(self):
27392846
axes = _check_plot_works(df.hist, by='classroom')
27402847
self._check_axes_shape(axes, axes_num=3, layout=(2, 2))
27412848

2742-
axes = _check_plot_works(df.hist, by='gender', layout=(3, 5))
2849+
axes = df.hist(by='gender', layout=(3, 5))
27432850
self._check_axes_shape(axes, axes_num=2, layout=(3, 5))
27442851

2745-
axes = _check_plot_works(df.hist, column=['height', 'weight', 'category'])
2852+
axes = df.hist(column=['height', 'weight', 'category'])
27462853
self._check_axes_shape(axes, axes_num=3, layout=(2, 2))
27472854

27482855
@slow
2856+
def test_grouped_hist_multiple_axes(self):
2857+
# GH 6970, GH 7069
2858+
df = self.hist_df
2859+
2860+
fig, axes = self.plt.subplots(2, 3)
2861+
returned = df.hist(column=['height', 'weight', 'category'], ax=axes[0])
2862+
self._check_axes_shape(returned, axes_num=3, layout=(1, 3))
2863+
self.assert_numpy_array_equal(returned, axes[0])
2864+
self.assertIs(returned[0].figure, fig)
2865+
returned = df.hist(by='classroom', ax=axes[1])
2866+
self._check_axes_shape(returned, axes_num=3, layout=(1, 3))
2867+
self.assert_numpy_array_equal(returned, axes[1])
2868+
self.assertIs(returned[0].figure, fig)
2869+
2870+
with tm.assertRaises(ValueError):
2871+
fig, axes = self.plt.subplots(2, 3)
2872+
# pass different number of axes from required
2873+
axes = df.hist(column='height', ax=axes)
2874+
@slow
27492875
def test_axis_share_x(self):
27502876
df = self.hist_df
27512877
# GH4089

0 commit comments

Comments
 (0)