1
+ import collections .abc
1
2
import functools
2
3
import itertools
3
4
import logging
@@ -3202,9 +3203,9 @@ def stem(self, *args, linefmt=None, markerfmt=None, basefmt=None, bottom=0,
3202
3203
3203
3204
@_api .make_keyword_only ("3.9" ,"explode" )
3204
3205
@_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 ,
3208
3209
wedgeprops = None ,textprops = None ,center = (0 ,0 ),
3209
3210
frame = False ,rotatelabels = False ,* ,normalize = True ,hatch = None ):
3210
3211
"""
@@ -3239,6 +3240,8 @@ def pie(self, x, explode=None, labels=None, colors=None,
3239
3240
3240
3241
.. versionadded:: 3.7
3241
3242
3243
+ wedge_labels :
3244
+
3242
3245
autopct : None or str or callable, default: None
3243
3246
If not *None*, *autopct* is a string or function used to label the
3244
3247
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,
3321
3324
The Axes aspect ratio can be controlled with `.Axes.set_aspect`.
3322
3325
"""
3323
3326
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 )
3327
3328
if x .ndim > 1 :
3328
3329
raise ValueError ("x must be 1D" )
3329
3330
@@ -3332,18 +3333,19 @@ def pie(self, x, explode=None, labels=None, colors=None,
3332
3333
3333
3334
sx = x .sum ()
3334
3335
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
+
3335
3340
if normalize :
3336
- x = x / sx
3341
+ fracs = x / sx
3337
3342
elif sx > 1 :
3338
3343
raise ValueError ('Cannot plot an unnormalized pie with sum(x) > 1' )
3339
- if labels is None :
3340
- labels = [ '' ] * len ( x )
3344
+ else :
3345
+ fracs = xt
3341
3346
if explode is None :
3342
3347
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 )
3347
3349
if colors is None :
3348
3350
get_next_color = self ._get_patches_for_fill .get_next_color
3349
3351
else :
@@ -3368,16 +3370,145 @@ def get_next_color():
3368
3370
3369
3371
texts = []
3370
3372
slices = []
3371
- autotexts = []
3373
+ wedgetexts = []
3374
+
3375
+ # Define some functions for choosing label fontize and horizontal alignment
3376
+ # based on distance and whether we are right of center (i.e. cartesian x > 0)
3377
+
3378
+ def legacy (distance ,is_right ):
3379
+ # Used to place `labels`. This function can be removed when the
3380
+ # `labeldistance` deprecation expires. Always align so the labels
3381
+ # do not overlap the pie
3382
+ ha = 'left' if is_right else 'right'
3383
+ return mpl .rcParams ['xtick.labelsize' ],ha
3384
+
3385
+ def flexible (distance ,is_right ):
3386
+ if distance >= 1 :
3387
+ # Align so the labels do not overlap the pie
3388
+ ha = 'left' if is_right else 'right'
3389
+ else :
3390
+ ha = 'center'
3391
+
3392
+ return None ,ha
3393
+
3394
+ def fixed (distance ,is_right ):
3395
+ # Used to place the labels generated with autopct. Always centered
3396
+ # for backwards compatibility
3397
+ return None ,'center'
3398
+
3399
+ # Build a (possibly empty) list of lists of wedge labels, with corresponding
3400
+ # lists of distances, rotation choices and alignment functions
3401
+
3402
+ def sanitize_formatted_string (s ):
3403
+ if mpl ._val_or_rc (textprops .get ("usetex" ),"text.usetex" ):
3404
+ # escape % (i.e. \%) if it is not already escaped
3405
+ return re .sub (r"([^\\])%" ,r"\1\\%" ,s )
3406
+
3407
+ return s
3408
+
3409
+ def fmt_str_to_list (wl ):
3410
+ return [sanitize_formatted_string (wl .format (abs = abs ,frac = frac ))
3411
+ for abs ,frac in zip (x ,fracs )]
3412
+
3413
+ if wedge_labels is None :
3414
+ processed_wedge_labels = []
3415
+ wedge_label_distance = []
3416
+ rotate_wedge_labels = []
3417
+ elif isinstance (wedge_labels ,str ):
3418
+ # Format string.
3419
+ processed_wedge_labels = [fmt_str_to_list (wedge_labels )]
3420
+ elif not isinstance (wedge_labels ,collections .abc .Sequence ):
3421
+ raise TypeError ("wedge_labels must be a string or sequence" )
3422
+ else :
3423
+ wl0 = wedge_labels [0 ]
3424
+ if isinstance (wl0 ,str )and wl0 .format (abs = abs ,frac = frac )== wl0 :
3425
+ # Plain string. Assume we have a sequence of ready-made labels
3426
+ check_length ("wedge_labels" ,wedge_labels )
3427
+ processed_wedge_labels = [wedge_labels ]
3428
+ else :
3429
+ processed_wedge_labels = []
3430
+ for wl in wedge_labels :
3431
+ if isinstance (wl ,str ):
3432
+ # Format string
3433
+ processed_wedge_labels .append (fmt_str_to_list (wl ))
3434
+ else :
3435
+ # Ready made list
3436
+ check_length ("wedge_labels[i]" ,wl )
3437
+ processed_wedge_labels .append (wl )
3438
+
3439
+ if isinstance (wedge_label_distance ,Number ):
3440
+ wedge_label_distance = [wedge_label_distance ]
3441
+ else :
3442
+ # Copy so we won't append to user input
3443
+ wedge_label_distance = wedge_label_distance [:]
3444
+
3445
+ if (nl := len (processed_wedge_labels ))!= (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 [:]
3372
3454
3373
- for frac ,label ,expl in zip (x ,labels ,explode ):
3374
- x ,y = center
3455
+ if (nl := len (processed_wedge_labels ))!= (nr := len (rotate_wedge_labels )):
3456
+ raise ValueError (
3457
+ f"Found{ nl } sets of wedge labels but{ nr } wedge label rotation choices." )
3458
+
3459
+ prop_funcs = [flexible ]* len (processed_wedge_labels )
3460
+
3461
+ print (f'{ labels = } ' )
3462
+
3463
+ if labels is None :
3464
+ labels = [None ]* len (x )
3465
+ else :
3466
+ check_length ("labels" ,labels )
3467
+ if labeldistance is not None :
3468
+ _api .warn_deprecated (
3469
+ "3.11" ,pending = True ,
3470
+ message = ("Setting a non-None labeldistance is deprecated and "
3471
+ "will error in future." ),
3472
+ alternative = "wedge_labels and wedge_label_distance" )
3473
+
3474
+ processed_wedge_labels .append (labels )
3475
+ wedge_label_distance .append (labeldistance )
3476
+ prop_funcs .append (legacy )
3477
+ rotate_wedge_labels .append (rotatelabels )
3478
+
3479
+ if autopct is not None :
3480
+ if isinstance (autopct ,str ):
3481
+ processed_pct = [sanitize_formatted_string (autopct % (100. * frac ))
3482
+ for frac in fracs ]
3483
+ elif callable (autopct ):
3484
+ processed_pct = [sanitize_formatted_string (autopct (100. * frac ))
3485
+ for frac in fracs ]
3486
+ else :
3487
+ raise TypeError ('autopct must be callable or a format string' )
3488
+
3489
+ processed_wedge_labels .append (processed_pct )
3490
+ wedge_label_distance .append (pctdistance )
3491
+ prop_funcs .append (fixed )
3492
+ rotate_wedge_labels .append (False )
3493
+
3494
+ # Transpose so we can loop over wedges
3495
+ processed_wedge_labels = np .transpose (processed_wedge_labels )
3496
+
3497
+ print (f'{ processed_wedge_labels = } ' )
3498
+ print (f'{ wedge_label_distance = } ' )
3499
+ print (f'{ prop_funcs = } ' )
3500
+ print (f'{ rotate_wedge_labels = } ' )
3501
+
3502
+ for abs ,frac ,label ,expl ,wls in zip (x ,fracs ,labels ,explode ,
3503
+ processed_wedge_labels ):
3504
+ print (f'{ wls = } ' )
3505
+ x_pos ,y_pos = center
3375
3506
theta2 = (theta1 + frac )if counterclock else (theta1 - frac )
3376
3507
thetam = 2 * np .pi * 0.5 * (theta1 + theta2 )
3377
- x += expl * math .cos (thetam )
3378
- y += expl * math .sin (thetam )
3508
+ x_pos += expl * math .cos (thetam )
3509
+ y_pos += expl * math .sin (thetam )
3379
3510
3380
- w = mpatches .Wedge ((x , y ),radius ,360. * min (theta1 ,theta2 ),
3511
+ w = mpatches .Wedge ((x_pos , y_pos ),radius ,360. * min (theta1 ,theta2 ),
3381
3512
360. * max (theta1 ,theta2 ),
3382
3513
facecolor = get_next_color (),
3383
3514
hatch = next (hatch_cycle ),
@@ -3395,44 +3526,35 @@ def get_next_color():
3395
3526
shadow_dict .update (shadow )
3396
3527
self .add_patch (mpatches .Shadow (w ,** shadow_dict ))
3397
3528
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 )
3529
+ if wls .size > 0 :
3530
+ # Add wedge labels
3531
+ print ('hello' )
3532
+ this_wedge_texts = []
3533
+ for wl ,ld ,pf ,rot in zip (wls ,wedge_label_distance ,prop_funcs ,
3534
+ rotate_wedge_labels ):
3535
+ print (f'{ wl = } ' )
3536
+ xt = x_pos + ld * radius * math .cos (thetam )
3537
+ yt = y_pos + ld * radius * math .sin (thetam )
3538
+ fontsize ,label_alignment_h = pf (ld ,xt > 0 )
3539
+ label_alignment_v = 'center'
3540
+ label_rotation = 'horizontal'
3541
+ if rot :
3542
+ label_alignment_v = 'bottom' if yt > 0 else 'top'
3543
+ label_rotation = (np .rad2deg (thetam )
3544
+ + (0 if xt > 0 else 180 ))
3545
+ t = self .text (xt ,yt ,wl ,
3546
+ clip_on = False ,
3547
+ horizontalalignment = label_alignment_h ,
3548
+ verticalalignment = label_alignment_v ,
3549
+ rotation = label_rotation ,
3550
+ size = fontsize )
3551
+ t .set (** textprops )
3552
+ if pf is legacy :
3553
+ texts .append (t )
3554
+ else :
3555
+ this_wedge_texts .append (t )
3556
+
3557
+ wedgetexts .append (this_wedge_texts )
3436
3558
3437
3559
theta1 = theta2
3438
3560
@@ -3443,10 +3565,12 @@ def get_next_color():
3443
3565
xlim = (- 1.25 + center [0 ],1.25 + center [0 ]),
3444
3566
ylim = (- 1.25 + center [1 ],1.25 + center [1 ]))
3445
3567
3446
- if autopct is None :
3568
+ if not wedgetexts :
3447
3569
return slices ,texts
3570
+ elif len (wedgetexts )== 1 :
3571
+ return slices ,texts ,wedgetexts [0 ]
3448
3572
else :
3449
- return slices ,texts ,autotexts
3573
+ return slices ,texts ,wedgetexts
3450
3574
3451
3575
@staticmethod
3452
3576
def _errorevery_to_mask (x ,errorevery ):