1
+ import collections .abc
1
2
import functools
2
3
import itertools
3
4
import logging
@@ -3202,10 +3203,10 @@ 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 ,
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 ),
3209
3210
frame = False , rotatelabels = False , * , normalize = True , hatch = None ):
3210
3211
"""
3211
3212
Plot a pie chart.
@@ -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,143 @@ 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
+ 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
3375
3504
theta2 = (theta1 + frac ) if counterclock else (theta1 - frac )
3376
3505
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 )
3379
3508
3380
- w = mpatches .Wedge ((x , y ), radius , 360. * min (theta1 , theta2 ),
3509
+ w = mpatches .Wedge ((x_pos , y_pos ), radius , 360. * min (theta1 , theta2 ),
3381
3510
360. * max (theta1 , theta2 ),
3382
3511
facecolor = get_next_color (),
3383
3512
hatch = next (hatch_cycle ),
@@ -3395,44 +3524,30 @@ def get_next_color():
3395
3524
shadow_dict .update (shadow )
3396
3525
self .add_patch (mpatches .Shadow (w , ** shadow_dict ))
3397
3526
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 )
3436
3551
3437
3552
theta1 = theta2
3438
3553
@@ -3443,10 +3558,12 @@ def get_next_color():
3443
3558
xlim = (- 1.25 + center [0 ], 1.25 + center [0 ]),
3444
3559
ylim = (- 1.25 + center [1 ], 1.25 + center [1 ]))
3445
3560
3446
- if autopct is None :
3561
+ if not wedgetexts :
3447
3562
return slices , texts
3563
+ elif len (wedgetexts ) == 1 :
3564
+ return slices , texts , wedgetexts [0 ]
3448
3565
else :
3449
- return slices , texts , autotexts
3566
+ return slices , texts , wedgetexts
3450
3567
3451
3568
@staticmethod
3452
3569
def _errorevery_to_mask (x , errorevery ):
0 commit comments