Skip to content

Commit 2fa9151

Browse files
timhoffmrcomer
andauthored
Deprecate ListedColormap(..., N=...) parameter (#29135)
* 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 <10599679+rcomer@users.noreply.github.com> --------- Co-authored-by: Ruth Comer <10599679+rcomer@users.noreply.github.com>
1 parent 72301c7 commit 2fa9151

File tree

7 files changed

+66
-10
lines changed

7 files changed

+66
-10
lines changed
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
Parameter ``ListedColormap(..., N=...)``
2+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
3+
4+
Passing the parameter *N* to `.ListedColormap` is deprecated.
5+
Please preprocess the list colors yourself if needed.

lib/matplotlib/cbook.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1739,6 +1739,26 @@ def sanitize_sequence(data):
17391739
else data)
17401740

17411741

1742+
def _resize_sequence(seq, N):
1743+
"""
1744+
Trim the given sequence to exactly N elements.
1745+
1746+
If there are more elements in the sequence, cut it.
1747+
If there are less elements in the sequence, repeat them.
1748+
1749+
Implementation detail: We maintain type stability for the output for
1750+
N <= len(seq). We simply return a list for N > len(seq); this was good
1751+
enough for the present use cases but is not a fixed design decision.
1752+
"""
1753+
num_elements = len(seq)
1754+
if N == num_elements:
1755+
return seq
1756+
elif N < num_elements:
1757+
return seq[:N]
1758+
else:
1759+
return list(itertools.islice(itertools.cycle(seq), N))
1760+
1761+
17421762
def normalize_kwargs(kw, alias_mapping=None):
17431763
"""
17441764
Helper function to normalize kwarg inputs.

lib/matplotlib/cbook.pyi

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ from typing import (
1414
Generic,
1515
IO,
1616
Literal,
17+
Sequence,
1718
TypeVar,
1819
overload,
1920
)
@@ -143,6 +144,7 @@ STEP_LOOKUP_MAP: dict[str, Callable]
143144
def index_of(y: float | ArrayLike) -> tuple[np.ndarray, np.ndarray]: ...
144145
def safe_first_element(obj: Collection[_T]) -> _T: ...
145146
def sanitize_sequence(data): ...
147+
def _resize_sequence(seq: Sequence, N: int) -> Sequence: ...
146148
def normalize_kwargs(
147149
kw: dict[str, Any],
148150
alias_mapping: dict[str, list[str]] | type[Artist] | Artist | None = ...,

lib/matplotlib/colors.py

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1190,6 +1190,13 @@ class ListedColormap(Colormap):
11901190
11911191
the list will be extended by repetition.
11921192
"""
1193+
1194+
@_api.delete_parameter(
1195+
"3.11", "N",
1196+
message="Passing 'N' to ListedColormap is deprecated since %(since)s "
1197+
"and will be removed in %(removal)s. Please ensure the list "
1198+
"of passed colors is the required length instead."
1199+
)
11931200
def __init__(self, colors, name='from_list', N=None):
11941201
if N is None:
11951202
self.colors = colors
@@ -1259,7 +1266,7 @@ def reversed(self, name=None):
12591266
name = self.name + "_r"
12601267

12611268
colors_r = list(reversed(self.colors))
1262-
new_cmap = ListedColormap(colors_r, name=name, N=self.N)
1269+
new_cmap = ListedColormap(colors_r, name=name)
12631270
# Reverse the over/under values too
12641271
new_cmap._rgba_over = self._rgba_under
12651272
new_cmap._rgba_under = self._rgba_over
@@ -1943,14 +1950,14 @@ def __getitem__(self, item):
19431950
if origin_1_as_int > self.M-1:
19441951
origin_1_as_int = self.M-1
19451952
one_d_lut = self._lut[:, origin_1_as_int]
1946-
new_cmap = ListedColormap(one_d_lut, name=f'{self.name}_0', N=self.N)
1953+
new_cmap = ListedColormap(one_d_lut, name=f'{self.name}_0')
19471954

19481955
elif item == 1:
19491956
origin_0_as_int = int(self._origin[0]*self.N)
19501957
if origin_0_as_int > self.N-1:
19511958
origin_0_as_int = self.N-1
19521959
one_d_lut = self._lut[origin_0_as_int, :]
1953-
new_cmap = ListedColormap(one_d_lut, name=f'{self.name}_1', N=self.M)
1960+
new_cmap = ListedColormap(one_d_lut, name=f'{self.name}_1')
19541961
else:
19551962
raise KeyError(f"only 0 or 1 are"
19561963
f" valid keys for BivarColormap, not {item!r}")

lib/matplotlib/contour.py

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -183,10 +183,14 @@ def clabel(self, levels=None, *,
183183
self.labelMappable = self
184184
self.labelCValueList = np.take(self.cvalues, self.labelIndiceList)
185185
else:
186-
cmap = mcolors.ListedColormap(colors, N=len(self.labelLevelList))
187-
self.labelCValueList = list(range(len(self.labelLevelList)))
188-
self.labelMappable = cm.ScalarMappable(cmap=cmap,
189-
norm=mcolors.NoNorm())
186+
# handling of explicit colors for labels:
187+
# make labelCValueList contain integers [0, 1, 2, ...] and a cmap
188+
# so that cmap(i) == colors[i]
189+
num_levels = len(self.labelLevelList)
190+
colors = cbook._resize_sequence(mcolors.to_rgba_array(colors), num_levels)
191+
self.labelMappable = cm.ScalarMappable(
192+
cmap=mcolors.ListedColormap(colors), norm=mcolors.NoNorm())
193+
self.labelCValueList = list(range(num_levels))
190194

191195
self.labelXYs = []
192196

@@ -738,7 +742,8 @@ def __init__(self, ax, *args,
738742
if self._extend_min:
739743
i0 = 1
740744

741-
cmap = mcolors.ListedColormap(color_sequence[i0:None], N=ncolors)
745+
cmap = mcolors.ListedColormap(
746+
cbook._resize_sequence(color_sequence[i0:], ncolors))
742747

743748
if use_set_under_over:
744749
if self._extend_min:

lib/matplotlib/tests/test_cbook.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -463,6 +463,23 @@ def test_sanitize_sequence():
463463
assert k == cbook.sanitize_sequence(k)
464464

465465

466+
def test_resize_sequence():
467+
a_list = [1, 2, 3]
468+
arr = np.array([1, 2, 3])
469+
470+
# already same length: passthrough
471+
assert cbook._resize_sequence(a_list, 3) is a_list
472+
assert cbook._resize_sequence(arr, 3) is arr
473+
474+
# shortening
475+
assert cbook._resize_sequence(a_list, 2) == [1, 2]
476+
assert_array_equal(cbook._resize_sequence(arr, 2), [1, 2])
477+
478+
# extending
479+
assert cbook._resize_sequence(a_list, 5) == [1, 2, 3, 1, 2]
480+
assert_array_equal(cbook._resize_sequence(arr, 5), [1, 2, 3, 1, 2])
481+
482+
466483
fail_mapping: tuple[tuple[dict, dict], ...] = (
467484
({'a': 1, 'b': 2}, {'alias_mapping': {'a': ['b']}}),
468485
({'a': 1, 'b': 2}, {'alias_mapping': {'a': ['a', 'b']}}),

lib/matplotlib/tests/test_colors.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1165,8 +1165,8 @@ def test_pandas_iterable(pd):
11651165
# a single color
11661166
lst = ['red', 'blue', 'green']
11671167
s = pd.Series(lst)
1168-
cm1 = mcolors.ListedColormap(lst, N=5)
1169-
cm2 = mcolors.ListedColormap(s, N=5)
1168+
cm1 = mcolors.ListedColormap(lst)
1169+
cm2 = mcolors.ListedColormap(s)
11701170
assert_array_equal(cm1.colors, cm2.colors)
11711171

11721172

0 commit comments

Comments
 (0)