@@ -2337,6 +2337,17 @@ def _changed(self):
23372337        """ 
23382338        self .callbacks .process ('changed' )
23392339
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+ 
23402351
23412352class  Normalize (Norm ):
23422353    """ 
@@ -2547,6 +2558,19 @@ def scaled(self):
25472558        # docstring inherited 
25482559        return  self .vmin  is  not None  and  self .vmax  is  not None 
25492560
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+ 
25502574
25512575class  TwoSlopeNorm (Normalize ):
25522576    def  __init__ (self , vcenter , vmin = None , vmax = None ):
@@ -3272,6 +3296,300 @@ def inverse(self, value):
32723296        return  value 
32733297
32743298
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 } { len (data .dtype .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}  )
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 } { 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 }  )
3583+ 
3584+             raise  ValueError (
3585+                 "MultiNorm expects a sequence with one element per component. " 
3586+                 f"This MultiNorm has { n_components }  
3587+                 f"with { n_elements }  
3588+                 )
3589+ 
3590+         return  tuple (data [i ] for  i  in  range (n_elements ))
3591+ 
3592+ 
32753593def  rgb_to_hsv (arr ):
32763594    """ 
32773595    Convert an array of float RGB values (in the range [0, 1]) to HSV values. 
0 commit comments