Skip to content

Commit 3a8ad1d

Browse files
trygvradtimhoffmQuLogic
authored
MultiNorm class (matplotlib#29876)
* MultiNorm class This commit merges a number of commits now contained in https://github.com/trygvrad/matplotlib/tree/multivariate-plot-prapare-backup , keeping only the MultiNorm class * Apply suggestions from code review Co-authored-by: Tim Hoffmann <[email protected]> * Apply suggestions from code review Co-authored-by: Tim Hoffmann <[email protected]> * Updated input types for MultiNorm.__call__() * improved error messages in MultiNorm * updated types and errors for MultiNorm * Apply suggestions from code review Co-authored-by: Tim Hoffmann <[email protected]> * Updated return type of MultiNorm * Apply suggestions from code review Co-authored-by: Tim Hoffmann <[email protected]> * updated docstrings in MultiNorm * Apply suggestions from code review Co-authored-by: Elliott Sales de Andrade <[email protected]> * changed so that vmin/vmax must be iterable * Update colors.pyi * change to handling of bad norm names in MultiNorm * Update colors.py * Apply suggestions from code review Co-authored-by: Elliott Sales de Andrade <[email protected]> * change minimum length of multiNorm to 1. * Apply suggestions from code review Co-authored-by: Elliott Sales de Andrade <[email protected]> * Apply suggestions from @QuLogic --------- Co-authored-by: Tim Hoffmann <[email protected]> Co-authored-by: Elliott Sales de Andrade <[email protected]>
1 parent 29ba27a commit 3a8ad1d

File tree

4 files changed

+536
-0
lines changed

4 files changed

+536
-0
lines changed

doc/api/colors_api.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ Color norms
3232
PowerNorm
3333
SymLogNorm
3434
TwoSlopeNorm
35+
MultiNorm
3536

3637
Univariate Colormaps
3738
--------------------

lib/matplotlib/colors.py

Lines changed: 318 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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

23412352
class 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

25512575
class 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+
32753593
def rgb_to_hsv(arr):
32763594
"""
32773595
Convert an array of float RGB values (in the range [0, 1]) to HSV values.

0 commit comments

Comments
 (0)