Skip to content

Commit c24c7ad

Browse files
committed
WIP [skip ci]
1 parent 677d990 commit c24c7ad

File tree

2 files changed

+243
-98
lines changed

2 files changed

+243
-98
lines changed

lib/matplotlib/axes/_axes.py

Lines changed: 194 additions & 59 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,9 +3203,9 @@ 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,
3206+
def pie(self, x, explode=None, labels=None, colors=None, wedge_labels=None,
3207+
wedge_label_distance=0.6, autopct=None, pctdistance=0.6, shadow=False,
3208+
labeldistance=1.1, startangle=0, radius=1, counterclock=True,
32083209
wedgeprops=None, textprops=None, center=(0, 0),
32093210
frame=False, rotatelabels=False, *, normalize=True, hatch=None):
32103211
"""
@@ -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,155 @@ 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+
if (nl := len(processed_wedge_labels)) != (nd := len(wedge_label_distance)):
3445+
raise ValueError(
3446+
f"Found {nl} sets of wedge labels but {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 (nl := len(processed_wedge_labels)) != (nr := len(rotate_wedge_labels)):
3455+
raise ValueError(
3456+
f"Found {nl} sets of wedge labels but {nr} wedge label rotation choices.")
3457+
3458+
prop_funcs = [flexible] * len(processed_wedge_labels)
3459+
3460+
3461+
if autopct is not None:
3462+
if isinstance(autopct, str):
3463+
processed_pct = [sanitize_formatted_string(autopct % (100. * frac))
3464+
for frac in fracs]
3465+
elif callable(autopct):
3466+
processed_pct = [sanitize_formatted_string(autopct(100. * frac))
3467+
for frac in fracs]
3468+
else:
3469+
raise TypeError('autopct must be callable or a format string')
3470+
3471+
processed_wedge_labels.append(processed_pct)
3472+
wedge_label_distance.append(pctdistance)
3473+
prop_funcs.append(fixed)
3474+
rotate_wedge_labels.append(False)
3475+
3476+
wedgetexts = [[]] * len(processed_wedge_labels)
3477+
3478+
3479+
print(f'{labels=}')
3480+
3481+
if labels is None:
3482+
labels = [None] * len(x)
3483+
else:
3484+
check_length("labels", labels)
3485+
if labeldistance is not None:
3486+
_api.warn_deprecated(
3487+
"3.11", pending=True,
3488+
message=("Setting a non-None labeldistance is deprecated and "
3489+
"will error in future."),
3490+
alternative="wedge_labels and wedge_label_distance")
3491+
3492+
processed_wedge_labels.append(labels)
3493+
wedge_label_distance.append(labeldistance)
3494+
prop_funcs.append(legacy)
3495+
rotate_wedge_labels.append(rotatelabels)
3496+
3497+
3498+
print(f'{processed_wedge_labels=}')
3499+
3500+
# Transpose so we can loop over wedges
3501+
processed_wedge_labels = np.transpose(processed_wedge_labels)
3502+
if not processed_wedge_labels.size:
3503+
print('reshaping')
3504+
processed_wedge_labels = processed_wedge_labels.reshape(len(x), 0)
3505+
3506+
print(f'{processed_wedge_labels=}')
3507+
print(f'{processed_wedge_labels.shape=}')
3508+
print(f'{wedge_label_distance=}')
3509+
print(f'{prop_funcs=}')
3510+
print(f'{rotate_wedge_labels=}')
3511+
3512+
for absval, frac, label, expl, wls in zip(x, fracs, labels, explode,
3513+
processed_wedge_labels):
3514+
print(f'{wls=}')
3515+
x_pos, y_pos = center
33753516
theta2 = (theta1 + frac) if counterclock else (theta1 - frac)
33763517
thetam = 2 * np.pi * 0.5 * (theta1 + theta2)
3377-
x += expl * math.cos(thetam)
3378-
y += expl * math.sin(thetam)
3518+
x_pos += expl * math.cos(thetam)
3519+
y_pos += expl * math.sin(thetam)
33793520

3380-
w = mpatches.Wedge((x, y), radius, 360. * min(theta1, theta2),
3521+
w = mpatches.Wedge((x_pos, y_pos), radius, 360. * min(theta1, theta2),
33813522
360. * max(theta1, theta2),
33823523
facecolor=get_next_color(),
33833524
hatch=next(hatch_cycle),
@@ -3395,44 +3536,32 @@ def get_next_color():
33953536
shadow_dict.update(shadow)
33963537
self.add_patch(mpatches.Shadow(w, **shadow_dict))
33973538

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)
3539+
if wls.size > 0:
3540+
# Add wedge labels
3541+
print('hello')
3542+
for i, (wl, ld, pf, rot) in enumerate(
3543+
zip(wls, wedge_label_distance, prop_funcs, rotate_wedge_labels)):
3544+
print(f'{wl=}')
3545+
xt = x_pos + ld * radius * math.cos(thetam)
3546+
yt = y_pos + ld * radius * math.sin(thetam)
3547+
fontsize, label_alignment_h = pf(ld, xt > 0)
3548+
label_alignment_v = 'center'
3549+
label_rotation = 'horizontal'
3550+
if rot:
3551+
label_alignment_v = 'bottom' if yt > 0 else 'top'
3552+
label_rotation = (np.rad2deg(thetam)
3553+
+ (0 if xt > 0 else 180))
3554+
t = self.text(xt, yt, wl,
3555+
clip_on=False,
3556+
horizontalalignment=label_alignment_h,
3557+
verticalalignment=label_alignment_v,
3558+
rotation=label_rotation,
3559+
size=fontsize)
3560+
t.set(**textprops)
3561+
if pf is legacy:
3562+
texts.append(t)
3563+
else:
3564+
wedgetexts[i].append(t)
34363565

34373566
theta1 = theta2
34383567

@@ -3443,10 +3572,16 @@ def get_next_color():
34433572
xlim=(-1.25 + center[0], 1.25 + center[0]),
34443573
ylim=(-1.25 + center[1], 1.25 + center[1]))
34453574

3446-
if autopct is None:
3575+
print(f'{wedgetexts=}')
3576+
print(f'{len(wedgetexts)=}')
3577+
3578+
if not wedgetexts:
34473579
return slices, texts
3580+
elif len(wedgetexts) == 1:
3581+
print('return first')
3582+
return slices, texts, wedgetexts[0]
34483583
else:
3449-
return slices, texts, autotexts
3584+
return slices, texts, wedgetexts
34503585

34513586
@staticmethod
34523587
def _errorevery_to_mask(x, errorevery):

0 commit comments

Comments
 (0)