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