diff --git a/doc/api/axes_api.rst b/doc/api/axes_api.rst index b742ce9b7a55..f5af8744a2bc 100644 --- a/doc/api/axes_api.rst +++ b/doc/api/axes_api.rst @@ -73,6 +73,7 @@ Basic Axes.eventplot Axes.pie + Axes.pie_label Axes.stackplot diff --git a/doc/api/pyplot_summary.rst b/doc/api/pyplot_summary.rst index c4a860fd2590..97d9c576cc86 100644 --- a/doc/api/pyplot_summary.rst +++ b/doc/api/pyplot_summary.rst @@ -64,6 +64,7 @@ Basic stem eventplot pie + pie_label stackplot broken_barh vlines diff --git a/doc/release/next_whats_new/pie_label.rst b/doc/release/next_whats_new/pie_label.rst new file mode 100644 index 000000000000..6dc9a3f619c2 --- /dev/null +++ b/doc/release/next_whats_new/pie_label.rst @@ -0,0 +1,28 @@ +Adding labels to pie chart wedges +--------------------------------- + +The new `~.Axes.pie_label` method adds a label to each wedge in a pie chart created with +`~.Axes.pie`. It can take + +* a list of strings, similar to the existing *labels* parameter of `~.Axes.pie` +* a format string similar to the existing *autopct* parameter of `~.Axes.pie` except + that it uses the `str.format` method and it can handle absolute values as well as + fractions/percentages + +For more examples, see :doc:`/gallery/pie_and_polar_charts/pie_label`. + +.. plot:: + :include-source: true + :alt: A pie chart with three labels on each wedge, showing a food type, number, and fraction associated with the wedge. + + import matplotlib.pyplot as plt + + data = [36, 24, 8, 12] + labels = ['spam', 'eggs', 'bacon', 'sausage'] + + fig, ax = plt.subplots() + pie = ax.pie(data) + + ax.pie_label(pie, labels, distance=1.1) + ax.pie_label(pie, '{frac:.1%}', distance=0.7) + ax.pie_label(pie, '{absval:d}', distance=0.4) diff --git a/galleries/examples/misc/svg_filter_pie.py b/galleries/examples/misc/svg_filter_pie.py index b19867be9a2f..f8ccc5bcb22b 100644 --- a/galleries/examples/misc/svg_filter_pie.py +++ b/galleries/examples/misc/svg_filter_pie.py @@ -28,16 +28,16 @@ # We want to draw the shadow for each pie, but we will not use "shadow" # option as it doesn't save the references to the shadow patches. -pies = ax.pie(fracs, explode=explode, labels=labels, autopct='%1.1f%%') +pie = ax.pie(fracs, explode=explode, labels=labels, autopct='%1.1f%%') -for w in pies[0]: +for w in pie.wedges: # set the id with the label. w.set_gid(w.get_label()) # we don't want to draw the edge of the pie w.set_edgecolor("none") -for w in pies[0]: +for w in pie.wedges: # create shadow patch s = Shadow(w, -0.01, -0.01) s.set_gid(w.get_gid() + "_shadow") diff --git a/galleries/examples/pie_and_polar_charts/pie_and_donut_labels.py b/galleries/examples/pie_and_polar_charts/pie_and_donut_labels.py index 13e3019bc7ba..78e884128d1e 100644 --- a/galleries/examples/pie_and_polar_charts/pie_and_donut_labels.py +++ b/galleries/examples/pie_and_polar_charts/pie_and_donut_labels.py @@ -6,7 +6,8 @@ Welcome to the Matplotlib bakery. We will create a pie and a donut chart through the `pie method ` and show how to label them with a `legend ` -as well as with `annotations `. +as well as with the `pie_label method ` and +`annotations `. """ # %% @@ -15,12 +16,14 @@ # Now it's time for the pie. Starting with a pie recipe, we create the data # and a list of labels from it. # -# We can provide a function to the ``autopct`` argument, which will expand -# automatic percentage labeling by showing absolute values; we calculate -# the latter back from relative data and the known sum of all values. +# We then create the pie and store the returned `~matplotlib.container.PieContainer` +# object for later. # -# We then create the pie and store the returned objects for later. The first -# returned element of the returned tuple is a list of the wedges. Those are +# We can provide the `~matplotlib.container.PieContainer` 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 # `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, @@ -31,32 +34,26 @@ import matplotlib.pyplot as plt import numpy as np -fig, ax = plt.subplots(figsize=(6, 3), subplot_kw=dict(aspect="equal")) +fig, ax = plt.subplots(figsize=(6, 3)) recipe = ["375 g flour", "75 g sugar", "250 g butter", "300 g berries"] -data = [float(x.split()[0]) for x in recipe] +data = [int(x.split()[0]) for x in recipe] ingredients = [x.split()[-1] for x in recipe] +pie = ax.pie(data) -def func(pct, allvals): - absolute = int(np.round(pct/100.*np.sum(allvals))) - return f"{pct:.1f}%\n({absolute:d} g)" - +ax.pie_label(pie, '{frac:.1%}\n({absval:d}g)', + textprops=dict(color="w", size=8, weight="bold")) -wedges, texts, autotexts = ax.pie(data, autopct=lambda pct: func(pct, data), - textprops=dict(color="w")) - -ax.legend(wedges, ingredients, +ax.legend(pie.wedges, ingredients, title="Ingredients", loc="center left", bbox_to_anchor=(1, 0, 0.5, 1)) -plt.setp(autotexts, size=8, weight="bold") - ax.set_title("Matplotlib bakery: A pie") plt.show() @@ -97,13 +94,13 @@ def func(pct, allvals): data = [225, 90, 50, 60, 100, 5] -wedges, texts = ax.pie(data, wedgeprops=dict(width=0.5), startangle=-40) +pie = ax.pie(data, wedgeprops=dict(width=0.5), startangle=-40) bbox_props = dict(boxstyle="square,pad=0.3", fc="w", ec="k", lw=0.72) kw = dict(arrowprops=dict(arrowstyle="-"), bbox=bbox_props, zorder=0, va="center") -for i, p in enumerate(wedges): +for i, p in enumerate(pie.wedges): ang = (p.theta2 - p.theta1)/2. + p.theta1 y = np.sin(np.deg2rad(ang)) x = np.cos(np.deg2rad(ang)) @@ -131,6 +128,7 @@ def func(pct, allvals): # in this example: # # - `matplotlib.axes.Axes.pie` / `matplotlib.pyplot.pie` +# - `matplotlib.axes.Axes.pie_label` / `matplotlib.pyplot.pie_label` # - `matplotlib.axes.Axes.legend` / `matplotlib.pyplot.legend` # # .. tags:: diff --git a/galleries/examples/pie_and_polar_charts/pie_label.py b/galleries/examples/pie_and_polar_charts/pie_label.py new file mode 100644 index 000000000000..d7f690bd6f85 --- /dev/null +++ b/galleries/examples/pie_and_polar_charts/pie_label.py @@ -0,0 +1,100 @@ +""" +=================== +Labeling pie charts +=================== + +This example illustrates some features of the `~matplotlib.axes.Axes.pie_label` +method, which adds labels to an existing pie chart created with +`~matplotlib.axes.Axes.pie`. +""" + +# %% +# The simplest option is to provide a list of strings to label each slice of the pie. + +import matplotlib.pyplot as plt + +data = [36, 24, 8, 12] +labels = ['spam', 'eggs', 'bacon', 'sausage'] + +fig, ax = plt.subplots() +pie = ax.pie(data) +ax.pie_label(pie, labels) + +# %% +# +# If you want the labels outside the pie, set a *distance* greater than 1. +# This is the distance from the center of the pie as a fraction of its radius. + +fig, ax = plt.subplots() +pie = ax.pie(data) +ax.pie_label(pie, labels, distance=1.1) + +# %% +# +# You can also rotate the labels so they are oriented away from the pie center. + +fig, ax = plt.subplots() +pie = ax.pie(data) +ax.pie_label(pie, labels, rotate=True) + +# %% +# +# Instead of explicit labels, pass a format string to label slices with their values... + +fig, ax = plt.subplots() +pie = ax.pie(data) +ax.pie_label(pie, '{absval:.1f}') + +# %% +# +# ...or with their percentages... + +fig, ax = plt.subplots() +pie = ax.pie(data) +ax.pie_label(pie, '{frac:.1%}') + +# %% +# +# ...or both. + +fig, ax = plt.subplots() +pie = ax.pie(data) +ax.pie_label(pie, '{absval:d}\n{frac:.1%}') + +# %% +# +# Font styling can be configured by passing a dictionary to the *textprops* parameter. + +fig, ax = plt.subplots() +pie = ax.pie(data) +ax.pie_label(pie, labels, textprops={'fontsize': 'large', 'color': 'white'}) + +# %% +# +# `~matplotlib.axes.Axes.pie_label` can be called repeatedly to add multiple sets +# of labels. + +# sphinx_gallery_thumbnail_number = -1 + +fig, ax = plt.subplots() +pie = ax.pie(data) + +ax.pie_label(pie, labels, distance=1.1) +ax.pie_label(pie, '{frac:.1%}', distance=0.7) +ax.pie_label(pie, '{absval:d}', distance=0.4) + +plt.show() + +# %% +# .. admonition:: References +# +# The use of the following functions, methods, classes and modules is shown +# in this example: +# +# - `matplotlib.axes.Axes.pie` / `matplotlib.pyplot.pie` +# - `matplotlib.axes.Axes.pie_label` / `matplotlib.pyplot.pie_label` +# +# .. tags:: +# +# plot-type: pie +# level: beginner diff --git a/lib/matplotlib/axes/_axes.py b/lib/matplotlib/axes/_axes.py index 8548d16d43de..ae12579ab172 100644 --- a/lib/matplotlib/axes/_axes.py +++ b/lib/matplotlib/axes/_axes.py @@ -36,7 +36,9 @@ from matplotlib.axes._base import ( _AxesBase, _TransformedBoundsLocator, _process_plot_format) from matplotlib.axes._secondary_axes import SecondaryAxis -from matplotlib.container import BarContainer, ErrorbarContainer, StemContainer +from matplotlib.container import ( + BarContainer, ErrorbarContainer, PieContainer, StemContainer) +from matplotlib.text import Text from matplotlib.transforms import _ScaledRotation _log = logging.getLogger(__name__) @@ -3594,7 +3596,7 @@ def pie(self, x, explode=None, labels=None, colors=None, keywords, properties passed to *wedgeprops* take precedence. textprops : dict, default: None - Dict of arguments to pass to the text objects. + Dict of arguments to pass to the `.Text` objects. center : (float, float), default: (0, 0) The coordinates of the center of the chart. @@ -3615,15 +3617,11 @@ def pie(self, x, explode=None, labels=None, colors=None, Returns ------- - patches : list - A sequence of `matplotlib.patches.Wedge` instances + `.PieContainer` + Container with all the wedge patches and any associated text objects. - texts : list - A list of the label `.Text` instances. - - autotexts : list - A list of `.Text` instances for the numeric labels. This will only - be returned if the parameter *autopct* is not *None*. + .. versionchanged:: 3.11 + Previously the wedges and texts were returned in a tuple. Notes ----- @@ -3633,9 +3631,7 @@ def pie(self, x, explode=None, labels=None, colors=None, The Axes aspect ratio can be controlled with `.Axes.set_aspect`. """ self.set_aspect('equal') - # The use of float32 is "historical", but can't be changed without - # regenerating the test baselines. - x = np.asarray(x, np.float32) + x = np.asarray(x) if x.ndim > 1: raise ValueError("x must be 1D") @@ -3651,9 +3647,11 @@ def pie(self, x, explode=None, labels=None, colors=None, raise ValueError('All wedge sizes are zero') if normalize: - x = x / sx + fracs = x / sx elif sx > 1: raise ValueError('Cannot plot an unnormalized pie with sum(x) > 1') + else: + fracs = x if labels is None: labels = [''] * len(x) if explode is None: @@ -3681,21 +3679,17 @@ def get_next_color(): if wedgeprops is None: wedgeprops = {} - if textprops is None: - textprops = {} - texts = [] slices = [] - autotexts = [] - for frac, label, expl in zip(x, labels, explode): - x, y = center + for frac, label, expl in zip(fracs, labels, explode): + x_pos, y_pos = center theta2 = (theta1 + frac) if counterclock else (theta1 - frac) thetam = 2 * np.pi * 0.5 * (theta1 + theta2) - x += expl * math.cos(thetam) - y += expl * math.sin(thetam) + x_pos += expl * math.cos(thetam) + y_pos += expl * math.sin(thetam) - w = mpatches.Wedge((x, y), radius, 360. * min(theta1, theta2), + w = mpatches.Wedge((x_pos, y_pos), radius, 360. * min(theta1, theta2), 360. * max(theta1, theta2), facecolor=get_next_color(), hatch=next(hatch_cycle), @@ -3713,28 +3707,28 @@ def get_next_color(): shadow_dict.update(shadow) self.add_patch(mpatches.Shadow(w, **shadow_dict)) - if labeldistance is not None: - xt = x + labeldistance * radius * math.cos(thetam) - yt = y + labeldistance * radius * math.sin(thetam) - label_alignment_h = 'left' if xt > 0 else 'right' - label_alignment_v = 'center' - label_rotation = 'horizontal' - if rotatelabels: - 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, - horizontalalignment=label_alignment_h, - verticalalignment=label_alignment_v, - rotation=label_rotation, - size=mpl.rcParams['xtick.labelsize']) - t.set(**textprops) - texts.append(t) - - if autopct is not None: - xt = x + pctdistance * radius * math.cos(thetam) - yt = y + pctdistance * radius * math.sin(thetam) + theta1 = theta2 + + pc = PieContainer(slices, x, normalize) + + if labeldistance is None: + # Insert an empty list of texts for backwards compatibility of the + # return value. + pc.add_texts([]) + else: + # 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, + alignment='outer', rotate=rotatelabels, + textprops=labels_textprops) + + if autopct is not None: + # Add automatic percentage labels to wedges + auto_labels = [] + for frac in fracs: if isinstance(autopct, str): s = autopct % (100. * frac) elif callable(autopct): @@ -3742,17 +3736,15 @@ def get_next_color(): else: raise TypeError( 'autopct must be callable or a format string') - if mpl._val_or_rc(textprops.get("usetex"), "text.usetex"): + if textprops is not None and mpl._val_or_rc(textprops.get("usetex"), + "text.usetex"): # escape % (i.e. \%) if it is not already escaped s = re.sub(r"([^\\])%", r"\1\\%", s) - t = self.text(xt, yt, s, - clip_on=False, - horizontalalignment='center', - verticalalignment='center') - t.set(**textprops) - autotexts.append(t) + auto_labels.append(s) - theta1 = theta2 + self.pie_label(pc, auto_labels, distance=pctdistance, + alignment='center', + textprops=textprops) if frame: self._request_autoscale_view() @@ -3761,10 +3753,107 @@ def get_next_color(): xlim=(-1.25 + center[0], 1.25 + center[0]), ylim=(-1.25 + center[1], 1.25 + center[1])) - if autopct is None: - return slices, texts - else: - return slices, texts, autotexts + return pc + + def pie_label(self, container, /, 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`. + + Parameters + ---------- + container : `.PieContainer` + Container 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 + string with ``absval`` and/or ``frac`` placeholders. For example, to label + each wedge with its value and the percentage in brackets:: + + wedge_labels="{absval:d} ({frac:.0%})" + + distance : float, default: 0.6 + The radial position of the labels, relative to the pie radius. Values > 1 + are outside the wedge and values < 1 are inside the wedge. + + textprops : dict, default: None + Dict of arguments to pass to the `.Text` objects. + + rotate : bool, default: False + Rotate each label to the angle of the corresponding slice if true. + + alignment : {'center', 'outer', 'auto'}, default: 'auto' + Controls the horizontal alignment of the text objects relative to their + nominal position. + + - 'center': The labels are centered on their points. + - 'outer': Labels are aligned away from the center of the pie, i.e., labels + on the left side of the pie are right-aligned and labels on the right + side are left-aligned. + - 'auto': Translates to 'outer' if *distance* > 1 (so that the labels do not + overlap the wedges) and 'center' if *distance* < 1. + + If *rotate* is True, the vertical alignment is also affected in an + analogous way. + + - 'center': The labels are centered on their points. + - 'outer': Labels are aligned away from the center of the pie, i.e., labels + on the top half of the pie are bottom-aligned and labels on the bottom + half are top-aligned. + + Returns + ------- + list + A list of the label `.Text` instances. + """ + _api.check_in_list(['center', 'outer', 'auto'], alignment=alignment) + if alignment == 'auto': + alignment = 'outer' if distance > 1 else 'center' + + if textprops is None: + textprops = {} + + 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)] + 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)): + raise ValueError( + f'The number of labels ({nl}) must match the number of wedges ({nw})') + + texts = [] + + for wedge, label in zip(container.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) + if alignment == 'outer': + label_alignment_h = 'left' if xt > 0 else 'right' + else: + label_alignment_h = 'center' + label_alignment_v = 'center' + label_rotation = 'horizontal' + if rotate: + 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, + horizontalalignment=label_alignment_h, + verticalalignment=label_alignment_v) + t.set(**textprops) + texts.append(t) + + container.add_texts(texts) + + return texts + @staticmethod def _errorevery_to_mask(x, errorevery): diff --git a/lib/matplotlib/axes/_axes.pyi b/lib/matplotlib/axes/_axes.pyi index 69d251aa21f7..09587ab753a3 100644 --- a/lib/matplotlib/axes/_axes.pyi +++ b/lib/matplotlib/axes/_axes.pyi @@ -13,7 +13,8 @@ from matplotlib.collections import ( ) from matplotlib.colorizer import Colorizer from matplotlib.colors import Colormap, Normalize -from matplotlib.container import BarContainer, ErrorbarContainer, StemContainer +from matplotlib.container import ( + BarContainer, PieContainer, ErrorbarContainer, StemContainer) from matplotlib.contour import ContourSet, QuadContourSet from matplotlib.image import AxesImage, PcolorImage from matplotlib.inset import InsetIndicator @@ -21,7 +22,7 @@ from matplotlib.legend import Legend 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, Wedge +from matplotlib.patches import Rectangle, FancyArrow, Polygon, StepPatch from matplotlib.quiver import Quiver, QuiverKey, Barbs from matplotlib.text import Annotation, Text from matplotlib.transforms import Transform @@ -324,9 +325,18 @@ class Axes(_AxesBase): normalize: bool = ..., hatch: str | Sequence[str] | None = ..., data=..., - ) -> tuple[list[Wedge], list[Text]] | tuple[ - list[Wedge], list[Text], list[Text] - ]: ... + ) -> PieContainer: ... + def pie_label( + self, + container: PieContainer, + /, + labels: str | Sequence[str], + *, + distance: float = ..., + textprops: dict | None = ..., + rotate: bool = ..., + alignment: str = ..., + ) -> list[Text]: ... def errorbar( self, x: float | ArrayLike, diff --git a/lib/matplotlib/container.py b/lib/matplotlib/container.py index fcf2e6016db9..96b14cfd26f7 100644 --- a/lib/matplotlib/container.py +++ b/lib/matplotlib/container.py @@ -148,6 +148,78 @@ 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. diff --git a/lib/matplotlib/container.pyi b/lib/matplotlib/container.pyi index ff11830c544c..772801b16d6d 100644 --- a/lib/matplotlib/container.pyi +++ b/lib/matplotlib/container.pyi @@ -1,11 +1,13 @@ from matplotlib.artist import Artist from matplotlib.lines import Line2D from matplotlib.collections import LineCollection -from matplotlib.patches import Rectangle +from matplotlib.patches import Rectangle, Wedge +from matplotlib.text import Text from collections.abc import Callable from typing import Any, Literal from numpy.typing import ArrayLike +from numpy import ndarray class Container(tuple): def __new__(cls, *args, **kwargs): ... @@ -51,6 +53,24 @@ class ErrorbarContainer(Container): **kwargs ) -> None: ... +class PieContainer(Container): + wedges: list[Wedge] + def __init__( + self, + wedges: list[Wedge], + values: ndarray, + normalize: bool, + ) -> None: ... + @property + def texts(self) -> list[list[Text]]: ... + @property + def values(self) -> ndarray: ... + @property + def fracs(self) -> ndarray: ... + def add_texts(self, + texts: list[Text], + ) -> None: ... + class StemContainer(Container): markerline: Line2D stemlines: LineCollection diff --git a/lib/matplotlib/pyplot.py b/lib/matplotlib/pyplot.py index cecd3dc24737..77c9a51352b2 100644 --- a/lib/matplotlib/pyplot.py +++ b/lib/matplotlib/pyplot.py @@ -126,13 +126,14 @@ from matplotlib.container import ( BarContainer, ErrorbarContainer, + PieContainer, StemContainer, ) from matplotlib.figure import SubFigure from matplotlib.legend import Legend from matplotlib.mlab import GaussianKDE from matplotlib.image import AxesImage, FigureImage - from matplotlib.patches import FancyArrow, StepPatch, Wedge + from matplotlib.patches import FancyArrow, StepPatch from matplotlib.quiver import Barbs, Quiver, QuiverKey from matplotlib.scale import ScaleBase from matplotlib.typing import ( @@ -3955,7 +3956,7 @@ def pie( normalize: bool = True, hatch: str | Sequence[str] | None = None, data=None, -) -> tuple[list[Wedge], list[Text]] | tuple[list[Wedge], list[Text], list[Text]]: +) -> PieContainer: return gca().pie( x, explode=explode, @@ -3979,6 +3980,28 @@ def pie( ) +# Autogenerated by boilerplate.py. Do not edit as changes will be lost. +@_copy_docstring_and_deprecators(Axes.pie_label) +def pie_label( + container: PieContainer, + /, + labels: str | Sequence[str], + *, + distance: float = 0.6, + textprops: dict | None = None, + rotate: bool = False, + alignment: str = "auto", +) -> list[Text]: + return gca().pie_label( + container, + labels, + distance=distance, + textprops=textprops, + rotate=rotate, + alignment=alignment, + ) + + # Autogenerated by boilerplate.py. Do not edit as changes will be lost. @_copy_docstring_and_deprecators(Axes.plot) def plot( diff --git a/lib/matplotlib/tests/test_axes.py b/lib/matplotlib/tests/test_axes.py index 7307951595cb..03942319e0b9 100644 --- a/lib/matplotlib/tests/test_axes.py +++ b/lib/matplotlib/tests/test_axes.py @@ -8,6 +8,7 @@ import io from itertools import product import platform +import re import sys from types import SimpleNamespace @@ -6608,6 +6609,57 @@ def test_pie_hatch_multi(fig_test, fig_ref): [w.set_hatch(hp) for w, hp in zip(wedges, hatch)] +def test_pie_label_formatter(): + fig, ax = plt.subplots() + pie = ax.pie([2, 3]) + + texts = ax.pie_label(pie, '{absval:03d}') + assert texts[0].get_text() == '002' + assert texts[1].get_text() == '003' + + texts = ax.pie_label(pie, '{frac:.1%}') + assert texts[0].get_text() == '40.0%' + assert texts[1].get_text() == '60.0%' + + +@pytest.mark.parametrize('distance', [0.6, 1.1]) +@pytest.mark.parametrize('rotate', [False, True]) +def test_pie_label_auto_align(distance, rotate): + fig, ax = plt.subplots() + pie = ax.pie([1, 1], startangle=45) + + texts = ax.pie_label( + pie, ['spam', 'eggs'], distance=distance, rotate=rotate, alignment='auto') + + if distance < 1: + for text in texts: + # labels within the pie should be centered + assert text.get_horizontalalignment() == 'center' + assert text.get_verticalalignment() == 'center' + + else: + # labels outside the pie should be aligned away from it + h_expected = ['right', 'left'] + v_expected = ['bottom', 'top'] + for text, h_align, v_align in zip(texts, h_expected, v_expected): + assert text.get_horizontalalignment() == h_align + if rotate: + assert text.get_verticalalignment() == v_align + else: + assert text.get_verticalalignment() == 'center' + + +def test_pie_label_fail(): + sizes = 15, 30, 45, 10 + labels = 'Frogs', 'Hogs' + fig, ax = plt.subplots() + pie = ax.pie(sizes) + + match = re.escape("The number of labels (2) must match the number of wedges (4)") + with pytest.raises(ValueError, match=match): + ax.pie_label(pie, labels) + + @image_comparison(['set_get_ticklabels.png'], tol=0 if platform.machine() == 'x86_64' else 0.025) def test_set_get_ticklabels(): diff --git a/lib/matplotlib/tests/test_container.py b/lib/matplotlib/tests/test_container.py index 6998101dd755..b7dfe1196685 100644 --- a/lib/matplotlib/tests/test_container.py +++ b/lib/matplotlib/tests/test_container.py @@ -53,3 +53,26 @@ def test_barcontainer_position_centers__bottoms__tops(): assert_array_equal(container.position_centers, pos) assert_array_equal(container.bottoms, bottoms) assert_array_equal(container.tops, bottoms + heights) + + +def test_piecontainer_remove(): + fig, ax = plt.subplots() + pie = ax.pie([2, 3], labels=['foo', 'bar'], autopct="%1.0f%%") + ax.pie_label(pie, ['baz', 'qux']) + assert len(ax.patches) == 2 + assert len(ax.texts) == 6 + + pie.remove() + assert not ax.patches + assert not ax.texts + + +def test_piecontainer_unpack_backcompat(): + fig, ax = plt.subplots() + wedges, texts, autotexts = ax.pie( + [2, 3], labels=['foo', 'bar'], autopct="%1.0f%%", labeldistance=None) + + assert len(wedges) == 2 + assert isinstance(texts, list) + assert not texts + assert len(autotexts) == 2 diff --git a/tools/boilerplate.py b/tools/boilerplate.py index a617d12c7072..0a1a26c7cb76 100644 --- a/tools/boilerplate.py +++ b/tools/boilerplate.py @@ -263,6 +263,7 @@ def boilerplate_gen(): 'pcolormesh', 'phase_spectrum', 'pie', + 'pie_label', 'plot', 'psd', 'quiver',