Skip to content

Commit ec2952a

Browse files
committed
WIP
1 parent 677d990 commit ec2952a

File tree

7 files changed

+292
-113
lines changed

7 files changed

+292
-113
lines changed

galleries/examples/misc/svg_filter_pie.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -28,11 +28,12 @@
2828

2929
# We want to draw the shadow for each pie, but we will not use "shadow"
3030
# option as it doesn't save the references to the shadow patches.
31-
pies = ax.pie(fracs, explode=explode, labels=labels, autopct='%1.1f%%')
31+
pies = ax.pie(fracs, explode=explode,
32+
wedge_labels=[labels, '{frac:.1%}'], wedge_label_distance=[1.1, 0.6])
3233

33-
for w in pies[0]:
34+
for w, label in zip(pies[0], labels):
3435
# set the id with the label.
35-
w.set_gid(w.get_label())
36+
w.set_gid(label)
3637

3738
# we don't want to draw the edge of the pie
3839
w.set_edgecolor("none")

galleries/examples/pie_and_polar_charts/bar_of_pie.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,8 +25,9 @@
2525
explode = [0.1, 0, 0]
2626
# rotate so that first wedge is split by the x-axis
2727
angle = -180 * overall_ratios[0]
28-
wedges, *_ = ax1.pie(overall_ratios, autopct='%1.1f%%', startangle=angle,
29-
labels=labels, explode=explode)
28+
wedges, *_ = ax1.pie(
29+
overall_ratios, startangle=angle, explode=explode,
30+
wedge_labels=[labels, '{frac:.1%}'], wedge_label_distance=[1.1, 0.6])
3031

3132
# bar chart parameters
3233
age_ratios = [.33, .54, .07, .06]

galleries/examples/pie_and_polar_charts/pie_and_donut_labels.py

Lines changed: 5 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -38,25 +38,16 @@
3838
"250 g butter",
3939
"300 g berries"]
4040

41-
data = [float(x.split()[0]) for x in recipe]
41+
data = [int(x.split()[0]) for x in recipe]
4242
ingredients = [x.split()[-1] for x in recipe]
4343

44+
ax.pie(data, wedge_labels='{frac:.1%}\n({abs:d}g)', labels=ingredients,
45+
labeldistance=None, textprops=dict(color="w", size=8, weight="bold"))
4446

45-
def func(pct, allvals):
46-
absolute = int(np.round(pct/100.*np.sum(allvals)))
47-
return f"{pct:.1f}%\n({absolute:d} g)"
48-
49-
50-
wedges, texts, autotexts = ax.pie(data, autopct=lambda pct: func(pct, data),
51-
textprops=dict(color="w"))
52-
53-
ax.legend(wedges, ingredients,
54-
title="Ingredients",
47+
ax.legend(title="Ingredients",
5548
loc="center left",
5649
bbox_to_anchor=(1, 0, 0.5, 1))
5750

58-
plt.setp(autotexts, size=8, weight="bold")
59-
6051
ax.set_title("Matplotlib bakery: A pie")
6152

6253
plt.show()
@@ -97,7 +88,7 @@ def func(pct, allvals):
9788

9889
data = [225, 90, 50, 60, 100, 5]
9990

100-
wedges, texts = ax.pie(data, wedgeprops=dict(width=0.5), startangle=-40)
91+
wedges, _ = ax.pie(data, wedgeprops=dict(width=0.5), startangle=-40)
10192

10293
bbox_props = dict(boxstyle="square,pad=0.3", fc="w", ec="k", lw=0.72)
10394
kw = dict(arrowprops=dict(arrowstyle="-"),

lib/matplotlib/axes/_axes.py

Lines changed: 182 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import collections.abc
12
import functools
23
import itertools
34
import logging
@@ -3200,13 +3201,12 @@ def stem(self, *args, linefmt=None, markerfmt=None, basefmt=None, bottom=0,
32003201
self.add_container(stem_container)
32013202
return stem_container
32023203

3203-
@_api.make_keyword_only("3.9", "explode")
32043204
@_preprocess_data(replace_names=["x", "explode", "labels", "colors"])
3205-
def pie(self, x, explode=None, labels=None, colors=None,
3206-
autopct=None, pctdistance=0.6, shadow=False, labeldistance=1.1,
3207-
startangle=0, radius=1, counterclock=True,
3208-
wedgeprops=None, textprops=None, center=(0, 0),
3209-
frame=False, rotatelabels=False, *, normalize=True, hatch=None):
3205+
def pie(self, x, *, explode=None, labels=None, colors=None, wedge_labels=None,
3206+
wedge_label_distance=0.6, rotate_wedge_labels=False, autopct=None,
3207+
pctdistance=0.6, shadow=False, labeldistance=False, startangle=0, radius=1,
3208+
counterclock=True, wedgeprops=None, textprops=None, center=(0, 0),
3209+
frame=False, rotatelabels=False, normalize=True, hatch=None):
32103210
"""
32113211
Plot a pie chart.
32123212
@@ -3239,6 +3239,8 @@ def pie(self, x, explode=None, labels=None, colors=None,
32393239
32403240
.. versionadded:: 3.7
32413241
3242+
wedge_labels :
3243+
32423244
autopct : None or str or callable, default: None
32433245
If not *None*, *autopct* is a string or function used to label the
32443246
wedges with their numeric value. The label will be placed inside
@@ -3321,9 +3323,7 @@ def pie(self, x, explode=None, labels=None, colors=None,
33213323
The Axes aspect ratio can be controlled with `.Axes.set_aspect`.
33223324
"""
33233325
self.set_aspect('equal')
3324-
# The use of float32 is "historical", but can't be changed without
3325-
# regenerating the test baselines.
3326-
x = np.asarray(x, np.float32)
3326+
x = np.asarray(x)
33273327
if x.ndim > 1:
33283328
raise ValueError("x must be 1D")
33293329

@@ -3332,18 +3332,19 @@ def pie(self, x, explode=None, labels=None, colors=None,
33323332

33333333
sx = x.sum()
33343334

3335+
def check_length(name, values):
3336+
if len(values) != len(x):
3337+
raise ValueError(f"'{name}' must be of length 'x', not {len(values)}")
3338+
33353339
if normalize:
3336-
x = x / sx
3340+
fracs = x / sx
33373341
elif sx > 1:
33383342
raise ValueError('Cannot plot an unnormalized pie with sum(x) > 1')
3339-
if labels is None:
3340-
labels = [''] * len(x)
3343+
else:
3344+
fracs = x
33413345
if explode is None:
33423346
explode = [0] * len(x)
3343-
if len(x) != len(labels):
3344-
raise ValueError(f"'labels' must be of length 'x', not {len(labels)}")
3345-
if len(x) != len(explode):
3346-
raise ValueError(f"'explode' must be of length 'x', not {len(explode)}")
3347+
check_length("explode", explode)
33473348
if colors is None:
33483349
get_next_color = self._get_patches_for_fill.get_next_color
33493350
else:
@@ -3366,18 +3367,147 @@ def get_next_color():
33663367
if textprops is None:
33673368
textprops = {}
33683369

3369-
texts = []
33703370
slices = []
33713371
autotexts = []
33723372

3373-
for frac, label, expl in zip(x, labels, explode):
3374-
x, y = center
3373+
# Define some functions for choosing label fontize and horizontal alignment
3374+
# based on distance and whether we are right of center (i.e. cartesian x > 0)
3375+
3376+
def legacy(distance, is_right):
3377+
# Used to place `labels`. This function can be removed when the
3378+
# `labeldistance` deprecation expires. Always align so the labels
3379+
# do not overlap the pie
3380+
ha = 'left' if is_right else 'right'
3381+
return mpl.rcParams['xtick.labelsize'], ha
3382+
3383+
def flexible(distance, is_right):
3384+
if distance >= 1:
3385+
# Align so the labels do not overlap the pie
3386+
ha = 'left' if is_right else 'right'
3387+
else:
3388+
ha = 'center'
3389+
3390+
return None, ha
3391+
3392+
def fixed(distance, is_right):
3393+
# Used to place the labels generated with autopct. Always centered
3394+
# for backwards compatibility
3395+
return None, 'center'
3396+
3397+
# Build a (possibly empty) list of lists of wedge labels, with corresponding
3398+
# lists of distances, rotation choices and alignment functions
3399+
3400+
def sanitize_formatted_string(s):
3401+
if mpl._val_or_rc(textprops.get("usetex"), "text.usetex"):
3402+
# escape % (i.e. \%) if it is not already escaped
3403+
return re.sub(r"([^\\])%", r"\1\\%", s)
3404+
3405+
return s
3406+
3407+
def fmt_str_to_list(wl):
3408+
return [sanitize_formatted_string(wl.format(abs=absval, frac=frac))
3409+
for absval, frac in zip(x, fracs)]
3410+
3411+
if wedge_labels is None:
3412+
processed_wedge_labels = []
3413+
wedge_label_distance = []
3414+
rotate_wedge_labels = []
3415+
elif isinstance(wedge_labels, str):
3416+
# Format string.
3417+
processed_wedge_labels = [fmt_str_to_list(wedge_labels)]
3418+
elif not isinstance(wedge_labels, collections.abc.Sequence):
3419+
raise TypeError("wedge_labels must be a string or sequence")
3420+
else:
3421+
wl0 = wedge_labels[0]
3422+
if isinstance(wl0, str) and wl0.format(abs=1, frac=1) == wl0:
3423+
# Plain string. Assume we have a sequence of ready-made labels
3424+
check_length("wedge_labels", wedge_labels)
3425+
processed_wedge_labels = [wedge_labels]
3426+
else:
3427+
processed_wedge_labels = []
3428+
for wl in wedge_labels:
3429+
if isinstance(wl, str):
3430+
# Format string
3431+
processed_wedge_labels.append(fmt_str_to_list(wl))
3432+
else:
3433+
# Ready made list
3434+
check_length("wedge_labels[i]", wl)
3435+
processed_wedge_labels.append(wl)
3436+
3437+
if isinstance(wedge_label_distance, Number):
3438+
wedge_label_distance = [wedge_label_distance]
3439+
else:
3440+
# Copy so we won't append to user input
3441+
wedge_label_distance = wedge_label_distance[:]
3442+
3443+
n_label_sets = len(processed_wedge_labels)
3444+
if n_label_sets != (nd := len(wedge_label_distance)):
3445+
raise ValueError(f"Found {n_label_sets} sets of wedge labels but "
3446+
f"{nd} wedge label distances.")
3447+
3448+
if isinstance(rotate_wedge_labels, bool):
3449+
rotate_wedge_labels = [rotate_wedge_labels]
3450+
else:
3451+
# Copy so we won't append to user input
3452+
rotate_wedge_labels = rotate_wedge_labels[:]
3453+
3454+
if len(rotate_wedge_labels) == 1:
3455+
rotate_wedge_labels = rotate_wedge_labels * n_label_sets
3456+
elif n_label_sets != (nr := len(rotate_wedge_labels)):
3457+
raise ValueError(f"Found {n_label_sets} sets of wedge labels but "
3458+
f"{nr} wedge label rotation choices.")
3459+
3460+
prop_funcs = [flexible] * n_label_sets
3461+
3462+
if labels is None:
3463+
labels = [None] * len(x)
3464+
else:
3465+
check_length("labels", labels)
3466+
3467+
if not labeldistance and labeldistance is False:
3468+
msg = ("In future labeldistance will default to None. To preserve "
3469+
"existing behavior, pass labeldistance=1.1. Consider using "
3470+
"wedge_labels instead of labels.")
3471+
_api.warn_deprecated("3.11", message=msg)
3472+
labeldistance = 1.1
3473+
3474+
if labeldistance is not None:
3475+
processed_wedge_labels.append(labels)
3476+
wedge_label_distance.append(labeldistance)
3477+
prop_funcs.append(legacy)
3478+
rotate_wedge_labels.append(rotatelabels)
3479+
3480+
wedgetexts = [[]] * len(processed_wedge_labels)
3481+
3482+
if autopct is not None:
3483+
if isinstance(autopct, str):
3484+
processed_pct = [sanitize_formatted_string(autopct % (100. * frac))
3485+
for frac in fracs]
3486+
elif callable(autopct):
3487+
processed_pct = [sanitize_formatted_string(autopct(100. * frac))
3488+
for frac in fracs]
3489+
else:
3490+
raise TypeError('autopct must be callable or a format string')
3491+
3492+
processed_wedge_labels.append(processed_pct)
3493+
wedge_label_distance.append(pctdistance)
3494+
prop_funcs.append(fixed)
3495+
rotate_wedge_labels.append(False)
3496+
3497+
# Transpose so we can loop over wedges
3498+
processed_wedge_labels = np.transpose(processed_wedge_labels)
3499+
if not processed_wedge_labels.size:
3500+
processed_wedge_labels = processed_wedge_labels.reshape(len(x), 0)
3501+
3502+
for frac, label, expl, wls in zip(fracs, labels, explode,
3503+
processed_wedge_labels):
3504+
x_pos, y_pos = center
33753505
theta2 = (theta1 + frac) if counterclock else (theta1 - frac)
33763506
thetam = 2 * np.pi * 0.5 * (theta1 + theta2)
3377-
x += expl * math.cos(thetam)
3378-
y += expl * math.sin(thetam)
3507+
x_pos += expl * math.cos(thetam)
3508+
y_pos += expl * math.sin(thetam)
33793509

3380-
w = mpatches.Wedge((x, y), radius, 360. * min(theta1, theta2),
3510+
w = mpatches.Wedge((x_pos, y_pos), radius, 360. * min(theta1, theta2),
33813511
360. * max(theta1, theta2),
33823512
facecolor=get_next_color(),
33833513
hatch=next(hatch_cycle),
@@ -3395,44 +3525,31 @@ def get_next_color():
33953525
shadow_dict.update(shadow)
33963526
self.add_patch(mpatches.Shadow(w, **shadow_dict))
33973527

3398-
if labeldistance is not None:
3399-
xt = x + labeldistance * radius * math.cos(thetam)
3400-
yt = y + labeldistance * radius * math.sin(thetam)
3401-
label_alignment_h = 'left' if xt > 0 else 'right'
3402-
label_alignment_v = 'center'
3403-
label_rotation = 'horizontal'
3404-
if rotatelabels:
3405-
label_alignment_v = 'bottom' if yt > 0 else 'top'
3406-
label_rotation = (np.rad2deg(thetam)
3407-
+ (0 if xt > 0 else 180))
3408-
t = self.text(xt, yt, label,
3409-
clip_on=False,
3410-
horizontalalignment=label_alignment_h,
3411-
verticalalignment=label_alignment_v,
3412-
rotation=label_rotation,
3413-
size=mpl.rcParams['xtick.labelsize'])
3414-
t.set(**textprops)
3415-
texts.append(t)
3416-
3417-
if autopct is not None:
3418-
xt = x + pctdistance * radius * math.cos(thetam)
3419-
yt = y + pctdistance * radius * math.sin(thetam)
3420-
if isinstance(autopct, str):
3421-
s = autopct % (100. * frac)
3422-
elif callable(autopct):
3423-
s = autopct(100. * frac)
3424-
else:
3425-
raise TypeError(
3426-
'autopct must be callable or a format string')
3427-
if mpl._val_or_rc(textprops.get("usetex"), "text.usetex"):
3428-
# escape % (i.e. \%) if it is not already escaped
3429-
s = re.sub(r"([^\\])%", r"\1\\%", s)
3430-
t = self.text(xt, yt, s,
3431-
clip_on=False,
3432-
horizontalalignment='center',
3433-
verticalalignment='center')
3434-
t.set(**textprops)
3435-
autotexts.append(t)
3528+
if wls.size > 0:
3529+
# Add wedge labels
3530+
for i, (wl, ld, pf, rot) in enumerate(
3531+
zip(wls, wedge_label_distance, prop_funcs,
3532+
rotate_wedge_labels)):
3533+
xt = x_pos + ld * radius * math.cos(thetam)
3534+
yt = y_pos + ld * radius * math.sin(thetam)
3535+
fontsize, label_alignment_h = pf(ld, xt > 0)
3536+
label_alignment_v = 'center'
3537+
label_rotation = 'horizontal'
3538+
if rot:
3539+
label_alignment_v = 'bottom' if yt > 0 else 'top'
3540+
label_rotation = (np.rad2deg(thetam) + (0 if xt > 0 else 180))
3541+
t = self.text(xt, yt, wl,
3542+
clip_on=False,
3543+
horizontalalignment=label_alignment_h,
3544+
verticalalignment=label_alignment_v,
3545+
rotation=label_rotation,
3546+
size=fontsize)
3547+
t.set(**textprops)
3548+
if i == len(wedgetexts):
3549+
# autopct texts are returned separately
3550+
autotexts.append(t)
3551+
else:
3552+
wedgetexts[i].append(t)
34363553

34373554
theta1 = theta2
34383555

@@ -3443,10 +3560,13 @@ def get_next_color():
34433560
xlim=(-1.25 + center[0], 1.25 + center[0]),
34443561
ylim=(-1.25 + center[1], 1.25 + center[1]))
34453562

3563+
if len(wedgetexts) == 1:
3564+
wedgetexts = wedgetexts[0]
3565+
34463566
if autopct is None:
3447-
return slices, texts
3567+
return slices, wedgetexts
34483568
else:
3449-
return slices, texts, autotexts
3569+
return slices, wedgetexts, autotexts
34503570

34513571
@staticmethod
34523572
def _errorevery_to_mask(x, errorevery):

lib/matplotlib/axes/_axes.pyi

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -299,6 +299,9 @@ class Axes(_AxesBase):
299299
explode: ArrayLike | None = ...,
300300
labels: Sequence[str] | None = ...,
301301
colors: ColorType | Sequence[ColorType] | None = ...,
302+
wedge_labels: str | Sequence | None = ...,
303+
wedge_label_distance: float | Sequence = ...,
304+
rotate_wedge_labels: bool | Sequence = ...,
302305
autopct: str | Callable[[float], str] | None = ...,
303306
pctdistance: float = ...,
304307
shadow: bool = ...,

0 commit comments

Comments
 (0)