Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
updated types and errors for MultiNorm
  • Loading branch information
trygvrad committed Jul 23, 2025
commit 270cb64a85dfa1352c47ad0721d8f375240b46b1
116 changes: 48 additions & 68 deletions lib/matplotlib/colors.py
Original file line number Diff line number Diff line change
Expand Up @@ -3413,9 +3413,9 @@ def __call__(self, values, clip=None, structured_output=None):
Parameters
----------
values : array-like
Data to normalize, as tuple or list or structured array.
The input data, as an iterable or a structured numpy array.

- If tuple or list, must be of length `n_components`
- If iterable, must be of length `n_components`
Comment thread
trygvrad marked this conversation as resolved.
Resolved
- If structured array, must have `n_components` fields.
Comment thread
trygvrad marked this conversation as resolved.
Resolved

clip : list of bools or bool or None, optional
Expand All @@ -3424,11 +3424,13 @@ def __call__(self, values, clip=None, structured_output=None):
`.Normalize`.
If ``None``, defaults to ``self.clip`` (which defaults to
``False``).

structured_output : bool, optional
Comment thread
timhoffm marked this conversation as resolved.
Resolved

- If True, output is returned as a structured array
- If False, output is returned as a tuple of length `n_components`
- If None (default) output is returned in the same format as the input.
- If None (default) output is returned as a structured array for
structured input, and otherwise returned as a tuple

Returns
Comment thread
trygvrad marked this conversation as resolved.
Resolved
-------
Expand Down Expand Up @@ -3466,13 +3468,13 @@ def inverse(self, values):

Parameters
----------
values
Normalized values, as tuple, scalar array or structured array.
values : array-like
The input data, as an iterable or a structured numpy array.

- If tuple, must be of length `n_components`
- If scalar array, the first axis must be of length `n_components`
- If iterable, must be of length `n_components`
- If structured array, must have `n_components` fields.

Comment thread
trygvrad marked this conversation as resolved.
Resolved

"""
values = self._iterable_components_in_data(values, self.n_components)
result = [n.inverse(v) for n, v in zip(self.norms, values)]
Expand Down Expand Up @@ -3505,7 +3507,7 @@ def autoscale_None(self, A):
Parameters
----------
A
Data, must be of length `n_components` or be a structured array or scalar
Data, must be of length `n_components` or be a structured array
with `n_components` fields.
"""
with self.callbacks.blocked():
Expand All @@ -3528,78 +3530,56 @@ def _iterable_components_in_data(data, n_components):

Parameters
----------
data : np.ndarray, tuple or list
The input data, as a tuple or list or structured array.
data : array-like
The input data, as an iterable or a structured numpy array.

- If tuple or list, must be of length `n_components`
- If iterable, must be of length `n_components`
- If structured array, must have `n_components` fields.


Returns
Comment thread
trygvrad marked this conversation as resolved.
Resolved
-------
tuple of np.ndarray
Comment thread
QuLogic marked this conversation as resolved.
Resolved

"""
if isinstance(data, np.ndarray):
if data.dtype.fields is not None:
data = tuple(data[descriptor[0]] for descriptor in data.dtype.descr)
if len(data) != n_components:
raise ValueError(f"{MultiNorm._get_input_err(n_components)}"
f". A structured array with "
f"{len(data)} fields is not compatible")
if isinstance(data, np.ndarray) and data.dtype.fields is not None:
# structured array
if len(data.dtype.fields) != n_components:
raise ValueError(
"Structured array inputs to MultiNorm must have the same "
"number of fields as components in the MultiNorm. Expected "
f"{n_components}, but got {len(data.dtype.fields)} fields"
)
else:
# Input is a scalar array, which we do not support.
# try to give a hint as to how the data can be converted to
# an accepted format
if ((len(data.shape) == 1 and
data.shape[0] == n_components) or
(len(data.shape) > 1 and
data.shape[0] == n_components and
data.shape[-1] != n_components)
):
raise ValueError(f"{MultiNorm._get_input_err(n_components)}"
". You can use `data_as_list = list(data)`"
" to convert the input data of shape"
f" {data.shape} to a compatible list")

elif (len(data.shape) > 1 and
data.shape[-1] == n_components and
data.shape[0] != n_components):
if len(data.shape) == 2:
raise ValueError(f"{MultiNorm._get_input_err(n_components)}"
". You can use `data_as_list = list(data.T)`"
" to convert the input data of shape"
f" {data.shape} to a compatible list")
else:
raise ValueError(f"{MultiNorm._get_input_err(n_components)}"
". You can use `data_as_list = [data[..., i]"
" for i in range(data.shape[-1])]`"
" to convert the input data of shape"
f" {data.shape} to a compatible list")

return tuple(data[field] for field in data.dtype.names)
try:
n_elements = len(data)
except TypeError:
raise ValueError("MultiNorm expects a sequence with one element per "
f"component as input, but got {data!r} instead")
if n_elements != n_components:
if isinstance(data, np.ndarray) and data.shape[-1] == n_components:
if len(data.shape) == 2:
raise ValueError(
f"MultiNorm expects a sequence with one element per component. "
"You can use `data_transposed = data.T`"
"to convert the input data of shape "
f"{data.shape} to a compatible shape {data.shape[::-1]} ")
Comment thread
trygvrad marked this conversation as resolved.
Resolved
else:
# Cannot give shape hint
# Either neither first nor last axis matches, or both do.
raise ValueError(f"{MultiNorm._get_input_err(n_components)}"
f". An np.ndarray of shape {data.shape} is"
" not compatible")
elif isinstance(data, (tuple, list)):
if len(data) != n_components:
raise ValueError(f"{MultiNorm._get_input_err(n_components)}"
f". A {type(data)} of length {len(data)} is"
" not compatible")
else:
raise ValueError(f"{MultiNorm._get_input_err(n_components)}"
f". Input of type {type(data)} is not supported")
raise ValueError(
f"MultiNorm expects a sequence with one element per component. "
"You can use `data_as_list = [data[..., i] for i in "
"range(data.shape[-1])]` to convert the input data of shape "
f" {data.shape} to a compatible list")

return data
raise ValueError(
"MultiNorm expects a sequence with one element per component. "
f"This MultiNorm has {n_components} components, but got a sequence "
f"with {n_elements} elements"
)

return tuple(data[i] for i in range(n_elements))

@staticmethod
def _get_input_err(n_components):
# returns the start of the error message given when a
# MultiNorm receives incompatible input
return ("The input to this `MultiNorm` must be a list or tuple "
f"of length {n_components}, or be structured array "
f"with {n_components} fields")

@staticmethod
def _ensure_multicomponent_data(data, n_components):
Comment thread
QuLogic marked this conversation as resolved.
Resolved
Expand Down
31 changes: 13 additions & 18 deletions lib/matplotlib/tests/test_colors.py
Original file line number Diff line number Diff line change
Expand Up @@ -1989,6 +1989,13 @@ def test_mult_norm_call_types():
assert_array_almost_equal(mn(tuple(vals.T)),
list(target.T))

# np.arrays of shapes that are compatible
assert_array_almost_equal(mn(np.zeros(2)),
0.5*np.ones(2))
assert_array_almost_equal(mn(np.zeros((2, 3))),
0.5*np.ones((2, 3)))
assert_array_almost_equal(mn(np.zeros((2, 3, 4))),
0.5*np.ones((2, 3, 4)))

# test setting structured_output true/false:
# structured input, structured output
Expand Down Expand Up @@ -2031,20 +2038,14 @@ def test_mult_norm_call_types():

# test single int as input
with pytest.raises(ValueError,
match="Input of type <class 'int'> is not supported"):
match="component as input, but got 1 instead"):
mn(1)

# test list of incompatible size
with pytest.raises(ValueError,
match="A <class 'list'> of length 3 is not compatible"):
match="but got a sequence with 3 elements"):
mn([3, 2, 1])

# np.arrays of shapes that can be converted:
for data in [np.zeros(2), np.zeros((2,3)), np.zeros((2,3,3))]:
with pytest.raises(ValueError,
match=r"You can use `data_as_list = list\(data\)`"):
mn(data)

# last axis matches, len(data.shape) > 2
with pytest.raises(ValueError,
match=(r"`data_as_list = \[data\[..., i\] for i in "
Expand All @@ -2053,22 +2054,16 @@ def test_mult_norm_call_types():

# last axis matches, len(data.shape) == 2
with pytest.raises(ValueError,
match=r"You can use `data_as_list = list\(data.T\)`"):
match=r"You can use `data_transposed = data.T`to convert"):
mn(np.zeros((3, 2)))

# np.ndarray that can be converted, but unclear if first or last axis
for data in [np.zeros((2, 2)), np.zeros((2, 3, 2))]:
with pytest.raises(ValueError,
match="An np.ndarray of shape"):
mn(data)

# incompatible arrays where no relevant axis matches
for data in [np.zeros(3), np.zeros((3, 2, 3))]:
with pytest.raises(ValueError,
match=r"An np.ndarray of shape"):
match=r"but got a sequence with 3 elements"):
mn(data)

# test incompatible class
with pytest.raises(ValueError,
match="Input of type <class 'str'> is not supported"):
mn("An object of incompatible class")
match="but got <object object"):
mn(object())