Skip to content

Commit 38363df

Browse files
committed
WIP
1 parent 677d990 commit 38363df

File tree

5 files changed

+238
-118
lines changed

5 files changed

+238
-118
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: 177 additions & 60 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
@@ -3202,10 +3203,10 @@ def stem(self, *args, linefmt=None, markerfmt=None, basefmt=None, bottom=0,
32023203

32033204
@_api.make_keyword_only("3.9", "explode")
32043205
@_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),
3206+
def pie(self, x, explode=None, labels=None, colors=None, wedge_labels=None,
3207+
wedge_label_distance=0.6, rotate_wedge_labels=False, autopct=None,
3208+
pctdistance=0.6, shadow=False, labeldistance=1.1, startangle=0, radius=1,
3209+
counterclock=True, wedgeprops=None, textprops=None, center=(0, 0),
32093210
frame=False, rotatelabels=False, *, normalize=True, hatch=None):
32103211
"""
32113212
Plot a pie chart.
@@ -3239,6 +3240,8 @@ def pie(self, x, explode=None, labels=None, colors=None,
32393240
32403241
.. versionadded:: 3.7
32413242
3243+
wedge_labels :
3244+
32423245
autopct : None or str or callable, default: None
32433246
If not *None*, *autopct* is a string or function used to label the
32443247
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,
33213324
The Axes aspect ratio can be controlled with `.Axes.set_aspect`.
33223325
"""
33233326
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)
3327+
x = np.asarray(x)
33273328
if x.ndim > 1:
33283329
raise ValueError("x must be 1D")
33293330

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

33333334
sx = x.sum()
33343335

3336+
def check_length(name, values):
3337+
if len(values) != len(x):
3338+
raise ValueError(f"'{name}' must be of length 'x', not {len(values)}")
3339+
33353340
if normalize:
3336-
x = x / sx
3341+
fracs = x / sx
33373342
elif sx > 1:
33383343
raise ValueError('Cannot plot an unnormalized pie with sum(x) > 1')
3339-
if labels is None:
3340-
labels = [''] * len(x)
3344+
else:
3345+
fracs = x
33413346
if explode is None:
33423347
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)}")
3348+
check_length("explode", explode)
33473349
if colors is None:
33483350
get_next_color = self._get_patches_for_fill.get_next_color
33493351
else:
@@ -3368,16 +3370,143 @@ def get_next_color():
33683370

33693371
texts = []
33703372
slices = []
3371-
autotexts = []
33723373

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

3380-
w = mpatches.Wedge((x, y), radius, 360. * min(theta1, theta2),
3509+
w = mpatches.Wedge((x_pos, y_pos), radius, 360. * min(theta1, theta2),
33813510
360. * max(theta1, theta2),
33823511
facecolor=get_next_color(),
33833512
hatch=next(hatch_cycle),
@@ -3395,44 +3524,30 @@ def get_next_color():
33953524
shadow_dict.update(shadow)
33963525
self.add_patch(mpatches.Shadow(w, **shadow_dict))
33973526

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)
3527+
if wls.size > 0:
3528+
# Add wedge labels
3529+
for i, (wl, ld, pf, rot) in enumerate(
3530+
zip(wls, wedge_label_distance, prop_funcs,
3531+
rotate_wedge_labels)):
3532+
xt = x_pos + ld * radius * math.cos(thetam)
3533+
yt = y_pos + ld * radius * math.sin(thetam)
3534+
fontsize, label_alignment_h = pf(ld, xt > 0)
3535+
label_alignment_v = 'center'
3536+
label_rotation = 'horizontal'
3537+
if rot:
3538+
label_alignment_v = 'bottom' if yt > 0 else 'top'
3539+
label_rotation = (np.rad2deg(thetam) + (0 if xt > 0 else 180))
3540+
t = self.text(xt, yt, wl,
3541+
clip_on=False,
3542+
horizontalalignment=label_alignment_h,
3543+
verticalalignment=label_alignment_v,
3544+
rotation=label_rotation,
3545+
size=fontsize)
3546+
t.set(**textprops)
3547+
if pf is legacy:
3548+
texts.append(t)
3549+
else:
3550+
wedgetexts[i].append(t)
34363551

34373552
theta1 = theta2
34383553

@@ -3443,10 +3558,12 @@ def get_next_color():
34433558
xlim=(-1.25 + center[0], 1.25 + center[0]),
34443559
ylim=(-1.25 + center[1], 1.25 + center[1]))
34453560

3446-
if autopct is None:
3561+
if not wedgetexts:
34473562
return slices, texts
3563+
elif len(wedgetexts) == 1:
3564+
return slices, texts, wedgetexts[0]
34483565
else:
3449-
return slices, texts, autotexts
3566+
return slices, texts, wedgetexts
34503567

34513568
@staticmethod
34523569
def _errorevery_to_mask(x, errorevery):

0 commit comments

Comments
 (0)