Skip to content

Commit f020cdd

Browse files
author
TomAugspurger
committed
API: accept -1 for layout
1 parent ccb196f commit f020cdd

File tree

4 files changed

+112
-5
lines changed

4 files changed

+112
-5
lines changed

doc/source/v0.15.0.txt

+3-1
Original file line numberDiff line numberDiff line change
@@ -624,7 +624,9 @@ Enhancements
624624

625625
- Added support for bool, uint8, uint16 and uint32 datatypes in ``to_stata`` (:issue:`7097`, :issue:`7365`)
626626

627-
- Added ``layout`` keyword to ``DataFrame.plot`` (:issue:`6667`)
627+
- Added ``layout`` keyword to ``DataFrame.plot``. You can pass a tuple of
628+
``(rows, columns)``, one of which can be ``-1`` to automatically
629+
infer (:issue:`6667`, :issue:`8071`).
628630
- Allow to pass multiple axes to ``DataFrame.plot``, ``hist`` and ``boxplot`` (:issue:`5353`, :issue:`6970`, :issue:`7069`)
629631
- Added support for ``c``, ``colormap`` and ``colorbar`` arguments for
630632
``DataFrame.plot`` with ``kind='scatter'`` (:issue:`7780`)

doc/source/visualization.rst

+11-1
Original file line numberDiff line numberDiff line change
@@ -1106,13 +1106,23 @@ The layout of subplots can be specified by ``layout`` keyword. It can accept
11061106

11071107
The number of axes which can be contained by rows x columns specified by ``layout`` must be
11081108
larger than the number of required subplots. If layout can contain more axes than required,
1109-
blank axes are not drawn.
1109+
blank axes are not drawn. Similar to a numpy array's ``reshape`` method, you
1110+
can use ``-1`` for one dimension to automatically calculate the number of rows
1111+
or columns needed, given the other.
11101112

11111113
.. ipython:: python
11121114
11131115
@savefig frame_plot_subplots_layout.png
11141116
df.plot(subplots=True, layout=(2, 3), figsize=(6, 6));
11151117
1118+
The above example is identical to using
1119+
1120+
.. ipython:: python
1121+
df.plot(subplots=True, layout=(-1, 3), figsize=(6, 6));
1122+
1123+
The required number of rows (2) is inferred from the number of series to plot
1124+
and the given number of columns (3).
1125+
11161126
Also, you can pass multiple axes created beforehand as list-like via ``ax`` keyword.
11171127
This allows to use more complicated layout.
11181128
The passed axes must be the same number as the subplots being drawn.

pandas/tests/test_graphics.py

+86-3
Original file line numberDiff line numberDiff line change
@@ -472,6 +472,11 @@ def test_plot(self):
472472
ax = _check_plot_works(self.ts.plot, subplots=True)
473473
self._check_axes_shape(ax, axes_num=1, layout=(1, 1))
474474

475+
ax = _check_plot_works(self.ts.plot, subplots=True, layout=(-1, 1))
476+
self._check_axes_shape(ax, axes_num=1, layout=(1, 1))
477+
ax = _check_plot_works(self.ts.plot, subplots=True, layout=(1, -1))
478+
self._check_axes_shape(ax, axes_num=1, layout=(1, 1))
479+
475480
@slow
476481
def test_plot_figsize_and_title(self):
477482
# figsize and title
@@ -677,9 +682,21 @@ def test_hist_layout_with_by(self):
677682
axes = _check_plot_works(df.height.hist, by=df.gender, layout=(2, 1))
678683
self._check_axes_shape(axes, axes_num=2, layout=(2, 1))
679684

685+
axes = _check_plot_works(df.height.hist, by=df.gender, layout=(3, -1))
686+
self._check_axes_shape(axes, axes_num=2, layout=(3, 1))
687+
680688
axes = _check_plot_works(df.height.hist, by=df.category, layout=(4, 1))
681689
self._check_axes_shape(axes, axes_num=4, layout=(4, 1))
682690

691+
axes = _check_plot_works(df.height.hist, by=df.category, layout=(2, -1))
692+
self._check_axes_shape(axes, axes_num=4, layout=(2, 2))
693+
694+
axes = _check_plot_works(df.height.hist, by=df.category, layout=(3, -1))
695+
self._check_axes_shape(axes, axes_num=4, layout=(3, 2))
696+
697+
axes = _check_plot_works(df.height.hist, by=df.category, layout=(-1, 4))
698+
self._check_axes_shape(axes, axes_num=4, layout=(1, 4))
699+
683700
axes = _check_plot_works(df.height.hist, by=df.classroom, layout=(2, 2))
684701
self._check_axes_shape(axes, axes_num=3, layout=(2, 2))
685702

@@ -927,7 +944,11 @@ def test_plot(self):
927944
_check_plot_works(df.plot, grid=False)
928945
axes = _check_plot_works(df.plot, subplots=True)
929946
self._check_axes_shape(axes, axes_num=4, layout=(4, 1))
930-
_check_plot_works(df.plot, subplots=True, use_index=False)
947+
948+
axes = _check_plot_works(df.plot, subplots=True, layout=(-1, 2))
949+
self._check_axes_shape(axes, axes_num=4, layout=(2, 2))
950+
951+
axes = _check_plot_works(df.plot, subplots=True, use_index=False)
931952
self._check_axes_shape(axes, axes_num=4, layout=(4, 1))
932953

933954
df = DataFrame({'x': [1, 2], 'y': [3, 4]})
@@ -985,6 +1006,9 @@ def test_plot(self):
9851006
axes = _check_plot_works(df.plot, kind='bar', subplots=True)
9861007
self._check_axes_shape(axes, axes_num=1, layout=(1, 1))
9871008

1009+
axes = _check_plot_works(df.plot, kind='bar', subplots=True,
1010+
layout=(-1, 1))
1011+
self._check_axes_shape(axes, axes_num=1, layout=(1, 1))
9881012
# When ax is supplied and required number of axes is 1,
9891013
# passed ax should be used:
9901014
fig, ax = self.plt.subplots()
@@ -1174,12 +1198,30 @@ def test_subplots_layout(self):
11741198
self._check_axes_shape(axes, axes_num=3, layout=(2, 2))
11751199
self.assertEqual(axes.shape, (2, 2))
11761200

1201+
axes = df.plot(subplots=True, layout=(-1, 2))
1202+
self._check_axes_shape(axes, axes_num=3, layout=(2, 2))
1203+
self.assertEqual(axes.shape, (2, 2))
1204+
1205+
axes = df.plot(subplots=True, layout=(2, -1))
1206+
self._check_axes_shape(axes, axes_num=3, layout=(2, 2))
1207+
self.assertEqual(axes.shape, (2, 2))
1208+
11771209
axes = df.plot(subplots=True, layout=(1, 4))
11781210
self._check_axes_shape(axes, axes_num=3, layout=(1, 4))
11791211
self.assertEqual(axes.shape, (1, 4))
11801212

1213+
axes = df.plot(subplots=True, layout=(-1, 4))
1214+
self._check_axes_shape(axes, axes_num=3, layout=(1, 4))
1215+
self.assertEqual(axes.shape, (1, 4))
1216+
1217+
axes = df.plot(subplots=True, layout=(4, -1))
1218+
self._check_axes_shape(axes, axes_num=3, layout=(4, 1))
1219+
self.assertEqual(axes.shape, (4, 1))
1220+
11811221
with tm.assertRaises(ValueError):
11821222
axes = df.plot(subplots=True, layout=(1, 1))
1223+
with tm.assertRaises(ValueError):
1224+
axes = df.plot(subplots=True, layout=(-1, -1))
11831225

11841226
# single column
11851227
df = DataFrame(np.random.rand(10, 1),
@@ -1228,6 +1270,14 @@ def test_subplots_multiple_axes(self):
12281270
self._check_axes_shape(returned, axes_num=4, layout=(2, 2))
12291271
self.assertEqual(returned.shape, (4, ))
12301272

1273+
returned = df.plot(subplots=True, ax=axes, layout=(2, -1))
1274+
self._check_axes_shape(returned, axes_num=4, layout=(2, 2))
1275+
self.assertEqual(returned.shape, (4, ))
1276+
1277+
returned = df.plot(subplots=True, ax=axes, layout=(-1, 2))
1278+
self._check_axes_shape(returned, axes_num=4, layout=(2, 2))
1279+
self.assertEqual(returned.shape, (4, ))
1280+
12311281
# single column
12321282
fig, axes = self.plt.subplots(1, 1)
12331283
df = DataFrame(np.random.rand(10, 1),
@@ -2135,6 +2185,10 @@ def test_hist_layout(self):
21352185
{'layout': (4, 1), 'expected_size': (4, 1)},
21362186
{'layout': (1, 4), 'expected_size': (1, 4)},
21372187
{'layout': (3, 3), 'expected_size': (3, 3)},
2188+
{'layout': (-1, 4), 'expected_size': (1, 4)},
2189+
{'layout': (4, -1), 'expected_size': (4, 1)},
2190+
{'layout': (-1, 2), 'expected_size': (2, 2)},
2191+
{'layout': (2, -1), 'expected_size': (2, 2)}
21382192
)
21392193

21402194
for layout_test in layout_to_expected_size:
@@ -2149,6 +2203,9 @@ def test_hist_layout(self):
21492203
# invalid format for layout
21502204
with tm.assertRaises(ValueError):
21512205
df.hist(layout=(1,))
2206+
with tm.assertRaises(ValueError):
2207+
df.hist(layout=(-1, -1))
2208+
21522209

21532210
@slow
21542211
def test_scatter(self):
@@ -3048,6 +3105,8 @@ def test_grouped_box_layout(self):
30483105
by=df.gender, layout=(1, 1))
30493106
self.assertRaises(ValueError, df.boxplot, column=['height', 'weight', 'category'],
30503107
layout=(2, 1), return_type='dict')
3108+
self.assertRaises(ValueError, df.boxplot, column=['weight', 'height'],
3109+
by=df.gender, layout=(-1, -1))
30513110

30523111
box = _check_plot_works(df.groupby('gender').boxplot, column='height',
30533112
return_type='dict')
@@ -3080,15 +3139,29 @@ def test_grouped_box_layout(self):
30803139
box = _check_plot_works(df.groupby('category').boxplot, column='height',
30813140
layout=(3, 2), return_type='dict')
30823141
self._check_axes_shape(self.plt.gcf().axes, axes_num=4, layout=(3, 2))
3142+
box = _check_plot_works(df.groupby('category').boxplot, column='height',
3143+
layout=(3, -1), return_type='dict')
3144+
self._check_axes_shape(self.plt.gcf().axes, axes_num=4, layout=(3, 2))
30833145

3084-
box = df.boxplot(column=['height', 'weight', 'category'], by='gender', layout=(4, 1))
3146+
box = df.boxplot(column=['height', 'weight', 'category'], by='gender',
3147+
layout=(4, 1))
30853148
self._check_axes_shape(self.plt.gcf().axes, axes_num=3, layout=(4, 1))
30863149

3150+
box = df.boxplot(column=['height', 'weight', 'category'], by='gender',
3151+
layout=(-1, 1))
3152+
self._check_axes_shape(self.plt.gcf().axes, axes_num=3, layout=(3, 1))
3153+
30873154
box = df.groupby('classroom').boxplot(
30883155
column=['height', 'weight', 'category'], layout=(1, 4),
30893156
return_type='dict')
30903157
self._check_axes_shape(self.plt.gcf().axes, axes_num=3, layout=(1, 4))
30913158

3159+
box = df.groupby('classroom').boxplot(
3160+
column=['height', 'weight', 'category'], layout=(1, -1),
3161+
return_type='dict')
3162+
self._check_axes_shape(self.plt.gcf().axes, axes_num=3, layout=(1, 3))
3163+
3164+
30923165
@slow
30933166
def test_grouped_box_multiple_axes(self):
30943167
# GH 6970, GH 7069
@@ -3132,13 +3205,23 @@ def test_grouped_hist_layout(self):
31323205
layout=(1, 1))
31333206
self.assertRaises(ValueError, df.hist, column='height', by=df.category,
31343207
layout=(1, 3))
3208+
self.assertRaises(ValueError, df.hist, column='height', by=df.category,
3209+
layout=(-1, -1))
3210+
3211+
axes = _check_plot_works(df.hist, column='height', by=df.gender,
3212+
layout=(2, 1))
3213+
self._check_axes_shape(axes, axes_num=2, layout=(2, 1))
31353214

3136-
axes = _check_plot_works(df.hist, column='height', by=df.gender, layout=(2, 1))
3215+
axes = _check_plot_works(df.hist, column='height', by=df.gender,
3216+
layout=(2, -1))
31373217
self._check_axes_shape(axes, axes_num=2, layout=(2, 1))
31383218

31393219
axes = df.hist(column='height', by=df.category, layout=(4, 1))
31403220
self._check_axes_shape(axes, axes_num=4, layout=(4, 1))
31413221

3222+
axes = df.hist(column='height', by=df.category, layout=(-1, 1))
3223+
self._check_axes_shape(axes, axes_num=4, layout=(4, 1))
3224+
31423225
axes = df.hist(column='height', by=df.category, layout=(4, 2), figsize=(12, 8))
31433226
self._check_axes_shape(axes, axes_num=4, layout=(4, 2), figsize=(12, 8))
31443227
tm.close()

pandas/tools/plotting.py

+12
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import datetime
44
import warnings
55
import re
6+
from math import ceil
67
from collections import namedtuple
78
from contextlib import contextmanager
89
from distutils.version import LooseVersion
@@ -3059,6 +3060,17 @@ def _get_layout(nplots, layout=None, layout_type='box'):
30593060
raise ValueError('Layout must be a tuple of (rows, columns)')
30603061

30613062
nrows, ncols = layout
3063+
3064+
# Python 2 compat
3065+
ceil_ = lambda x: int(ceil(x))
3066+
if nrows == -1 and ncols >0:
3067+
layout = nrows, ncols = (ceil_(float(nplots) / ncols), ncols)
3068+
elif ncols == -1 and nrows > 0:
3069+
layout = nrows, ncols = (nrows, ceil_(float(nplots) / nrows))
3070+
elif ncols <= 0 and nrows <= 0:
3071+
msg = "At least one dimension of layout must be positive"
3072+
raise ValueError(msg)
3073+
30623074
if nrows * ncols < nplots:
30633075
raise ValueError('Layout of %sx%s must be larger than required size %s' %
30643076
(nrows, ncols, nplots))

0 commit comments

Comments
 (0)