From f020cdd44d8d2d53851b8ac37bf43eea3b3e21e1 Mon Sep 17 00:00:00 2001 From: TomAugspurger Date: Wed, 17 Sep 2014 13:49:08 -0500 Subject: [PATCH] API: accept -1 for layout --- doc/source/v0.15.0.txt | 4 +- doc/source/visualization.rst | 12 ++++- pandas/tests/test_graphics.py | 89 +++++++++++++++++++++++++++++++++-- pandas/tools/plotting.py | 12 +++++ 4 files changed, 112 insertions(+), 5 deletions(-) diff --git a/doc/source/v0.15.0.txt b/doc/source/v0.15.0.txt index 7d5327a68ee2e..74cffa7859a1d 100644 --- a/doc/source/v0.15.0.txt +++ b/doc/source/v0.15.0.txt @@ -624,7 +624,9 @@ Enhancements - Added support for bool, uint8, uint16 and uint32 datatypes in ``to_stata`` (:issue:`7097`, :issue:`7365`) -- Added ``layout`` keyword to ``DataFrame.plot`` (:issue:`6667`) +- Added ``layout`` keyword to ``DataFrame.plot``. You can pass a tuple of + ``(rows, columns)``, one of which can be ``-1`` to automatically + infer (:issue:`6667`, :issue:`8071`). - Allow to pass multiple axes to ``DataFrame.plot``, ``hist`` and ``boxplot`` (:issue:`5353`, :issue:`6970`, :issue:`7069`) - Added support for ``c``, ``colormap`` and ``colorbar`` arguments for ``DataFrame.plot`` with ``kind='scatter'`` (:issue:`7780`) diff --git a/doc/source/visualization.rst b/doc/source/visualization.rst index b64388df62bd7..32892a431cd29 100644 --- a/doc/source/visualization.rst +++ b/doc/source/visualization.rst @@ -1106,13 +1106,23 @@ The layout of subplots can be specified by ``layout`` keyword. It can accept The number of axes which can be contained by rows x columns specified by ``layout`` must be larger than the number of required subplots. If layout can contain more axes than required, -blank axes are not drawn. +blank axes are not drawn. Similar to a numpy array's ``reshape`` method, you +can use ``-1`` for one dimension to automatically calculate the number of rows +or columns needed, given the other. .. ipython:: python @savefig frame_plot_subplots_layout.png df.plot(subplots=True, layout=(2, 3), figsize=(6, 6)); +The above example is identical to using + +.. ipython:: python + df.plot(subplots=True, layout=(-1, 3), figsize=(6, 6)); + +The required number of rows (2) is inferred from the number of series to plot +and the given number of columns (3). + Also, you can pass multiple axes created beforehand as list-like via ``ax`` keyword. This allows to use more complicated layout. The passed axes must be the same number as the subplots being drawn. diff --git a/pandas/tests/test_graphics.py b/pandas/tests/test_graphics.py index 7ee532d7b1d3a..1e8cf4d700f39 100644 --- a/pandas/tests/test_graphics.py +++ b/pandas/tests/test_graphics.py @@ -472,6 +472,11 @@ def test_plot(self): ax = _check_plot_works(self.ts.plot, subplots=True) self._check_axes_shape(ax, axes_num=1, layout=(1, 1)) + ax = _check_plot_works(self.ts.plot, subplots=True, layout=(-1, 1)) + self._check_axes_shape(ax, axes_num=1, layout=(1, 1)) + ax = _check_plot_works(self.ts.plot, subplots=True, layout=(1, -1)) + self._check_axes_shape(ax, axes_num=1, layout=(1, 1)) + @slow def test_plot_figsize_and_title(self): # figsize and title @@ -677,9 +682,21 @@ def test_hist_layout_with_by(self): axes = _check_plot_works(df.height.hist, by=df.gender, layout=(2, 1)) self._check_axes_shape(axes, axes_num=2, layout=(2, 1)) + axes = _check_plot_works(df.height.hist, by=df.gender, layout=(3, -1)) + self._check_axes_shape(axes, axes_num=2, layout=(3, 1)) + axes = _check_plot_works(df.height.hist, by=df.category, layout=(4, 1)) self._check_axes_shape(axes, axes_num=4, layout=(4, 1)) + axes = _check_plot_works(df.height.hist, by=df.category, layout=(2, -1)) + self._check_axes_shape(axes, axes_num=4, layout=(2, 2)) + + axes = _check_plot_works(df.height.hist, by=df.category, layout=(3, -1)) + self._check_axes_shape(axes, axes_num=4, layout=(3, 2)) + + axes = _check_plot_works(df.height.hist, by=df.category, layout=(-1, 4)) + self._check_axes_shape(axes, axes_num=4, layout=(1, 4)) + axes = _check_plot_works(df.height.hist, by=df.classroom, layout=(2, 2)) self._check_axes_shape(axes, axes_num=3, layout=(2, 2)) @@ -927,7 +944,11 @@ def test_plot(self): _check_plot_works(df.plot, grid=False) axes = _check_plot_works(df.plot, subplots=True) self._check_axes_shape(axes, axes_num=4, layout=(4, 1)) - _check_plot_works(df.plot, subplots=True, use_index=False) + + axes = _check_plot_works(df.plot, subplots=True, layout=(-1, 2)) + self._check_axes_shape(axes, axes_num=4, layout=(2, 2)) + + axes = _check_plot_works(df.plot, subplots=True, use_index=False) self._check_axes_shape(axes, axes_num=4, layout=(4, 1)) df = DataFrame({'x': [1, 2], 'y': [3, 4]}) @@ -985,6 +1006,9 @@ def test_plot(self): axes = _check_plot_works(df.plot, kind='bar', subplots=True) self._check_axes_shape(axes, axes_num=1, layout=(1, 1)) + axes = _check_plot_works(df.plot, kind='bar', subplots=True, + layout=(-1, 1)) + self._check_axes_shape(axes, axes_num=1, layout=(1, 1)) # When ax is supplied and required number of axes is 1, # passed ax should be used: fig, ax = self.plt.subplots() @@ -1174,12 +1198,30 @@ def test_subplots_layout(self): self._check_axes_shape(axes, axes_num=3, layout=(2, 2)) self.assertEqual(axes.shape, (2, 2)) + axes = df.plot(subplots=True, layout=(-1, 2)) + self._check_axes_shape(axes, axes_num=3, layout=(2, 2)) + self.assertEqual(axes.shape, (2, 2)) + + axes = df.plot(subplots=True, layout=(2, -1)) + self._check_axes_shape(axes, axes_num=3, layout=(2, 2)) + self.assertEqual(axes.shape, (2, 2)) + axes = df.plot(subplots=True, layout=(1, 4)) self._check_axes_shape(axes, axes_num=3, layout=(1, 4)) self.assertEqual(axes.shape, (1, 4)) + axes = df.plot(subplots=True, layout=(-1, 4)) + self._check_axes_shape(axes, axes_num=3, layout=(1, 4)) + self.assertEqual(axes.shape, (1, 4)) + + axes = df.plot(subplots=True, layout=(4, -1)) + self._check_axes_shape(axes, axes_num=3, layout=(4, 1)) + self.assertEqual(axes.shape, (4, 1)) + with tm.assertRaises(ValueError): axes = df.plot(subplots=True, layout=(1, 1)) + with tm.assertRaises(ValueError): + axes = df.plot(subplots=True, layout=(-1, -1)) # single column df = DataFrame(np.random.rand(10, 1), @@ -1228,6 +1270,14 @@ def test_subplots_multiple_axes(self): self._check_axes_shape(returned, axes_num=4, layout=(2, 2)) self.assertEqual(returned.shape, (4, )) + returned = df.plot(subplots=True, ax=axes, layout=(2, -1)) + self._check_axes_shape(returned, axes_num=4, layout=(2, 2)) + self.assertEqual(returned.shape, (4, )) + + returned = df.plot(subplots=True, ax=axes, layout=(-1, 2)) + self._check_axes_shape(returned, axes_num=4, layout=(2, 2)) + self.assertEqual(returned.shape, (4, )) + # single column fig, axes = self.plt.subplots(1, 1) df = DataFrame(np.random.rand(10, 1), @@ -2135,6 +2185,10 @@ def test_hist_layout(self): {'layout': (4, 1), 'expected_size': (4, 1)}, {'layout': (1, 4), 'expected_size': (1, 4)}, {'layout': (3, 3), 'expected_size': (3, 3)}, + {'layout': (-1, 4), 'expected_size': (1, 4)}, + {'layout': (4, -1), 'expected_size': (4, 1)}, + {'layout': (-1, 2), 'expected_size': (2, 2)}, + {'layout': (2, -1), 'expected_size': (2, 2)} ) for layout_test in layout_to_expected_size: @@ -2149,6 +2203,9 @@ def test_hist_layout(self): # invalid format for layout with tm.assertRaises(ValueError): df.hist(layout=(1,)) + with tm.assertRaises(ValueError): + df.hist(layout=(-1, -1)) + @slow def test_scatter(self): @@ -3048,6 +3105,8 @@ def test_grouped_box_layout(self): by=df.gender, layout=(1, 1)) self.assertRaises(ValueError, df.boxplot, column=['height', 'weight', 'category'], layout=(2, 1), return_type='dict') + self.assertRaises(ValueError, df.boxplot, column=['weight', 'height'], + by=df.gender, layout=(-1, -1)) box = _check_plot_works(df.groupby('gender').boxplot, column='height', return_type='dict') @@ -3080,15 +3139,29 @@ def test_grouped_box_layout(self): box = _check_plot_works(df.groupby('category').boxplot, column='height', layout=(3, 2), return_type='dict') self._check_axes_shape(self.plt.gcf().axes, axes_num=4, layout=(3, 2)) + box = _check_plot_works(df.groupby('category').boxplot, column='height', + layout=(3, -1), return_type='dict') + self._check_axes_shape(self.plt.gcf().axes, axes_num=4, layout=(3, 2)) - box = df.boxplot(column=['height', 'weight', 'category'], by='gender', layout=(4, 1)) + box = df.boxplot(column=['height', 'weight', 'category'], by='gender', + layout=(4, 1)) self._check_axes_shape(self.plt.gcf().axes, axes_num=3, layout=(4, 1)) + box = df.boxplot(column=['height', 'weight', 'category'], by='gender', + layout=(-1, 1)) + self._check_axes_shape(self.plt.gcf().axes, axes_num=3, layout=(3, 1)) + box = df.groupby('classroom').boxplot( column=['height', 'weight', 'category'], layout=(1, 4), return_type='dict') self._check_axes_shape(self.plt.gcf().axes, axes_num=3, layout=(1, 4)) + box = df.groupby('classroom').boxplot( + column=['height', 'weight', 'category'], layout=(1, -1), + return_type='dict') + self._check_axes_shape(self.plt.gcf().axes, axes_num=3, layout=(1, 3)) + + @slow def test_grouped_box_multiple_axes(self): # GH 6970, GH 7069 @@ -3132,13 +3205,23 @@ def test_grouped_hist_layout(self): layout=(1, 1)) self.assertRaises(ValueError, df.hist, column='height', by=df.category, layout=(1, 3)) + self.assertRaises(ValueError, df.hist, column='height', by=df.category, + layout=(-1, -1)) + + axes = _check_plot_works(df.hist, column='height', by=df.gender, + layout=(2, 1)) + self._check_axes_shape(axes, axes_num=2, layout=(2, 1)) - axes = _check_plot_works(df.hist, column='height', by=df.gender, layout=(2, 1)) + axes = _check_plot_works(df.hist, column='height', by=df.gender, + layout=(2, -1)) self._check_axes_shape(axes, axes_num=2, layout=(2, 1)) axes = df.hist(column='height', by=df.category, layout=(4, 1)) self._check_axes_shape(axes, axes_num=4, layout=(4, 1)) + axes = df.hist(column='height', by=df.category, layout=(-1, 1)) + self._check_axes_shape(axes, axes_num=4, layout=(4, 1)) + axes = df.hist(column='height', by=df.category, layout=(4, 2), figsize=(12, 8)) self._check_axes_shape(axes, axes_num=4, layout=(4, 2), figsize=(12, 8)) tm.close() diff --git a/pandas/tools/plotting.py b/pandas/tools/plotting.py index 9556e6b81c356..f8d7a16e686b7 100644 --- a/pandas/tools/plotting.py +++ b/pandas/tools/plotting.py @@ -3,6 +3,7 @@ import datetime import warnings import re +from math import ceil from collections import namedtuple from contextlib import contextmanager from distutils.version import LooseVersion @@ -3059,6 +3060,17 @@ def _get_layout(nplots, layout=None, layout_type='box'): raise ValueError('Layout must be a tuple of (rows, columns)') nrows, ncols = layout + + # Python 2 compat + ceil_ = lambda x: int(ceil(x)) + if nrows == -1 and ncols >0: + layout = nrows, ncols = (ceil_(float(nplots) / ncols), ncols) + elif ncols == -1 and nrows > 0: + layout = nrows, ncols = (nrows, ceil_(float(nplots) / nrows)) + elif ncols <= 0 and nrows <= 0: + msg = "At least one dimension of layout must be positive" + raise ValueError(msg) + if nrows * ncols < nplots: raise ValueError('Layout of %sx%s must be larger than required size %s' % (nrows, ncols, nplots))