Skip to content

Commit 74610b0

Browse files
author
Tom Augspurger
committed
Merge pull request #7736 from sinhrks/multi_ax
ENH: plot functions accept multiple axes and layout kw
2 parents 931b466 + 1404ce9 commit 74610b0

File tree

4 files changed

+286
-106
lines changed

4 files changed

+286
-106
lines changed

doc/source/v0.15.0.txt

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

430+
- Added ``layout`` keyword to ``DataFrame.plot`` (:issue:`6667`)
431+
- Allow to pass multiple axes to ``DataFrame.plot``, ``hist`` and ``boxplot`` (:issue:`5353`, :issue:`6970`, :issue:`7069`)
432+
430433

431434
- ``PeriodIndex`` supports ``resolution`` as the same as ``DatetimeIndex`` (:issue:`7708`)
432435
- ``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
@@ -992,10 +992,41 @@ with the ``subplots`` keyword:
992992
@savefig frame_plot_subplots.png
993993
df.plot(subplots=True, figsize=(6, 6));
994994
995-
Targeting Different Subplots
996-
~~~~~~~~~~~~~~~~~~~~~~~~~~~~
995+
Using Layout and Targetting Multiple Axes
996+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
997997

998-
You can pass an ``ax`` argument to :meth:`Series.plot` to plot on a particular axis:
998+
The layout of subplots can be specified by ``layout`` keyword. It can accept
999+
``(rows, columns)``. The ``layout`` keyword can be used in
1000+
``hist`` and ``boxplot`` also. If input is invalid, ``ValueError`` will be raised.
1001+
1002+
The number of axes which can be contained by rows x columns specified by ``layout`` must be
1003+
larger than the number of required subplots. If layout can contain more axes than required,
1004+
blank axes are not drawn.
1005+
1006+
.. ipython:: python
1007+
1008+
@savefig frame_plot_subplots_layout.png
1009+
df.plot(subplots=True, layout=(2, 3), figsize=(6, 6));
1010+
1011+
Also, you can pass multiple axes created beforehand as list-like via ``ax`` keyword.
1012+
This allows to use more complicated layout.
1013+
The passed axes must be the same number as the subplots being drawn.
1014+
1015+
When multiple axes are passed via ``ax`` keyword, ``layout``, ``sharex`` and ``sharey`` keywords are ignored.
1016+
These must be configured when creating axes.
1017+
1018+
.. ipython:: python
1019+
1020+
fig, axes = plt.subplots(4, 4, figsize=(6, 6));
1021+
plt.adjust_subplots(wspace=0.5, hspace=0.5);
1022+
target1 = [axes[0][0], axes[1][1], axes[2][2], axes[3][3]]
1023+
target2 = [axes[3][0], axes[2][1], axes[1][2], axes[0][3]]
1024+
1025+
df.plot(subplots=True, ax=target1, legend=False);
1026+
@savefig frame_plot_subplots_multi_ax.png
1027+
(-df).plot(subplots=True, ax=target2, legend=False);
1028+
1029+
Another option is passing an ``ax`` argument to :meth:`Series.plot` to plot on a particular axis:
9991030

10001031
.. ipython:: python
10011032
:suppress:
@@ -1010,12 +1041,12 @@ You can pass an ``ax`` argument to :meth:`Series.plot` to plot on a particular a
10101041
.. ipython:: python
10111042
10121043
fig, axes = plt.subplots(nrows=2, ncols=2)
1013-
df['A'].plot(ax=axes[0,0]); axes[0,0].set_title('A')
1014-
df['B'].plot(ax=axes[0,1]); axes[0,1].set_title('B')
1015-
df['C'].plot(ax=axes[1,0]); axes[1,0].set_title('C')
1044+
df['A'].plot(ax=axes[0,0]); axes[0,0].set_title('A');
1045+
df['B'].plot(ax=axes[0,1]); axes[0,1].set_title('B');
1046+
df['C'].plot(ax=axes[1,0]); axes[1,0].set_title('C');
10161047
10171048
@savefig series_plot_multi.png
1018-
df['D'].plot(ax=axes[1,1]); axes[1,1].set_title('D')
1049+
df['D'].plot(ax=axes[1,1]); axes[1,1].set_title('D');
10191050
10201051
.. ipython:: python
10211052
:suppress:

pandas/tests/test_graphics.py

+132-6
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]),
@@ -2733,6 +2805,41 @@ def test_grouped_box_layout(self):
27332805
return_type='dict')
27342806
self._check_axes_shape(self.plt.gcf().axes, axes_num=3, layout=(1, 4))
27352807

2808+
@slow
2809+
def test_grouped_box_multiple_axes(self):
2810+
# GH 6970, GH 7069
2811+
df = self.hist_df
2812+
2813+
# check warning to ignore sharex / sharey
2814+
# this check should be done in the first function which
2815+
# passes multiple axes to plot, hist or boxplot
2816+
# location should be changed if other test is added
2817+
# which has earlier alphabetical order
2818+
with tm.assert_produces_warning(UserWarning):
2819+
fig, axes = self.plt.subplots(2, 2)
2820+
df.groupby('category').boxplot(column='height', return_type='axes', ax=axes)
2821+
self._check_axes_shape(self.plt.gcf().axes, axes_num=4, layout=(2, 2))
2822+
2823+
fig, axes = self.plt.subplots(2, 3)
2824+
returned = df.boxplot(column=['height', 'weight', 'category'], by='gender',
2825+
return_type='axes', ax=axes[0])
2826+
returned = np.array(returned.values())
2827+
self._check_axes_shape(returned, axes_num=3, layout=(1, 3))
2828+
self.assert_numpy_array_equal(returned, axes[0])
2829+
self.assertIs(returned[0].figure, fig)
2830+
# draw on second row
2831+
returned = df.groupby('classroom').boxplot(column=['height', 'weight', 'category'],
2832+
return_type='axes', ax=axes[1])
2833+
returned = np.array(returned.values())
2834+
self._check_axes_shape(returned, axes_num=3, layout=(1, 3))
2835+
self.assert_numpy_array_equal(returned, axes[1])
2836+
self.assertIs(returned[0].figure, fig)
2837+
2838+
with tm.assertRaises(ValueError):
2839+
fig, axes = self.plt.subplots(2, 3)
2840+
# pass different number of axes from required
2841+
axes = df.groupby('classroom').boxplot(ax=axes)
2842+
27362843
@slow
27372844
def test_grouped_hist_layout(self):
27382845

@@ -2745,12 +2852,12 @@ def test_grouped_hist_layout(self):
27452852
axes = _check_plot_works(df.hist, column='height', by=df.gender, layout=(2, 1))
27462853
self._check_axes_shape(axes, axes_num=2, layout=(2, 1))
27472854

2748-
axes = _check_plot_works(df.hist, column='height', by=df.category, layout=(4, 1))
2855+
axes = df.hist(column='height', by=df.category, layout=(4, 1))
27492856
self._check_axes_shape(axes, axes_num=4, layout=(4, 1))
27502857

2751-
axes = _check_plot_works(df.hist, column='height', by=df.category,
2752-
layout=(4, 2), figsize=(12, 8))
2858+
axes = df.hist(column='height', by=df.category, layout=(4, 2), figsize=(12, 8))
27532859
self._check_axes_shape(axes, axes_num=4, layout=(4, 2), figsize=(12, 8))
2860+
tm.close()
27542861

27552862
# GH 6769
27562863
axes = _check_plot_works(df.hist, column='height', by='classroom', layout=(2, 2))
@@ -2760,13 +2867,32 @@ def test_grouped_hist_layout(self):
27602867
axes = _check_plot_works(df.hist, by='classroom')
27612868
self._check_axes_shape(axes, axes_num=3, layout=(2, 2))
27622869

2763-
axes = _check_plot_works(df.hist, by='gender', layout=(3, 5))
2870+
axes = df.hist(by='gender', layout=(3, 5))
27642871
self._check_axes_shape(axes, axes_num=2, layout=(3, 5))
27652872

2766-
axes = _check_plot_works(df.hist, column=['height', 'weight', 'category'])
2873+
axes = df.hist(column=['height', 'weight', 'category'])
27672874
self._check_axes_shape(axes, axes_num=3, layout=(2, 2))
27682875

27692876
@slow
2877+
def test_grouped_hist_multiple_axes(self):
2878+
# GH 6970, GH 7069
2879+
df = self.hist_df
2880+
2881+
fig, axes = self.plt.subplots(2, 3)
2882+
returned = df.hist(column=['height', 'weight', 'category'], ax=axes[0])
2883+
self._check_axes_shape(returned, axes_num=3, layout=(1, 3))
2884+
self.assert_numpy_array_equal(returned, axes[0])
2885+
self.assertIs(returned[0].figure, fig)
2886+
returned = df.hist(by='classroom', ax=axes[1])
2887+
self._check_axes_shape(returned, axes_num=3, layout=(1, 3))
2888+
self.assert_numpy_array_equal(returned, axes[1])
2889+
self.assertIs(returned[0].figure, fig)
2890+
2891+
with tm.assertRaises(ValueError):
2892+
fig, axes = self.plt.subplots(2, 3)
2893+
# pass different number of axes from required
2894+
axes = df.hist(column='height', ax=axes)
2895+
@slow
27702896
def test_axis_share_x(self):
27712897
df = self.hist_df
27722898
# GH4089

0 commit comments

Comments
 (0)