Skip to content
Open
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
3 changes: 0 additions & 3 deletions ci/mypy-stubtest-allowlist.txt
Original file line number Diff line number Diff line change
Expand Up @@ -54,8 +54,5 @@ matplotlib\.animation\.EventSourceProtocol
# https://github.com/python/mypy/issues/19877
matplotlib\.ft2font\.GlyphIndexType\.__init__

# getitem method only exists for 3.11 deprecation backcompatability
matplotlib.container.PieContainer.__getitem__

# 3.12 deprecation
matplotlib\.axes\._base\._AxesBase\.ArtistList
1 change: 1 addition & 0 deletions doc/api/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,7 @@ Alphabetical list of modules:
patches_api.rst
path_api.rst
patheffects_api.rst
pie_api.rst
pyplot_summary.rst
projections_api.rst
quiver_api.rst
Expand Down
8 changes: 8 additions & 0 deletions doc/api/next_api_changes/behavior/32359-RM.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
Axes.pie returns a Pie artist
-----------------------------

`~.Axes.pie` now returns a `.Pie` artist instead of a
``matplotlib.container.PieContainer``. The wedge patches, shadows and labels
are children of the returned `.Pie` and are no longer added to
``Axes.patches`` and ``Axes.texts``; use ``Pie.wedges`` and `.Pie.texts` to
access them.
6 changes: 6 additions & 0 deletions doc/api/next_api_changes/deprecations/32359-RM.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
matplotlib.container.PieContainer
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

``matplotlib.container.PieContainer`` is deprecated. `~.Axes.pie` now returns
a `.Pie` artist, which collects the wedge patches, shadows and labels as child
artists. Use `.Pie` with its ``wedges`` and `.Pie.texts` attributes instead.
8 changes: 8 additions & 0 deletions doc/api/pie_api.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
******************
``matplotlib.pie``
******************

.. automodule:: matplotlib.pie
:members:
:undoc-members:
:show-inheritance:
18 changes: 18 additions & 0 deletions doc/release/next_whats_new/pie_artist.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
Pie charts are now a single artist
----------------------------------

`.Axes.pie` now returns a `.Pie` artist instead of a ``PieContainer`` (which
is deprecated). The `.Pie` collects the wedge patches, the optional shadow
patches and all labels as child artists, so the whole chart can be treated
as one object::

pie = ax.pie([1, 2, 3], shadow=True)
pie.remove()

Because the wedges and labels are children of the `.Pie`, they are no longer
added to ``Axes.patches`` and ``Axes.texts`` directly. Access them through
``Pie.wedges`` and `.Pie.texts` instead.

Compound artists can provide their own legend entries by implementing
`~.Pie.get_legend_handles`; `.Axes.legend` uses this to show the individual
wedges of a pie.
Original file line number Diff line number Diff line change
Expand Up @@ -16,14 +16,15 @@
# Now it's time for the pie. Starting with a pie recipe, we create the data
# and a list of labels from it.
#
# We then create the pie and store the returned `~matplotlib.container.PieContainer`
# We then create the pie and store the returned `~matplotlib.pie.Pie`
# object for later.
#
# We can provide the `~matplotlib.container.PieContainer` and a format string to
# We can provide the `~matplotlib.pie.Pie` and a format string to
# the `~matplotlib.axes.Axes.pie_label` method to automatically label each
# ingredient's wedge with its weight in grams and percentages.
#
# The `~.PieContainer` has a list of patches as one of its attributes. Those are
# The `~matplotlib.pie.Pie` has a list of ``wedges`` as one of its attributes.
# Those are
# `matplotlib.patches.Wedge` patches, which can directly be used as the handles
# for a legend. We can use the legend's ``bbox_to_anchor`` argument to position
# the legend outside of the pie. Here we use the axes coordinates ``(1, 0, 0.5,
Expand Down
51 changes: 31 additions & 20 deletions lib/matplotlib/axes/_axes.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,8 @@
_AxesBase, _TransformedBoundsLocator, _process_plot_format)
from matplotlib.axes._secondary_axes import SecondaryAxis
from matplotlib.container import (
BarContainer, ErrorbarContainer, PieContainer, StemContainer)
BarContainer, ErrorbarContainer, StemContainer)
from matplotlib.pie import Pie
from matplotlib.text import Text
from matplotlib.transforms import _ScaledRotation
from matplotlib._api import UNSET as _UNSET
Expand Down Expand Up @@ -3673,12 +3674,19 @@ def pie(self, x, *, explode=None, labels=None, colors=None, wedge_labels=None,

Returns
-------
`.PieContainer`
Container with all the wedge patches and any associated text objects.
`.Pie`
Artist with all the wedge patches, shadow patches, and any
associated text objects.

.. versionchanged:: 3.11
Previously the wedges and texts were returned in a tuple.

.. versionchanged:: 3.12
The returned `.Pie` artist replaces the deprecated
``matplotlib.container.PieContainer``. The wedges, shadows and
labels are now children of the returned artist instead of being
added to the Axes directly.

Notes
-----
The pie chart will probably look best if the figure and Axes are
Expand Down Expand Up @@ -3762,6 +3770,7 @@ def get_next_color():
wedgeprops = {}

slices = []
shadows = []

for frac, label, expl in zip(fracs, labels, explode):
x_pos, y_pos = center
Expand All @@ -3778,36 +3787,38 @@ def get_next_color():
label=label)
w.set(**wedgeprops)
slices.append(w)
self.add_patch(w)

if shadow:
# Make sure to add a shadow after the call to add_patch so the
# figure and transform props will be set.
shadow_dict = {'ox': -0.02, 'oy': -0.02, 'label': '_nolegend_'}
if isinstance(shadow, dict):
shadow_dict.update(shadow)
self.add_patch(mpatches.Shadow(w, **shadow_dict))
shadows.append(mpatches.Shadow(w, **shadow_dict))

theta1 = theta2

pc = PieContainer(slices, x, normalize)
pie = Pie(slices, x, normalize, shadows)
self.add_artist(pie)
for w in slices:
self._update_patch_limits(w)

if wedge_labels is not None:
self.pie_label(pc, wedge_labels, distance=wedge_label_distance,
self.pie_label(pie, wedge_labels, distance=wedge_label_distance,
textprops=textprops)

elif labeldistance is None:
# Insert an empty list of texts for backwards compatibility of the
# return value.
pc.add_texts([])
pie.add_texts([])

if labeldistance is not None:
# Add labels to the wedges.
labels_textprops = {
'fontsize': mpl.rcParams['xtick.labelsize'],
**cbook.normalize_kwargs(textprops or {}, Text)
}
self.pie_label(pc, labels, distance=labeldistance,
self.pie_label(pie, labels, distance=labeldistance,
alignment='outer', rotate=rotatelabels,
textprops=labels_textprops)

Expand All @@ -3828,7 +3839,7 @@ def get_next_color():
s = re.sub(r"([^\\])%", r"\1\\%", s)
auto_labels.append(s)

self.pie_label(pc, auto_labels, distance=pctdistance,
self.pie_label(pie, auto_labels, distance=pctdistance,
alignment='center',
textprops=textprops)

Expand All @@ -3839,21 +3850,21 @@ def get_next_color():
xlim=(-1.25 + center[0], 1.25 + center[0]),
ylim=(-1.25 + center[1], 1.25 + center[1]))

return pc
return pie

def pie_label(self, container, /, labels, *, distance=0.6,
def pie_label(self, pie, /, labels, *, distance=0.6,
textprops=None, rotate=False, alignment='auto'):
"""
Label a pie chart.

.. versionadded:: 3.11

Adds labels to wedges in the given `.PieContainer`.
Adds labels to wedges in the given `.Pie`.

Parameters
----------
container : `.PieContainer`
Container with all the wedges, likely returned from `.pie`.
pie : `.Pie`
Pie artist with all the wedges, likely returned from `.pie`.

labels : str or list of str
A sequence of strings providing the labels for each wedge, or a format
Expand Down Expand Up @@ -3906,17 +3917,17 @@ def pie_label(self, container, /, labels, *, distance=0.6,
if isinstance(labels, str):
# Assume we have a format string
labels = [labels.format(absval=val, frac=frac) for val, frac in
zip(container.values, container.fracs)]
zip(pie.values, pie.fracs)]
if mpl._val_or_rc(textprops.get("usetex"), "text.usetex"):
# escape % (i.e. \%) if it is not already escaped
labels = [re.sub(r"([^\\])%", r"\1\\%", s) for s in labels]
elif (nw := len(container.wedges)) != (nl := len(labels)):
elif (nw := len(pie.wedges)) != (nl := len(labels)):
raise ValueError(
f'The number of labels ({nl}) must match the number of wedges ({nw})')

texts = []

for wedge, label in zip(container.wedges, labels):
for wedge, label in zip(pie.wedges, labels):
thetam = 2 * np.pi * 0.5 * (wedge.theta1 + wedge.theta2) / 360
xt = wedge.center[0] + distance * wedge.r * math.cos(thetam)
yt = wedge.center[1] + distance * wedge.r * math.sin(thetam)
Expand All @@ -3930,13 +3941,13 @@ def pie_label(self, container, /, labels, *, distance=0.6,
if alignment == 'outer':
label_alignment_v = 'bottom' if yt > 0 else 'top'
label_rotation = (np.rad2deg(thetam) + (0 if xt > 0 else 180))
t = self.text(xt, yt, label, clip_on=False, rotation=label_rotation,
t = mtext.Text(xt, yt, label, clip_on=False, rotation=label_rotation,
horizontalalignment=label_alignment_h,
verticalalignment=label_alignment_v)
t.set(**textprops)
texts.append(t)

container.add_texts(texts)
pie.add_texts(texts)

return texts

Expand Down
7 changes: 4 additions & 3 deletions lib/matplotlib/axes/_axes.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ from matplotlib.colors import (
Normalize,
)
from matplotlib.container import (
BarContainer, PieContainer, ErrorbarContainer, StemContainer)
BarContainer, ErrorbarContainer, StemContainer)
from matplotlib.contour import ContourSet, QuadContourSet
from matplotlib.image import AxesImage, PcolorImage
from matplotlib.inset import InsetIndicator
Expand All @@ -29,6 +29,7 @@ from matplotlib.legend_handler import HandlerBase
from matplotlib.lines import Line2D, AxLine
from matplotlib.mlab import GaussianKDE
from matplotlib.patches import Rectangle, FancyArrow, Polygon, StepPatch
from matplotlib.pie import Pie
from matplotlib.quiver import Quiver, QuiverKey, Barbs
from matplotlib.text import Annotation, Text
from matplotlib.transforms import Transform
Expand Down Expand Up @@ -334,10 +335,10 @@ class Axes(_AxesBase):
normalize: bool = ...,
hatch: str | Sequence[str] | None = ...,
data: DataParamType = ...,
) -> PieContainer: ...
) -> Pie: ...
def pie_label(
self,
container: PieContainer,
pie: Pie,
/,
labels: str | Sequence[str],
*,
Expand Down
83 changes: 10 additions & 73 deletions lib/matplotlib/container.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from matplotlib import cbook
from matplotlib import _api, cbook
from matplotlib.artist import Artist
from matplotlib.pie import Pie


class Container(tuple):
Expand Down Expand Up @@ -168,78 +169,6 @@ def __init__(self, lines, has_xerr=False, has_yerr=False, **kwargs):
super().__init__(lines, **kwargs)


class PieContainer:
"""
Container for the artists of pie charts (e.g. created by `.Axes.pie`).

.. versionadded:: 3.11

.. warning::
The class name ``PieContainer`` name is provisional and may change in future
to reflect development of its functionality.

You can access the wedge patches and further parameters by the attributes.

Attributes
----------
wedges : list of `~matplotlib.patches.Wedge`
The artists of the pie wedges.

values : `numpy.ndarray`
The data that the pie is based on.

fracs : `numpy.ndarray`
The fraction of the pie that each wedge represents.

texts : list of list of `~matplotlib.text.Text`
The artists of any labels on the pie wedges. Each inner list has one
text label per wedge.

"""
def __init__(self, wedges, values, normalize):
self.wedges = wedges
self._texts = []
self._values = values
self._normalize = normalize

@property
def texts(self):
# Only return non-empty sublists. An empty sublist may have been added
# for backwards compatibility of the Axes.pie return value (see __getitem__).
return [t_list for t_list in self._texts if t_list]

@property
def values(self):
result = self._values.copy()
result.flags.writeable = False
return result

@property
def fracs(self):
if self._normalize:
result = self._values / self._values.sum()
else:
result = self._values

result.flags.writeable = False
return result

def add_texts(self, texts):
"""Add a list of `~matplotlib.text.Text` objects to the container."""
self._texts.append(texts)

def remove(self):
"""Remove all wedges and texts from the axes"""
for artist_list in self.wedges, self._texts:
for artist in cbook.flatten(artist_list):
artist.remove()

def __getitem__(self, key):
# needed to support unpacking into a tuple for backward compatibility of the
# Axes.pie return value
return (self.wedges, *self._texts)[key]


class StemContainer(Container):
"""
Container for the artists created in a :meth:`.Axes.stem` plot.
Expand Down Expand Up @@ -273,3 +202,11 @@ def __init__(self, markerline_stemlines_baseline, **kwargs):
self.stemlines = stemlines
self.baseline = baseline
super().__init__(markerline_stemlines_baseline, **kwargs)


@_api.caching_module_getattr
class __getattr__:
@_api.deprecated("3.12", alternative="matplotlib.pie.Pie")
@property
def PieContainer(self):
return Pie
Loading
Loading