Skip to content

API: accept -1 for layout in plot functions #8297

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Sep 18, 2014
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion doc/source/v0.15.0.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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`)
Expand Down
12 changes: 11 additions & 1 deletion doc/source/visualization.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
89 changes: 86 additions & 3 deletions pandas/tests/test_graphics.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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))

Expand Down Expand Up @@ -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]})
Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -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),
Expand Down Expand Up @@ -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),
Expand Down Expand Up @@ -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:
Expand All @@ -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):
Expand Down Expand Up @@ -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')
Expand Down Expand Up @@ -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))

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

do you need a test case for (-1,5) where their are only 4 plots? (e.g. the reshape is too big/small) ?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have one of those here (and probably elsewhere).

The tests are scattered all over because I decided to put them in with wherever layout was already used.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Doesn't look like my link worked; it's line 1218. I've got a too small test on line 3208.

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
Expand Down Expand Up @@ -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()
Expand Down
12 changes: 12 additions & 0 deletions pandas/tools/plotting.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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))
Expand Down