Skip to content

Commit 49aece0

Browse files
author
Tom Augspurger
committed
Merge pull request #6976 from sinhrks/pie
ENH: support pie plot in series and dataframe plot
2 parents e54fbee + 775057b commit 49aece0

File tree

6 files changed

+258
-11
lines changed

6 files changed

+258
-11
lines changed

doc/source/conf.py

+1
Original file line numberDiff line numberDiff line change
@@ -277,6 +277,7 @@
277277
# Example configuration for intersphinx: refer to the Python standard library.
278278
intersphinx_mapping = {
279279
'statsmodels': ('http://statsmodels.sourceforge.net/devel/', None),
280+
'matplotlib': ('http://matplotlib.org/', None),
280281
'python': ('http://docs.python.org/', None)
281282
}
282283
import glob

doc/source/release.rst

+1
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@ New features
5858
``DataFrame(dict)`` and ``Series(dict)`` create ``MultiIndex``
5959
columns and index where applicable (:issue:`4187`)
6060
- Hexagonal bin plots from ``DataFrame.plot`` with ``kind='hexbin'`` (:issue:`5478`)
61+
- Pie plots from ``Series.plot`` and ``DataFrame.plot`` with ``kind='pie'`` (:issue:`6976`)
6162
- Added the ``sym_diff`` method to ``Index`` (:issue:`5543`)
6263
- Added ``to_julian_date`` to ``TimeStamp`` and ``DatetimeIndex``. The Julian
6364
Date is used primarily in astronomy and represents the number of days from

doc/source/v0.14.0.txt

+2-1
Original file line numberDiff line numberDiff line change
@@ -366,7 +366,8 @@ Plotting
366366
~~~~~~~~
367367

368368
- Hexagonal bin plots from ``DataFrame.plot`` with ``kind='hexbin'`` (:issue:`5478`), See :ref:`the docs<visualization.hexbin>`.
369-
- ``DataFrame.plot`` and ``Series.plot`` now supports area plot with specifying ``kind='area'`` (:issue:`6656`)
369+
- ``DataFrame.plot`` and ``Series.plot`` now supports area plot with specifying ``kind='area'`` (:issue:`6656`), See :ref:`the docs<visualization.area>`
370+
- Pie plots from ``Series.plot`` and ``DataFrame.plot`` with ``kind='pie'`` (:issue:`6976`), See :ref:`the docs<visualization.pie>`.
370371
- Plotting with Error Bars is now supported in the ``.plot`` method of ``DataFrame`` and ``Series`` objects (:issue:`3796`, :issue:`6834`), See :ref:`the docs<visualization.errorbars>`.
371372
- ``DataFrame.plot`` and ``Series.plot`` now support a ``table`` keyword for plotting ``matplotlib.Table``, See :ref:`the docs<visualization.table>`.
372373
- ``plot(legend='reverse')`` will now reverse the order of legend labels for

doc/source/visualization.rst

+74
Original file line numberDiff line numberDiff line change
@@ -588,6 +588,80 @@ given by column ``z``. The bins are aggregated with numpy's ``max`` function.
588588
589589
See the `matplotlib hexbin documenation <http://matplotlib.org/api/pyplot_api.html#matplotlib.pyplot.hexbin>`__ for more.
590590

591+
.. _visualization.pie:
592+
593+
Pie plot
594+
~~~~~~~~~~~~~~~~~~
595+
596+
.. versionadded:: 0.14
597+
598+
You can create pie plot with ``DataFrame.plot`` or ``Series.plot`` with ``kind='pie'``.
599+
If data includes ``NaN``, it will be automatically filled by 0.
600+
If data contains negative value, ``ValueError`` will be raised.
601+
602+
.. ipython:: python
603+
:suppress:
604+
605+
plt.figure()
606+
607+
.. ipython:: python
608+
609+
series = Series(3 * rand(4), index=['a', 'b', 'c', 'd'], name='series')
610+
611+
@savefig series_pie_plot.png
612+
series.plot(kind='pie')
613+
614+
Note that pie plot with ``DataFrame`` requires either to specify target column by ``y``
615+
argument or ``subplots=True``. When ``y`` is specified, pie plot of selected column
616+
will be drawn. If ``subplots=True`` is specified, pie plots for each columns are drawn as subplots.
617+
Legend will be drawn in each pie plots by default, specify ``legend=False`` to hide it.
618+
619+
.. ipython:: python
620+
:suppress:
621+
622+
plt.figure()
623+
624+
.. ipython:: python
625+
626+
df = DataFrame(3 * rand(4, 2), index=['a', 'b', 'c', 'd'], columns=['x', 'y'])
627+
628+
@savefig df_pie_plot.png
629+
df.plot(kind='pie', subplots=True)
630+
631+
You can use ``labels`` and ``colors`` keywords to specify labels and colors of each wedges
632+
(Cannot use ``label`` and ``color``, because of matplotlib's specification).
633+
If you want to hide wedge labels, specify ``labels=None``.
634+
If ``fontsize`` is specified, the value will be applied to wedge labels.
635+
Also, other keywords supported by :func:`matplotlib.pyplot.pie` can be used.
636+
637+
638+
.. ipython:: python
639+
:suppress:
640+
641+
plt.figure()
642+
643+
.. ipython:: python
644+
645+
@savefig series_pie_plot_options.png
646+
series.plot(kind='pie', labels=['AA', 'BB', 'CC', 'DD'], colors=['r', 'g', 'b', 'c'],
647+
autopct='%.2f', fontsize=20)
648+
649+
If you pass values which sum total is less than 1.0, matplotlib draws semicircle.
650+
651+
.. ipython:: python
652+
:suppress:
653+
654+
plt.figure()
655+
656+
.. ipython:: python
657+
658+
series = Series([0.1] * 4, index=['a', 'b', 'c', 'd'], name='series2')
659+
660+
@savefig series_pie_plot_semi.png
661+
series.plot(kind='pie')
662+
663+
See the `matplotlib pie documenation <http://matplotlib.org/api/pyplot_api.html#matplotlib.pyplot.pie>`__ for more.
664+
591665
.. _visualization.andrews_curves:
592666

593667
Andrews Curves

pandas/tests/test_graphics.py

+92
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
# coding: utf-8
33

44
import nose
5+
import itertools
56
import os
67
import string
78
from distutils.version import LooseVersion
@@ -138,6 +139,63 @@ def test_irregular_datetime(self):
138139
ax.set_xlim('1/1/1999', '1/1/2001')
139140
self.assertEqual(xp, ax.get_xlim()[0])
140141

142+
@slow
143+
def test_pie_series(self):
144+
# if sum of values is less than 1.0, pie handle them as rate and draw semicircle.
145+
series = Series(np.random.randint(1, 5),
146+
index=['a', 'b', 'c', 'd', 'e'], name='YLABEL')
147+
ax = _check_plot_works(series.plot, kind='pie')
148+
for t, expected in zip(ax.texts, series.index):
149+
self.assertEqual(t.get_text(), expected)
150+
self.assertEqual(ax.get_ylabel(), 'YLABEL')
151+
152+
# without wedge labels
153+
ax = _check_plot_works(series.plot, kind='pie', labels=None)
154+
for t, expected in zip(ax.texts, [''] * 5):
155+
self.assertEqual(t.get_text(), expected)
156+
157+
# with less colors than elements
158+
color_args = ['r', 'g', 'b']
159+
ax = _check_plot_works(series.plot, kind='pie', colors=color_args)
160+
161+
import matplotlib.colors as colors
162+
conv = colors.colorConverter
163+
color_expected = ['r', 'g', 'b', 'r', 'g']
164+
for p, expected in zip(ax.patches, color_expected):
165+
self.assertEqual(p.get_facecolor(), conv.to_rgba(expected))
166+
167+
# with labels and colors
168+
labels = ['A', 'B', 'C', 'D', 'E']
169+
color_args = ['r', 'g', 'b', 'c', 'm']
170+
ax = _check_plot_works(series.plot, kind='pie', labels=labels, colors=color_args)
171+
172+
for t, expected in zip(ax.texts, labels):
173+
self.assertEqual(t.get_text(), expected)
174+
for p, expected in zip(ax.patches, color_args):
175+
self.assertEqual(p.get_facecolor(), conv.to_rgba(expected))
176+
177+
# with autopct and fontsize
178+
ax = _check_plot_works(series.plot, kind='pie', colors=color_args,
179+
autopct='%.2f', fontsize=7)
180+
pcts = ['{0:.2f}'.format(s * 100) for s in series.values / float(series.sum())]
181+
iters = [iter(series.index), iter(pcts)]
182+
expected_texts = list(it.next() for it in itertools.cycle(iters))
183+
for t, expected in zip(ax.texts, expected_texts):
184+
self.assertEqual(t.get_text(), expected)
185+
self.assertEqual(t.get_fontsize(), 7)
186+
187+
# includes negative value
188+
with tm.assertRaises(ValueError):
189+
series = Series([1, 2, 0, 4, -1], index=['a', 'b', 'c', 'd', 'e'])
190+
series.plot(kind='pie')
191+
192+
# includes nan
193+
series = Series([1, 2, np.nan, 4],
194+
index=['a', 'b', 'c', 'd'], name='YLABEL')
195+
ax = _check_plot_works(series.plot, kind='pie')
196+
for t, expected in zip(ax.texts, series.index):
197+
self.assertEqual(t.get_text(), expected)
198+
141199
@slow
142200
def test_hist(self):
143201
_check_plot_works(self.ts.hist)
@@ -1511,6 +1569,39 @@ def test_allow_cmap(self):
15111569
df.plot(kind='hexbin', x='A', y='B', cmap='YlGn',
15121570
colormap='BuGn')
15131571

1572+
@slow
1573+
def test_pie_df(self):
1574+
df = DataFrame(np.random.rand(5, 3), columns=['X', 'Y', 'Z'],
1575+
index=['a', 'b', 'c', 'd', 'e'])
1576+
with tm.assertRaises(ValueError):
1577+
df.plot(kind='pie')
1578+
1579+
ax = _check_plot_works(df.plot, kind='pie', y='Y')
1580+
for t, expected in zip(ax.texts, df.index):
1581+
self.assertEqual(t.get_text(), expected)
1582+
1583+
axes = _check_plot_works(df.plot, kind='pie', subplots=True)
1584+
self.assertEqual(len(axes), len(df.columns))
1585+
for ax in axes:
1586+
for t, expected in zip(ax.texts, df.index):
1587+
self.assertEqual(t.get_text(), expected)
1588+
for ax, ylabel in zip(axes, df.columns):
1589+
self.assertEqual(ax.get_ylabel(), ylabel)
1590+
1591+
labels = ['A', 'B', 'C', 'D', 'E']
1592+
color_args = ['r', 'g', 'b', 'c', 'm']
1593+
axes = _check_plot_works(df.plot, kind='pie', subplots=True,
1594+
labels=labels, colors=color_args)
1595+
self.assertEqual(len(axes), len(df.columns))
1596+
1597+
import matplotlib.colors as colors
1598+
conv = colors.colorConverter
1599+
for ax in axes:
1600+
for t, expected in zip(ax.texts, labels):
1601+
self.assertEqual(t.get_text(), expected)
1602+
for p, expected in zip(ax.patches, color_args):
1603+
self.assertEqual(p.get_facecolor(), conv.to_rgba(expected))
1604+
15141605
def test_errorbar_plot(self):
15151606

15161607
d = {'x': np.arange(12), 'y': np.arange(12, 0, -1)}
@@ -1918,6 +2009,7 @@ def _check_plot_works(f, *args, **kwargs):
19182009
plt.savefig(path)
19192010
finally:
19202011
tm.close(fig)
2012+
19212013
return ret
19222014

19232015

pandas/tools/plotting.py

+88-10
Original file line numberDiff line numberDiff line change
@@ -1251,16 +1251,17 @@ def _get_style(self, i, col_name):
12511251

12521252
return style or None
12531253

1254-
def _get_colors(self):
1254+
def _get_colors(self, num_colors=None, color_kwds='color'):
12551255
from pandas.core.frame import DataFrame
1256-
if isinstance(self.data, DataFrame):
1257-
num_colors = len(self.data.columns)
1258-
else:
1259-
num_colors = 1
1256+
if num_colors is None:
1257+
if isinstance(self.data, DataFrame):
1258+
num_colors = len(self.data.columns)
1259+
else:
1260+
num_colors = 1
12601261

12611262
return _get_standard_colors(num_colors=num_colors,
12621263
colormap=self.colormap,
1263-
color=self.kwds.get('color'))
1264+
color=self.kwds.get(color_kwds))
12641265

12651266
def _maybe_add_color(self, colors, kwds, style, i):
12661267
has_color = 'color' in kwds or self.colormap is not None
@@ -1939,6 +1940,63 @@ def _post_plot_logic(self):
19391940
# self.axes[0].legend(loc='best')
19401941

19411942

1943+
class PiePlot(MPLPlot):
1944+
1945+
def __init__(self, data, kind=None, **kwargs):
1946+
data = data.fillna(value=0)
1947+
if (data < 0).any().any():
1948+
raise ValueError("{0} doesn't allow negative values".format(kind))
1949+
MPLPlot.__init__(self, data, kind=kind, **kwargs)
1950+
1951+
def _args_adjust(self):
1952+
self.grid = False
1953+
self.logy = False
1954+
self.logx = False
1955+
self.loglog = False
1956+
1957+
def _get_layout(self):
1958+
from pandas import DataFrame
1959+
if isinstance(self.data, DataFrame):
1960+
return (1, len(self.data.columns))
1961+
else:
1962+
return (1, 1)
1963+
1964+
def _validate_color_args(self):
1965+
pass
1966+
1967+
def _make_plot(self):
1968+
self.kwds.setdefault('colors', self._get_colors(num_colors=len(self.data),
1969+
color_kwds='colors'))
1970+
1971+
for i, (label, y) in enumerate(self._iter_data()):
1972+
ax = self._get_ax(i)
1973+
if label is not None:
1974+
label = com.pprint_thing(label)
1975+
ax.set_ylabel(label)
1976+
1977+
kwds = self.kwds.copy()
1978+
1979+
idx = [com.pprint_thing(v) for v in self.data.index]
1980+
labels = kwds.pop('labels', idx)
1981+
# labels is used for each wedge's labels
1982+
results = ax.pie(y, labels=labels, **kwds)
1983+
1984+
if kwds.get('autopct', None) is not None:
1985+
patches, texts, autotexts = results
1986+
else:
1987+
patches, texts = results
1988+
autotexts = []
1989+
1990+
if self.fontsize is not None:
1991+
for t in texts + autotexts:
1992+
t.set_fontsize(self.fontsize)
1993+
1994+
# leglabels is used for legend labels
1995+
leglabels = labels if labels is not None else idx
1996+
for p, l in zip(patches, leglabels):
1997+
self._add_legend_handle(p, l)
1998+
1999+
19422000
class BoxPlot(MPLPlot):
19432001
pass
19442002

@@ -1950,12 +2008,14 @@ class HistPlot(MPLPlot):
19502008
_common_kinds = ['line', 'bar', 'barh', 'kde', 'density', 'area']
19512009
# kinds supported by dataframe
19522010
_dataframe_kinds = ['scatter', 'hexbin']
1953-
_all_kinds = _common_kinds + _dataframe_kinds
2011+
# kinds supported only by series or dataframe single column
2012+
_series_kinds = ['pie']
2013+
_all_kinds = _common_kinds + _dataframe_kinds + _series_kinds
19542014

19552015
_plot_klass = {'line': LinePlot, 'bar': BarPlot, 'barh': BarPlot,
19562016
'kde': KdePlot,
19572017
'scatter': ScatterPlot, 'hexbin': HexBinPlot,
1958-
'area': AreaPlot}
2018+
'area': AreaPlot, 'pie': PiePlot}
19592019

19602020

19612021
def plot_frame(frame=None, x=None, y=None, subplots=False, sharex=True,
@@ -2054,7 +2114,7 @@ def plot_frame(frame=None, x=None, y=None, subplots=False, sharex=True,
20542114
"""
20552115

20562116
kind = _get_standard_kind(kind.lower().strip())
2057-
if kind in _dataframe_kinds or kind in _common_kinds:
2117+
if kind in _all_kinds:
20582118
klass = _plot_klass[kind]
20592119
else:
20602120
raise ValueError('Invalid chart type given %s' % kind)
@@ -2068,6 +2128,24 @@ def plot_frame(frame=None, x=None, y=None, subplots=False, sharex=True,
20682128
figsize=figsize, logx=logx, logy=logy,
20692129
sort_columns=sort_columns, secondary_y=secondary_y,
20702130
**kwds)
2131+
elif kind in _series_kinds:
2132+
if y is None and subplots is False:
2133+
msg = "{0} requires either y column or 'subplots=True'"
2134+
raise ValueError(msg.format(kind))
2135+
elif y is not None:
2136+
if com.is_integer(y) and not frame.columns.holds_integer():
2137+
y = frame.columns[y]
2138+
frame = frame[y] # converted to series actually
2139+
frame.index.name = y
2140+
2141+
plot_obj = klass(frame, kind=kind, subplots=subplots,
2142+
rot=rot,legend=legend, ax=ax, style=style,
2143+
fontsize=fontsize, use_index=use_index, sharex=sharex,
2144+
sharey=sharey, xticks=xticks, yticks=yticks,
2145+
xlim=xlim, ylim=ylim, title=title, grid=grid,
2146+
figsize=figsize,
2147+
sort_columns=sort_columns,
2148+
**kwds)
20712149
else:
20722150
if x is not None:
20732151
if com.is_integer(x) and not frame.columns.holds_integer():
@@ -2168,7 +2246,7 @@ def plot_series(series, label=None, kind='line', use_index=True, rot=None,
21682246
"""
21692247

21702248
kind = _get_standard_kind(kind.lower().strip())
2171-
if kind in _common_kinds:
2249+
if kind in _common_kinds or kind in _series_kinds:
21722250
klass = _plot_klass[kind]
21732251
else:
21742252
raise ValueError('Invalid chart type given %s' % kind)

0 commit comments

Comments
 (0)