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 = x
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,155 @@ def get_next_color():
3368
3370
3369
3371
texts = []
3370
3372
slices = []
3371
- autotexts = []
3372
3373
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
3375
3516
theta2 = (theta1 + frac )if counterclock else (theta1 - frac )
3376
3517
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 )
3379
3520
3380
- w = mpatches .Wedge ((x , y ),radius ,360. * min (theta1 ,theta2 ),
3521
+ w = mpatches .Wedge ((x_pos , y_pos ),radius ,360. * min (theta1 ,theta2 ),
3381
3522
360. * max (theta1 ,theta2 ),
3382
3523
facecolor = get_next_color (),
3383
3524
hatch = next (hatch_cycle ),
@@ -3395,44 +3536,32 @@ def get_next_color():
3395
3536
shadow_dict .update (shadow )
3396
3537
self .add_patch (mpatches .Shadow (w ,** shadow_dict ))
3397
3538
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 )
3436
3565
3437
3566
theta1 = theta2
3438
3567
@@ -3443,10 +3572,16 @@ def get_next_color():
3443
3572
xlim = (- 1.25 + center [0 ],1.25 + center [0 ]),
3444
3573
ylim = (- 1.25 + center [1 ],1.25 + center [1 ]))
3445
3574
3446
- if autopct is None :
3575
+ print (f'{ wedgetexts = } ' )
3576
+ print (f'{ len (wedgetexts )= } ' )
3577
+
3578
+ if not wedgetexts :
3447
3579
return slices ,texts
3580
+ elif len (wedgetexts )== 1 :
3581
+ print ('return first' )
3582
+ return slices ,texts ,wedgetexts [0 ]
3448
3583
else :
3449
- return slices ,texts ,autotexts
3584
+ return slices ,texts ,wedgetexts
3450
3585
3451
3586
@staticmethod
3452
3587
def _errorevery_to_mask (x ,errorevery ):