@@ -2337,6 +2337,17 @@ def _changed(self):
2337
2337
"""
2338
2338
self .callbacks .process ('changed' )
2339
2339
2340
+ @property
2341
+ @abstractmethod
2342
+ def n_components (self ):
2343
+ """
2344
+ The number of normalized components.
2345
+
2346
+ This is the number of elements of the parameter to ``__call__`` and of
2347
+ *vmin*, *vmax*.
2348
+ """
2349
+ pass
2350
+
2340
2351
2341
2352
class Normalize (Norm ):
2342
2353
"""
@@ -2547,6 +2558,19 @@ def scaled(self):
2547
2558
# docstring inherited
2548
2559
return self .vmin is not None and self .vmax is not None
2549
2560
2561
+ @property
2562
+ def n_components (self ):
2563
+ """
2564
+ The number of distinct components supported (1).
2565
+
2566
+ This is the number of elements of the parameter to ``__call__`` and of
2567
+ *vmin*, *vmax*.
2568
+
2569
+ This class support only a single component, as opposed to `MultiNorm`
2570
+ which supports multiple components.
2571
+ """
2572
+ return 1
2573
+
2550
2574
2551
2575
class TwoSlopeNorm (Normalize ):
2552
2576
def __init__ (self , vcenter , vmin = None , vmax = None ):
@@ -3272,6 +3296,300 @@ def inverse(self, value):
3272
3296
return value
3273
3297
3274
3298
3299
+ class MultiNorm (Norm ):
3300
+ """
3301
+ A class which contains multiple scalar norms.
3302
+ """
3303
+
3304
+ def __init__ (self , norms , vmin = None , vmax = None , clip = None ):
3305
+ """
3306
+ Parameters
3307
+ ----------
3308
+ norms : list of (str or `Normalize`)
3309
+ The constituent norms. The list must have a minimum length of 1.
3310
+ vmin, vmax : None or list of (float or None)
3311
+ Limits of the constituent norms.
3312
+ If a list, one value is assigned to each of the constituent
3313
+ norms.
3314
+ If None, the limits of the constituent norms
3315
+ are not changed.
3316
+ clip : None or list of bools, default: None
3317
+ Determines the behavior for mapping values outside the range
3318
+ ``[vmin, vmax]`` for the constituent norms.
3319
+ If a list, each value is assigned to each of the constituent
3320
+ norms.
3321
+ If None, the behaviour of the constituent norms is not changed.
3322
+ """
3323
+ if cbook .is_scalar_or_string (norms ):
3324
+ raise ValueError (
3325
+ "MultiNorm must be assigned an iterable of norms, where each "
3326
+ f"norm is of type `str`, or `Normalize`, not { type (norms )} " )
3327
+
3328
+ if len (norms ) < 1 :
3329
+ raise ValueError ("MultiNorm must be assigned at least one norm" )
3330
+
3331
+ def resolve (norm ):
3332
+ if isinstance (norm , str ):
3333
+ scale_cls = _api .check_getitem (scale ._scale_mapping , norm = norm )
3334
+ return mpl .colorizer ._auto_norm_from_scale (scale_cls )()
3335
+ elif isinstance (norm , Normalize ):
3336
+ return norm
3337
+ else :
3338
+ raise ValueError (
3339
+ "Each norm assigned to MultiNorm must be "
3340
+ f"of type `str`, or `Normalize`, not { type (norm )} " )
3341
+
3342
+ self ._norms = tuple (resolve (norm ) for norm in norms )
3343
+
3344
+ self .callbacks = cbook .CallbackRegistry (signals = ["changed" ])
3345
+
3346
+ self .vmin = vmin
3347
+ self .vmax = vmax
3348
+ self .clip = clip
3349
+
3350
+ for n in self ._norms :
3351
+ n .callbacks .connect ('changed' , self ._changed )
3352
+
3353
+ @property
3354
+ def n_components (self ):
3355
+ """Number of norms held by this `MultiNorm`."""
3356
+ return len (self ._norms )
3357
+
3358
+ @property
3359
+ def norms (self ):
3360
+ """The individual norms held by this `MultiNorm`."""
3361
+ return self ._norms
3362
+
3363
+ @property
3364
+ def vmin (self ):
3365
+ """The lower limit of each constituent norm."""
3366
+ return tuple (n .vmin for n in self ._norms )
3367
+
3368
+ @vmin .setter
3369
+ def vmin (self , values ):
3370
+ if values is None :
3371
+ return
3372
+ if not np .iterable (values ) or len (values ) != self .n_components :
3373
+ raise ValueError ("*vmin* must have one component for each norm. "
3374
+ f"Expected an iterable of length { self .n_components } , "
3375
+ f"but got { values !r} " )
3376
+ with self .callbacks .blocked ():
3377
+ for norm , v in zip (self .norms , values ):
3378
+ norm .vmin = v
3379
+ self ._changed ()
3380
+
3381
+ @property
3382
+ def vmax (self ):
3383
+ """The upper limit of each constituent norm."""
3384
+ return tuple (n .vmax for n in self ._norms )
3385
+
3386
+ @vmax .setter
3387
+ def vmax (self , values ):
3388
+ if values is None :
3389
+ return
3390
+ if not np .iterable (values ) or len (values ) != self .n_components :
3391
+ raise ValueError ("*vmax* must have one component for each norm. "
3392
+ f"Expected an iterable of length { self .n_components } , "
3393
+ f"but got { values !r} " )
3394
+ with self .callbacks .blocked ():
3395
+ for norm , v in zip (self .norms , values ):
3396
+ norm .vmax = v
3397
+ self ._changed ()
3398
+
3399
+ @property
3400
+ def clip (self ):
3401
+ """The clip behaviour of each constituent norm."""
3402
+ return tuple (n .clip for n in self ._norms )
3403
+
3404
+ @clip .setter
3405
+ def clip (self , values ):
3406
+ if values is None :
3407
+ return
3408
+ if not np .iterable (values ) or len (values ) != self .n_components :
3409
+ raise ValueError ("*clip* must have one component for each norm. "
3410
+ f"Expected an iterable of length { self .n_components } , "
3411
+ f"but got { values !r} " )
3412
+ with self .callbacks .blocked ():
3413
+ for norm , v in zip (self .norms , values ):
3414
+ norm .clip = v
3415
+ self ._changed ()
3416
+
3417
+ def _changed (self ):
3418
+ """
3419
+ Call this whenever the norm is changed to notify all the
3420
+ callback listeners to the 'changed' signal.
3421
+ """
3422
+ self .callbacks .process ('changed' )
3423
+
3424
+ def __call__ (self , values , clip = None ):
3425
+ """
3426
+ Normalize the data and return the normalized data.
3427
+
3428
+ Each component of the input is normalized via the constituent norm.
3429
+
3430
+ Parameters
3431
+ ----------
3432
+ values : array-like
3433
+ The input data, as an iterable or a structured numpy array.
3434
+
3435
+ - If iterable, must be of length `n_components`. Each element can be a
3436
+ scalar or array-like and is normalized through the corresponding norm.
3437
+ - If structured array, must have `n_components` fields. Each field
3438
+ is normalized through the corresponding norm.
3439
+
3440
+ clip : list of bools or None, optional
3441
+ Determines the behavior for mapping values outside the range
3442
+ ``[vmin, vmax]``. See the description of the parameter *clip* in
3443
+ `.Normalize`.
3444
+ If ``None``, defaults to ``self.clip`` (which defaults to
3445
+ ``False``).
3446
+
3447
+ Returns
3448
+ -------
3449
+ tuple
3450
+ Normalized input values
3451
+
3452
+ Notes
3453
+ -----
3454
+ If not already initialized, ``self.vmin`` and ``self.vmax`` are
3455
+ initialized using ``self.autoscale_None(values)``.
3456
+ """
3457
+ if clip is None :
3458
+ clip = self .clip
3459
+ if not np .iterable (clip ) or len (clip ) != self .n_components :
3460
+ raise ValueError ("*clip* must have one component for each norm. "
3461
+ f"Expected an iterable of length { self .n_components } , "
3462
+ f"but got { clip !r} " )
3463
+
3464
+ values = self ._iterable_components_in_data (values , self .n_components )
3465
+ result = tuple (n (v , clip = c ) for n , v , c in zip (self .norms , values , clip ))
3466
+ return result
3467
+
3468
+ def inverse (self , values ):
3469
+ """
3470
+ Map the normalized values (i.e., index in the colormap) back to data values.
3471
+
3472
+ Parameters
3473
+ ----------
3474
+ values : array-like
3475
+ The input data, as an iterable or a structured numpy array.
3476
+
3477
+ - If iterable, must be of length `n_components`. Each element can be a
3478
+ scalar or array-like and is mapped through the corresponding norm.
3479
+ - If structured array, must have `n_components` fields. Each field
3480
+ is mapped through the the corresponding norm.
3481
+
3482
+ """
3483
+ values = self ._iterable_components_in_data (values , self .n_components )
3484
+ result = tuple (n .inverse (v ) for n , v in zip (self .norms , values ))
3485
+ return result
3486
+
3487
+ def autoscale (self , A ):
3488
+ """
3489
+ For each constituent norm, set *vmin*, *vmax* to min, max of the corresponding
3490
+ component in *A*.
3491
+
3492
+ Parameters
3493
+ ----------
3494
+ A : array-like
3495
+ The input data, as an iterable or a structured numpy array.
3496
+
3497
+ - If iterable, must be of length `n_components`. Each element
3498
+ is used for the limits of one constituent norm.
3499
+ - If structured array, must have `n_components` fields. Each field
3500
+ is used for the limits of one constituent norm.
3501
+ """
3502
+ with self .callbacks .blocked ():
3503
+ A = self ._iterable_components_in_data (A , self .n_components )
3504
+ for n , a in zip (self .norms , A ):
3505
+ n .autoscale (a )
3506
+ self ._changed ()
3507
+
3508
+ def autoscale_None (self , A ):
3509
+ """
3510
+ If *vmin* or *vmax* are not set on any constituent norm,
3511
+ use the min/max of the corresponding component in *A* to set them.
3512
+
3513
+ Parameters
3514
+ ----------
3515
+ A : array-like
3516
+ The input data, as an iterable or a structured numpy array.
3517
+
3518
+ - If iterable, must be of length `n_components`. Each element
3519
+ is used for the limits of one constituent norm.
3520
+ - If structured array, must have `n_components` fields. Each field
3521
+ is used for the limits of one constituent norm.
3522
+ """
3523
+ with self .callbacks .blocked ():
3524
+ A = self ._iterable_components_in_data (A , self .n_components )
3525
+ for n , a in zip (self .norms , A ):
3526
+ n .autoscale_None (a )
3527
+ self ._changed ()
3528
+
3529
+ def scaled (self ):
3530
+ """Return whether both *vmin* and *vmax* are set on all constituent norms."""
3531
+ return all (n .scaled () for n in self .norms )
3532
+
3533
+ @staticmethod
3534
+ def _iterable_components_in_data (data , n_components ):
3535
+ """
3536
+ Provides an iterable over the components contained in the data.
3537
+
3538
+ An input array with `n_components` fields is returned as a tuple of length n
3539
+ referencing slices of the original array.
3540
+
3541
+ Parameters
3542
+ ----------
3543
+ data : array-like
3544
+ The input data, as an iterable or a structured numpy array.
3545
+
3546
+ - If iterable, must be of length `n_components`
3547
+ - If structured array, must have `n_components` fields.
3548
+
3549
+ Returns
3550
+ -------
3551
+ tuple of np.ndarray
3552
+
3553
+ """
3554
+ if isinstance (data , np .ndarray ) and data .dtype .fields is not None :
3555
+ # structured array
3556
+ if len (data .dtype .fields ) != n_components :
3557
+ raise ValueError (
3558
+ "Structured array inputs to MultiNorm must have the same "
3559
+ "number of fields as components in the MultiNorm. Expected "
3560
+ f"{ n_components } , but got { len (data .dtype .fields )} fields"
3561
+ )
3562
+ else :
3563
+ return tuple (data [field ] for field in data .dtype .names )
3564
+ try :
3565
+ n_elements = len (data )
3566
+ except TypeError :
3567
+ raise ValueError ("MultiNorm expects a sequence with one element per "
3568
+ f"component as input, but got { data !r} instead" )
3569
+ if n_elements != n_components :
3570
+ if isinstance (data , np .ndarray ) and data .shape [- 1 ] == n_components :
3571
+ if len (data .shape ) == 2 :
3572
+ raise ValueError (
3573
+ f"MultiNorm expects a sequence with one element per component. "
3574
+ "You can use `data_transposed = data.T` "
3575
+ "to convert the input data of shape "
3576
+ f"{ data .shape } to a compatible shape { data .shape [::- 1 ]} " )
3577
+ else :
3578
+ raise ValueError (
3579
+ f"MultiNorm expects a sequence with one element per component. "
3580
+ "You can use `data_as_list = [data[..., i] for i in "
3581
+ "range(data.shape[-1])]` to convert the input data of shape "
3582
+ f" { data .shape } to a compatible list" )
3583
+
3584
+ raise ValueError (
3585
+ "MultiNorm expects a sequence with one element per component. "
3586
+ f"This MultiNorm has { n_components } components, but got a sequence "
3587
+ f"with { n_elements } elements"
3588
+ )
3589
+
3590
+ return tuple (data [i ] for i in range (n_elements ))
3591
+
3592
+
3275
3593
def rgb_to_hsv (arr ):
3276
3594
"""
3277
3595
Convert an array of float RGB values (in the range [0, 1]) to HSV values.
0 commit comments