From ab53432f3ce8ebdbc46b60f2ef7dcea2bf740662 Mon Sep 17 00:00:00 2001 From: Ruth Comer <10599679+rcomer@users.noreply.github.com> Date: Sun, 22 Dec 2024 14:20:36 +0000 Subject: [PATCH] WIP --- galleries/examples/misc/svg_filter_pie.py | 7 +- .../pie_and_polar_charts/bar_of_pie.py | 5 +- .../pie_and_donut_labels.py | 19 +- lib/matplotlib/__init__.py | 13 +- lib/matplotlib/axes/_axes.py | 247 +++++++++++++----- lib/matplotlib/axes/_axes.pyi | 3 + lib/matplotlib/pyplot.py | 10 +- lib/matplotlib/tests/test_axes.py | 120 ++++++--- 8 files changed, 305 insertions(+), 119 deletions(-) diff --git a/galleries/examples/misc/svg_filter_pie.py b/galleries/examples/misc/svg_filter_pie.py index b823cc9670c9..f76985ae8256 100644 --- a/galleries/examples/misc/svg_filter_pie.py +++ b/galleries/examples/misc/svg_filter_pie.py @@ -28,11 +28,12 @@ # 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%%') +pies = ax.pie(fracs, explode=explode, + wedge_labels=[labels, '{frac:.1%}'], wedge_label_distance=[1.1, 0.6]) -for w in pies[0]: +for w, label in zip(pies[0], labels): # set the id with the label. - w.set_gid(w.get_label()) + w.set_gid(label) # we don't want to draw the edge of the pie w.set_edgecolor("none") diff --git a/galleries/examples/pie_and_polar_charts/bar_of_pie.py b/galleries/examples/pie_and_polar_charts/bar_of_pie.py index 6f18b964cef7..0e5049bcf901 100644 --- a/galleries/examples/pie_and_polar_charts/bar_of_pie.py +++ b/galleries/examples/pie_and_polar_charts/bar_of_pie.py @@ -25,8 +25,9 @@ explode = [0.1, 0, 0] # rotate so that first wedge is split by the x-axis angle = -180 * overall_ratios[0] -wedges, *_ = ax1.pie(overall_ratios, autopct='%1.1f%%', startangle=angle, - labels=labels, explode=explode) +wedges, *_ = ax1.pie( + overall_ratios, startangle=angle, explode=explode, + wedge_labels=[labels, '{frac:.1%}'], wedge_label_distance=[1.1, 0.6]) # bar chart parameters age_ratios = [.33, .54, .07, .06] 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..7da11bb1a949 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 @@ -38,25 +38,16 @@ "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] +ax.pie(data, wedge_labels='{frac:.1%}\n({abs:d}g)', labels=ingredients, + labeldistance=None, textprops=dict(color="w", size=8, weight="bold")) -def func(pct, allvals): - absolute = int(np.round(pct/100.*np.sum(allvals))) - return f"{pct:.1f}%\n({absolute:d} g)" - - -wedges, texts, autotexts = ax.pie(data, autopct=lambda pct: func(pct, data), - textprops=dict(color="w")) - -ax.legend(wedges, ingredients, - title="Ingredients", +ax.legend(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,7 +88,7 @@ def func(pct, allvals): data = [225, 90, 50, 60, 100, 5] -wedges, texts = ax.pie(data, wedgeprops=dict(width=0.5), startangle=-40) +wedges, _ = 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="-"), diff --git a/lib/matplotlib/__init__.py b/lib/matplotlib/__init__.py index 5f964e0b34de..538d3b677df0 100644 --- a/lib/matplotlib/__init__.py +++ b/lib/matplotlib/__init__.py @@ -137,7 +137,7 @@ import atexit from collections import namedtuple -from collections.abc import MutableMapping +from collections.abc import MutableMapping, Sequence import contextlib import functools import importlib @@ -1377,14 +1377,17 @@ def _init_tests(): def _replacer(data, value): """ - Either returns ``data[value]`` or passes ``data`` back, converts either to - a sequence. + Either returns ``data[value]`` or passes ``value`` back, converts either to + a sequence. If ``value`` is a non-string sequence, processes each element + and returns a list. """ try: # if key isn't a string don't bother if isinstance(value, str): # try to use __getitem__ value = data[value] + elif isinstance(value, Sequence): + return [_replacer(data, x) for x in value] except Exception: # key does not exist, silently fall back to key pass @@ -1459,7 +1462,9 @@ def func(ax, *args, **kwargs): ... - if called with ``data=None``, forward the other arguments to ``func``; - otherwise, *data* must be a mapping; for any argument passed in as a string ``name``, replace the argument by ``data[name]`` (if this does not - throw an exception), then forward the arguments to ``func``. + throw an exception), then forward the arguments to ``func``. For any + argument passed as a non-string sequence, replace any string elements + by ``data[name]`` (if that does not throw an exception). In either case, any argument that is a `MappingView` is also converted to a list. diff --git a/lib/matplotlib/axes/_axes.py b/lib/matplotlib/axes/_axes.py index b6f7bbc3971b..f1b17cf6e8df 100644 --- a/lib/matplotlib/axes/_axes.py +++ b/lib/matplotlib/axes/_axes.py @@ -1,3 +1,4 @@ +import collections.abc import functools import itertools import logging @@ -3200,13 +3201,13 @@ def stem(self, *args, linefmt=None, markerfmt=None, basefmt=None, bottom=0, self.add_container(stem_container) return stem_container - @_api.make_keyword_only("3.9", "explode") - @_preprocess_data(replace_names=["x", "explode", "labels", "colors"]) - def pie(self, x, explode=None, labels=None, colors=None, - autopct=None, pctdistance=0.6, shadow=False, labeldistance=1.1, - startangle=0, radius=1, counterclock=True, - wedgeprops=None, textprops=None, center=(0, 0), - frame=False, rotatelabels=False, *, normalize=True, hatch=None): + @_preprocess_data(replace_names=["x", "explode", "labels", "colors", + "wedge_labels"]) + def pie(self, x, *, explode=None, labels=None, colors=None, wedge_labels=None, + wedge_label_distance=0.6, rotate_wedge_labels=False, autopct=None, + pctdistance=0.6, shadow=False, labeldistance=False, startangle=0, radius=1, + counterclock=True, wedgeprops=None, textprops=None, center=(0, 0), + frame=False, rotatelabels=False, normalize=True, hatch=None): """ Plot a pie chart. @@ -3239,6 +3240,8 @@ def pie(self, x, explode=None, labels=None, colors=None, .. versionadded:: 3.7 + wedge_labels : + autopct : None or str or callable, default: None If not *None*, *autopct* is a string or function used to label the wedges with their numeric value. The label will be placed inside @@ -3321,9 +3324,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") @@ -3332,18 +3333,19 @@ def pie(self, x, explode=None, labels=None, colors=None, sx = x.sum() + def check_length(name, values): + if len(values) != len(x): + raise ValueError(f"'{name}' must be of length 'x', not {len(values)}") + if normalize: - x = x / sx + fracs = x / sx elif sx > 1: raise ValueError('Cannot plot an unnormalized pie with sum(x) > 1') - if labels is None: - labels = [''] * len(x) + else: + fracs = x if explode is None: explode = [0] * len(x) - if len(x) != len(labels): - raise ValueError(f"'labels' must be of length 'x', not {len(labels)}") - if len(x) != len(explode): - raise ValueError(f"'explode' must be of length 'x', not {len(explode)}") + check_length("explode", explode) if colors is None: get_next_color = self._get_patches_for_fill.get_next_color else: @@ -3366,18 +3368,147 @@ def get_next_color(): if textprops is None: textprops = {} - texts = [] slices = [] autotexts = [] - for frac, label, expl in zip(x, labels, explode): - x, y = center + # Define some functions for choosing label fontize and horizontal alignment + # based on distance and whether we are right of center (i.e. cartesian x > 0) + + def legacy(distance, is_right): + # Used to place `labels`. This function can be removed when the + # `labeldistance` deprecation expires. Always align so the labels + # do not overlap the pie + ha = 'left' if is_right else 'right' + return mpl.rcParams['xtick.labelsize'], ha + + def flexible(distance, is_right): + if distance >= 1: + # Align so the labels do not overlap the pie + ha = 'left' if is_right else 'right' + else: + ha = 'center' + + return None, ha + + def fixed(distance, is_right): + # Used to place the labels generated with autopct. Always centered + # for backwards compatibility + return None, 'center' + + # Build a (possibly empty) list of lists of wedge labels, with corresponding + # lists of distances, rotation choices and alignment functions + + def sanitize_formatted_string(s): + if mpl._val_or_rc(textprops.get("usetex"), "text.usetex"): + # escape % (i.e. \%) if it is not already escaped + return re.sub(r"([^\\])%", r"\1\\%", s) + + return s + + def fmt_str_to_list(wl): + return [sanitize_formatted_string(wl.format(abs=absval, frac=frac)) + for absval, frac in zip(x, fracs)] + + if wedge_labels is None: + processed_wedge_labels = [] + wedge_label_distance = [] + rotate_wedge_labels = [] + elif isinstance(wedge_labels, str): + # Format string. + processed_wedge_labels = [fmt_str_to_list(wedge_labels)] + elif not isinstance(wedge_labels, collections.abc.Sequence): + raise TypeError("wedge_labels must be a string or sequence") + else: + wl0 = wedge_labels[0] + if isinstance(wl0, str) and wl0.format(abs=1, frac=1) == wl0: + # Plain string. Assume we have a sequence of ready-made labels + check_length("wedge_labels", wedge_labels) + processed_wedge_labels = [wedge_labels] + else: + processed_wedge_labels = [] + for wl in wedge_labels: + if isinstance(wl, str): + # Format string + processed_wedge_labels.append(fmt_str_to_list(wl)) + else: + # Ready made list + check_length("wedge_labels[i]", wl) + processed_wedge_labels.append(wl) + + if isinstance(wedge_label_distance, Number): + wedge_label_distance = [wedge_label_distance] + else: + # Copy so we won't append to user input + wedge_label_distance = wedge_label_distance[:] + + n_label_sets = len(processed_wedge_labels) + if n_label_sets != (nd := len(wedge_label_distance)): + raise ValueError(f"Found {n_label_sets} sets of wedge labels but " + f"{nd} wedge label distances.") + + if isinstance(rotate_wedge_labels, bool): + rotate_wedge_labels = [rotate_wedge_labels] + else: + # Copy so we won't append to user input + rotate_wedge_labels = rotate_wedge_labels[:] + + if len(rotate_wedge_labels) == 1: + rotate_wedge_labels = rotate_wedge_labels * n_label_sets + elif n_label_sets != (nr := len(rotate_wedge_labels)): + raise ValueError(f"Found {n_label_sets} sets of wedge labels but " + f"{nr} wedge label rotation choices.") + + prop_funcs = [flexible] * n_label_sets + + if labels is None: + labels = [None] * len(x) + else: + check_length("labels", labels) + + if not labeldistance and labeldistance is False: + msg = ("In future labeldistance will default to None. To preserve " + "existing behavior, pass labeldistance=1.1. Consider using " + "wedge_labels instead of labels.") + _api.warn_deprecated("3.11", message=msg) + labeldistance = 1.1 + + if labeldistance is not None: + processed_wedge_labels.append(labels) + wedge_label_distance.append(labeldistance) + prop_funcs.append(legacy) + rotate_wedge_labels.append(rotatelabels) + + wedgetexts = [[]] * len(processed_wedge_labels) + + if autopct is not None: + if isinstance(autopct, str): + processed_pct = [sanitize_formatted_string(autopct % (100. * frac)) + for frac in fracs] + elif callable(autopct): + processed_pct = [sanitize_formatted_string(autopct(100. * frac)) + for frac in fracs] + else: + raise TypeError('autopct must be callable or a format string') + + processed_wedge_labels.append(processed_pct) + wedge_label_distance.append(pctdistance) + prop_funcs.append(fixed) + rotate_wedge_labels.append(False) + + # Transpose so we can loop over wedges + processed_wedge_labels = np.transpose(processed_wedge_labels) + if not processed_wedge_labels.size: + processed_wedge_labels = processed_wedge_labels.reshape(len(x), 0) + + for frac, label, expl, wls in zip(fracs, labels, explode, + processed_wedge_labels): + 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), @@ -3395,44 +3526,31 @@ 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) - if isinstance(autopct, str): - s = autopct % (100. * frac) - elif callable(autopct): - s = autopct(100. * frac) - else: - raise TypeError( - 'autopct must be callable or a format string') - if 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) + if wls.size > 0: + # Add wedge labels + for i, (wl, ld, pf, rot) in enumerate( + zip(wls, wedge_label_distance, prop_funcs, + rotate_wedge_labels)): + xt = x_pos + ld * radius * math.cos(thetam) + yt = y_pos + ld * radius * math.sin(thetam) + fontsize, label_alignment_h = pf(ld, xt > 0) + label_alignment_v = 'center' + label_rotation = 'horizontal' + if rot: + 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, wl, + clip_on=False, + horizontalalignment=label_alignment_h, + verticalalignment=label_alignment_v, + rotation=label_rotation, + size=fontsize) + t.set(**textprops) + if i == len(wedgetexts): + # autopct texts are returned separately + autotexts.append(t) + else: + wedgetexts[i].append(t) theta1 = theta2 @@ -3443,10 +3561,13 @@ def get_next_color(): xlim=(-1.25 + center[0], 1.25 + center[0]), ylim=(-1.25 + center[1], 1.25 + center[1])) + if len(wedgetexts) == 1: + wedgetexts = wedgetexts[0] + if autopct is None: - return slices, texts + return slices, wedgetexts else: - return slices, texts, autotexts + return slices, wedgetexts, autotexts @staticmethod def _errorevery_to_mask(x, errorevery): diff --git a/lib/matplotlib/axes/_axes.pyi b/lib/matplotlib/axes/_axes.pyi index 1877cc192b15..25dcb9a8f093 100644 --- a/lib/matplotlib/axes/_axes.pyi +++ b/lib/matplotlib/axes/_axes.pyi @@ -299,6 +299,9 @@ class Axes(_AxesBase): explode: ArrayLike | None = ..., labels: Sequence[str] | None = ..., colors: ColorType | Sequence[ColorType] | None = ..., + wedge_labels: str | Sequence | None = ..., + wedge_label_distance: float | Sequence = ..., + rotate_wedge_labels: bool | Sequence = ..., autopct: str | Callable[[float], str] | None = ..., pctdistance: float = ..., shadow: bool = ..., diff --git a/lib/matplotlib/pyplot.py b/lib/matplotlib/pyplot.py index 1e8cf869e6b4..627d67f380ee 100644 --- a/lib/matplotlib/pyplot.py +++ b/lib/matplotlib/pyplot.py @@ -3775,13 +3775,17 @@ def phase_spectrum( @_copy_docstring_and_deprecators(Axes.pie) def pie( x: ArrayLike, + *, explode: ArrayLike | None = None, labels: Sequence[str] | None = None, colors: ColorType | Sequence[ColorType] | None = None, + wedge_labels: str | Sequence | None = None, + wedge_label_distance: float | Sequence = 0.6, + rotate_wedge_labels: bool | Sequence = False, autopct: str | Callable[[float], str] | None = None, pctdistance: float = 0.6, shadow: bool = False, - labeldistance: float | None = 1.1, + labeldistance: float | None = False, startangle: float = 0, radius: float = 1, counterclock: bool = True, @@ -3790,7 +3794,6 @@ def pie( center: tuple[float, float] = (0, 0), frame: bool = False, rotatelabels: bool = False, - *, normalize: bool = True, hatch: str | Sequence[str] | None = None, data=None, @@ -3800,6 +3803,9 @@ def pie( explode=explode, labels=labels, colors=colors, + wedge_labels=wedge_labels, + wedge_label_distance=wedge_label_distance, + rotate_wedge_labels=rotate_wedge_labels, autopct=autopct, pctdistance=pctdistance, shadow=shadow, diff --git a/lib/matplotlib/tests/test_axes.py b/lib/matplotlib/tests/test_axes.py index 38857e846c55..5c8886dd960a 100644 --- a/lib/matplotlib/tests/test_axes.py +++ b/lib/matplotlib/tests/test_axes.py @@ -5994,8 +5994,22 @@ def test_pie_default(): colors = ['yellowgreen', 'gold', 'lightskyblue', 'lightcoral'] explode = (0, 0.1, 0, 0) # only "explode" the 2nd slice (i.e. 'Hogs') fig1, ax1 = plt.subplots(figsize=(8, 6)) - ax1.pie(sizes, explode=explode, labels=labels, colors=colors, - autopct='%1.1f%%', shadow=True, startangle=90) + ax1.pie(sizes, explode=explode, wedge_labels=[labels, '{frac:.1%}'], + wedge_label_distance=[1.1, 0.6], colors=colors, shadow=True, startangle=90) + + +@image_comparison(['pie_default.png'], tol=0.01) +def test_pie_default_legacy(): + # Same as above, but uses old labels and autopct. + # The slices will be ordered and plotted counter-clockwise. + labels = 'Frogs', 'Hogs', 'Dogs', 'Logs' + sizes = [15, 30, 45, 10] + colors = ['yellowgreen', 'gold', 'lightskyblue', 'lightcoral'] + explode = (0, 0.1, 0, 0) # only "explode" the 2nd slice (i.e. 'Hogs') + fig1, ax1 = plt.subplots(figsize=(8, 6)) + with pytest.warns(mpl.MatplotlibDeprecationWarning): + ax1.pie(sizes, explode=explode, labels=labels, colors=colors, + autopct='%1.1f%%', shadow=True, startangle=90) @image_comparison(['pie_linewidth_0', 'pie_linewidth_0', 'pie_linewidth_0'], @@ -6007,27 +6021,29 @@ def test_pie_linewidth_0(): colors = ['yellowgreen', 'gold', 'lightskyblue', 'lightcoral'] explode = (0, 0.1, 0, 0) # only "explode" the 2nd slice (i.e. 'Hogs') - plt.pie(sizes, explode=explode, labels=labels, colors=colors, - autopct='%1.1f%%', shadow=True, startangle=90, + plt.pie(sizes, explode=explode, wedge_labels=[labels, '{frac:.1%}'], + wedge_label_distance=[1.1, 0.6], colors=colors, shadow=True, startangle=90, wedgeprops={'linewidth': 0}) - # Set aspect ratio to be equal so that pie is drawn as a circle. plt.axis('equal') - # Reuse testcase from above for a labeled data test + # Reuse testcase from above for a labeled data test. Include legend labels + # to smoke test that they are correctly unpacked. data = {"l": labels, "s": sizes, "c": colors, "ex": explode} fig = plt.figure() ax = fig.gca() - ax.pie("s", explode="ex", labels="l", colors="c", - autopct='%1.1f%%', shadow=True, startangle=90, - wedgeprops={'linewidth': 0}, data=data) + ax.pie("s", explode="ex", wedge_labels=["l", "{frac:.1%}"], colors="c", + wedge_label_distance=[1.1, 0.6], shadow=True, startangle=90, + labels="l", labeldistance=None, wedgeprops={'linewidth': 0}, + data=data) ax.axis('equal') # And again to test the pyplot functions which should also be able to be # called with a data kwarg plt.figure() - plt.pie("s", explode="ex", labels="l", colors="c", - autopct='%1.1f%%', shadow=True, startangle=90, - wedgeprops={'linewidth': 0}, data=data) + with pytest.warns(mpl.MatplotlibDeprecationWarning): + plt.pie("s", explode="ex", labels="l", colors="c", + autopct='%1.1f%%', shadow=True, startangle=90, + wedgeprops={'linewidth': 0}, data=data) plt.axis('equal') @@ -6039,8 +6055,8 @@ def test_pie_center_radius(): colors = ['yellowgreen', 'gold', 'lightskyblue', 'lightcoral'] explode = (0, 0.1, 0, 0) # only "explode" the 2nd slice (i.e. 'Hogs') - plt.pie(sizes, explode=explode, labels=labels, colors=colors, - autopct='%1.1f%%', shadow=True, startangle=90, + plt.pie(sizes, explode=explode, wedge_labels=[labels, '{frac:.1%}'], + wedge_label_distance=[1.1, 0.6], colors=colors, shadow=True, startangle=90, wedgeprops={'linewidth': 0}, center=(1, 2), radius=1.5) plt.annotate("Center point", xy=(1, 2), xytext=(1, 1.3), @@ -6059,8 +6075,8 @@ def test_pie_linewidth_2(): colors = ['yellowgreen', 'gold', 'lightskyblue', 'lightcoral'] explode = (0, 0.1, 0, 0) # only "explode" the 2nd slice (i.e. 'Hogs') - plt.pie(sizes, explode=explode, labels=labels, colors=colors, - autopct='%1.1f%%', shadow=True, startangle=90, + plt.pie(sizes, explode=explode, wedge_labels=[labels, '{frac:.1%}'], + wedge_label_distance=[1.1, 0.6], colors=colors, shadow=True, startangle=90, wedgeprops={'linewidth': 2}) # Set aspect ratio to be equal so that pie is drawn as a circle. plt.axis('equal') @@ -6074,9 +6090,9 @@ def test_pie_ccw_true(): colors = ['yellowgreen', 'gold', 'lightskyblue', 'lightcoral'] explode = (0, 0.1, 0, 0) # only "explode" the 2nd slice (i.e. 'Hogs') - plt.pie(sizes, explode=explode, labels=labels, colors=colors, - autopct='%1.1f%%', shadow=True, startangle=90, - counterclock=True) + plt.pie(sizes, explode=explode, wedge_labels=[labels, '{frac:.1%}'], + wedge_label_distance=[1.1, 0.6], colors=colors, shadow=True, startangle=90, + counterclock=True) # Set aspect ratio to be equal so that pie is drawn as a circle. plt.axis('equal') @@ -6090,18 +6106,18 @@ def test_pie_frame_grid(): # only "explode" the 2nd slice (i.e. 'Hogs') explode = (0, 0.1, 0, 0) - plt.pie(sizes, explode=explode, labels=labels, colors=colors, - autopct='%1.1f%%', shadow=True, startangle=90, + plt.pie(sizes, explode=explode, wedge_labels=[labels, '{frac:.1%}'], + wedge_label_distance=[1.1, 0.6], colors=colors, shadow=True, startangle=90, wedgeprops={'linewidth': 0}, frame=True, center=(2, 2)) - plt.pie(sizes[::-1], explode=explode, labels=labels, colors=colors, - autopct='%1.1f%%', shadow=True, startangle=90, + plt.pie(sizes[::-1], explode=explode, wedge_labels=[labels, '{frac:.1%}'], + wedge_label_distance=[1.1, 0.6], colors=colors, shadow=True, startangle=90, wedgeprops={'linewidth': 0}, frame=True, center=(5, 2)) - plt.pie(sizes, explode=explode[::-1], labels=labels, colors=colors, - autopct='%1.1f%%', shadow=True, startangle=90, + plt.pie(sizes, explode=explode[::-1], wedge_labels=[labels, '{frac:.1%}'], + wedge_label_distance=[1.1, 0.6], colors=colors, shadow=True, startangle=90, wedgeprops={'linewidth': 0}, frame=True, center=(3, 5)) # Set aspect ratio to be equal so that pie is drawn as a circle. @@ -6116,9 +6132,26 @@ def test_pie_rotatelabels_true(): colors = ['yellowgreen', 'gold', 'lightskyblue', 'lightcoral'] explode = (0, 0.1, 0, 0) # only "explode" the 2nd slice (i.e. 'Hogs') - plt.pie(sizes, explode=explode, labels=labels, colors=colors, - autopct='%1.1f%%', shadow=True, startangle=90, - rotatelabels=True) + plt.pie(sizes, explode=explode, wedge_labels=[labels, '{frac:.1%}'], + wedge_label_distance=[1.1, 0.6], colors=colors, shadow=True, startangle=90, + rotate_wedge_labels=[True, False]) + # Set aspect ratio to be equal so that pie is drawn as a circle. + plt.axis('equal') + + +@image_comparison(['pie_rotatelabels_true.png'], style='mpl20', tol=0.009) +def test_pie_rotatelabels_true_legacy(): + # As above but using legacy labels and rotatelabels parameters + # The slices will be ordered and plotted counter-clockwise. + labels = 'Hogwarts', 'Frogs', 'Dogs', 'Logs' + sizes = [15, 30, 45, 10] + colors = ['yellowgreen', 'gold', 'lightskyblue', 'lightcoral'] + explode = (0, 0.1, 0, 0) # only "explode" the 2nd slice (i.e. 'Hogs') + + with pytest.warns(mpl.MatplotlibDeprecationWarning): + plt.pie(sizes, explode=explode, labels=labels, colors=colors, + wedge_labels='{frac:.1%}', shadow=True, startangle=90, + rotatelabels=True) # Set aspect ratio to be equal so that pie is drawn as a circle. plt.axis('equal') @@ -6130,7 +6163,7 @@ def test_pie_nolabel_but_legend(): colors = ['yellowgreen', 'gold', 'lightskyblue', 'lightcoral'] explode = (0, 0.1, 0, 0) # only "explode" the 2nd slice (i.e. 'Hogs') plt.pie(sizes, explode=explode, labels=labels, colors=colors, - autopct='%1.1f%%', shadow=True, startangle=90, labeldistance=None, + wedge_labels='{frac:.1%}', shadow=True, startangle=90, labeldistance=None, rotatelabels=True) plt.axis('equal') plt.ylim(-1.2, 1.2) @@ -6171,8 +6204,33 @@ def test_pie_textprops(): rotation_mode="anchor", size=12, color="red") - _, texts, autopct = plt.gca().pie(data, labels=labels, autopct='%.2f', - textprops=textprops) + fig, ax = plt.subplots() + + _, texts = ax.pie(data, wedge_labels=[labels, '{frac:.1%}'], + wedge_label_distance=[1.1, 0.6], textprops=textprops) + for labels in texts: + for tx in labels: + assert tx.get_ha() == textprops["horizontalalignment"] + assert tx.get_va() == textprops["verticalalignment"] + assert tx.get_rotation() == textprops["rotation"] + assert tx.get_rotation_mode() == textprops["rotation_mode"] + assert tx.get_size() == textprops["size"] + assert tx.get_color() == textprops["color"] + + +def test_pie_textprops_legacy(): + data = [23, 34, 45] + labels = ["Long name 1", "Long name 2", "Long name 3"] + + textprops = dict(horizontalalignment="center", + verticalalignment="top", + rotation=90, + rotation_mode="anchor", + size=12, color="red") + + with pytest.warns(mpl.MatplotlibDeprecationWarning): + _, texts, autopct = plt.gca().pie(data, labels=labels, autopct='%.2f', + textprops=textprops) for labels in [texts, autopct]: for tx in labels: assert tx.get_ha() == textprops["horizontalalignment"]