@@ -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 } , 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+
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