Skip to content

Commit

Permalink
Deprecate ListedColormap(..., N=...) parameter (matplotlib#29135)
Browse files Browse the repository at this point in the history
* Deprecate ListedColormap(..., N=...) parameter

Truncating or repeating the given colors to a specific value of N
is not within the scope of colormaps. Instead, users should
create an appropriate color list themselves and feed that to
ListedColormap.

Also the current behavior can be surprising: It may well be that
a given N was intended to truncate, but depending on the list, it
could repeat. Repeated colors in a colormap are dangerous / often
not intentended because they create an ambiguity in the color ->
value mapping.

* Update lib/matplotlib/colors.py

Co-authored-by: Ruth Comer <[email protected]>

---------

Co-authored-by: Ruth Comer <[email protected]>
  • Loading branch information
timhoffm and rcomer authored Nov 20, 2024
1 parent 72301c7 commit 2fa9151
Show file tree
Hide file tree
Showing 7 changed files with 66 additions and 10 deletions.
5 changes: 5 additions & 0 deletions doc/api/next_api_changes/deprecations/29135-TH.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
Parameter ``ListedColormap(..., N=...)``
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

Passing the parameter *N* to `.ListedColormap` is deprecated.
Please preprocess the list colors yourself if needed.
20 changes: 20 additions & 0 deletions lib/matplotlib/cbook.py
Original file line number Diff line number Diff line change
Expand Up @@ -1739,6 +1739,26 @@ def sanitize_sequence(data):
else data)


def _resize_sequence(seq, N):
"""
Trim the given sequence to exactly N elements.
If there are more elements in the sequence, cut it.
If there are less elements in the sequence, repeat them.
Implementation detail: We maintain type stability for the output for
N <= len(seq). We simply return a list for N > len(seq); this was good
enough for the present use cases but is not a fixed design decision.
"""
num_elements = len(seq)
if N == num_elements:
return seq
elif N < num_elements:
return seq[:N]
else:
return list(itertools.islice(itertools.cycle(seq), N))


def normalize_kwargs(kw, alias_mapping=None):
"""
Helper function to normalize kwarg inputs.
Expand Down
2 changes: 2 additions & 0 deletions lib/matplotlib/cbook.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ from typing import (
Generic,
IO,
Literal,
Sequence,
TypeVar,
overload,
)
Expand Down Expand Up @@ -143,6 +144,7 @@ STEP_LOOKUP_MAP: dict[str, Callable]
def index_of(y: float | ArrayLike) -> tuple[np.ndarray, np.ndarray]: ...
def safe_first_element(obj: Collection[_T]) -> _T: ...
def sanitize_sequence(data): ...
def _resize_sequence(seq: Sequence, N: int) -> Sequence: ...
def normalize_kwargs(
kw: dict[str, Any],
alias_mapping: dict[str, list[str]] | type[Artist] | Artist | None = ...,
Expand Down
13 changes: 10 additions & 3 deletions lib/matplotlib/colors.py
Original file line number Diff line number Diff line change
Expand Up @@ -1190,6 +1190,13 @@ class ListedColormap(Colormap):
the list will be extended by repetition.
"""

@_api.delete_parameter(
"3.11", "N",
message="Passing 'N' to ListedColormap is deprecated since %(since)s "
"and will be removed in %(removal)s. Please ensure the list "
"of passed colors is the required length instead."
)
def __init__(self, colors, name='from_list', N=None):
if N is None:
self.colors = colors
Expand Down Expand Up @@ -1259,7 +1266,7 @@ def reversed(self, name=None):
name = self.name + "_r"

colors_r = list(reversed(self.colors))
new_cmap = ListedColormap(colors_r, name=name, N=self.N)
new_cmap = ListedColormap(colors_r, name=name)
# Reverse the over/under values too
new_cmap._rgba_over = self._rgba_under
new_cmap._rgba_under = self._rgba_over
Expand Down Expand Up @@ -1943,14 +1950,14 @@ def __getitem__(self, item):
if origin_1_as_int > self.M-1:
origin_1_as_int = self.M-1
one_d_lut = self._lut[:, origin_1_as_int]
new_cmap = ListedColormap(one_d_lut, name=f'{self.name}_0', N=self.N)
new_cmap = ListedColormap(one_d_lut, name=f'{self.name}_0')

elif item == 1:
origin_0_as_int = int(self._origin[0]*self.N)
if origin_0_as_int > self.N-1:
origin_0_as_int = self.N-1
one_d_lut = self._lut[origin_0_as_int, :]
new_cmap = ListedColormap(one_d_lut, name=f'{self.name}_1', N=self.M)
new_cmap = ListedColormap(one_d_lut, name=f'{self.name}_1')
else:
raise KeyError(f"only 0 or 1 are"
f" valid keys for BivarColormap, not {item!r}")
Expand Down
15 changes: 10 additions & 5 deletions lib/matplotlib/contour.py
Original file line number Diff line number Diff line change
Expand Up @@ -183,10 +183,14 @@ def clabel(self, levels=None, *,
self.labelMappable = self
self.labelCValueList = np.take(self.cvalues, self.labelIndiceList)
else:
cmap = mcolors.ListedColormap(colors, N=len(self.labelLevelList))
self.labelCValueList = list(range(len(self.labelLevelList)))
self.labelMappable = cm.ScalarMappable(cmap=cmap,
norm=mcolors.NoNorm())
# handling of explicit colors for labels:
# make labelCValueList contain integers [0, 1, 2, ...] and a cmap
# so that cmap(i) == colors[i]
num_levels = len(self.labelLevelList)
colors = cbook._resize_sequence(mcolors.to_rgba_array(colors), num_levels)
self.labelMappable = cm.ScalarMappable(
cmap=mcolors.ListedColormap(colors), norm=mcolors.NoNorm())
self.labelCValueList = list(range(num_levels))

self.labelXYs = []

Expand Down Expand Up @@ -738,7 +742,8 @@ def __init__(self, ax, *args,
if self._extend_min:
i0 = 1

cmap = mcolors.ListedColormap(color_sequence[i0:None], N=ncolors)
cmap = mcolors.ListedColormap(
cbook._resize_sequence(color_sequence[i0:], ncolors))

if use_set_under_over:
if self._extend_min:
Expand Down
17 changes: 17 additions & 0 deletions lib/matplotlib/tests/test_cbook.py
Original file line number Diff line number Diff line change
Expand Up @@ -463,6 +463,23 @@ def test_sanitize_sequence():
assert k == cbook.sanitize_sequence(k)


def test_resize_sequence():
a_list = [1, 2, 3]
arr = np.array([1, 2, 3])

# already same length: passthrough
assert cbook._resize_sequence(a_list, 3) is a_list
assert cbook._resize_sequence(arr, 3) is arr

# shortening
assert cbook._resize_sequence(a_list, 2) == [1, 2]
assert_array_equal(cbook._resize_sequence(arr, 2), [1, 2])

# extending
assert cbook._resize_sequence(a_list, 5) == [1, 2, 3, 1, 2]
assert_array_equal(cbook._resize_sequence(arr, 5), [1, 2, 3, 1, 2])


fail_mapping: tuple[tuple[dict, dict], ...] = (
({'a': 1, 'b': 2}, {'alias_mapping': {'a': ['b']}}),
({'a': 1, 'b': 2}, {'alias_mapping': {'a': ['a', 'b']}}),
Expand Down
4 changes: 2 additions & 2 deletions lib/matplotlib/tests/test_colors.py
Original file line number Diff line number Diff line change
Expand Up @@ -1165,8 +1165,8 @@ def test_pandas_iterable(pd):
# a single color
lst = ['red', 'blue', 'green']
s = pd.Series(lst)
cm1 = mcolors.ListedColormap(lst, N=5)
cm2 = mcolors.ListedColormap(s, N=5)
cm1 = mcolors.ListedColormap(lst)
cm2 = mcolors.ListedColormap(s)
assert_array_equal(cm1.colors, cm2.colors)


Expand Down

0 comments on commit 2fa9151

Please sign in to comment.