diff --git a/doc/api/next_api_changes/deprecations/29135-TH.rst b/doc/api/next_api_changes/deprecations/29135-TH.rst new file mode 100644 index 000000000000..e2289a248076 --- /dev/null +++ b/doc/api/next_api_changes/deprecations/29135-TH.rst @@ -0,0 +1,5 @@ +Parameter ``ListedColormap(..., N=...)`` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Passing the parameter *N* to `.ListedColormap` is deprecated. +Please preprocess the list colors yourself if needed. diff --git a/lib/matplotlib/cbook.py b/lib/matplotlib/cbook.py index 3c1593ea2e37..2b1d817257f4 100644 --- a/lib/matplotlib/cbook.py +++ b/lib/matplotlib/cbook.py @@ -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. diff --git a/lib/matplotlib/cbook.pyi b/lib/matplotlib/cbook.pyi index cc6b4e8f4e19..6c2d9c303eb2 100644 --- a/lib/matplotlib/cbook.pyi +++ b/lib/matplotlib/cbook.pyi @@ -14,6 +14,7 @@ from typing import ( Generic, IO, Literal, + Sequence, TypeVar, overload, ) @@ -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 = ..., diff --git a/lib/matplotlib/colors.py b/lib/matplotlib/colors.py index 9b7c331d31a8..ffecb305bb0f 100644 --- a/lib/matplotlib/colors.py +++ b/lib/matplotlib/colors.py @@ -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 @@ -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 @@ -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}") diff --git a/lib/matplotlib/contour.py b/lib/matplotlib/contour.py index 6b685fa0ed6a..0d384f32c8c0 100644 --- a/lib/matplotlib/contour.py +++ b/lib/matplotlib/contour.py @@ -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 = [] @@ -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: diff --git a/lib/matplotlib/tests/test_cbook.py b/lib/matplotlib/tests/test_cbook.py index a8faa9be3782..7cb057cf4723 100644 --- a/lib/matplotlib/tests/test_cbook.py +++ b/lib/matplotlib/tests/test_cbook.py @@ -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']}}), diff --git a/lib/matplotlib/tests/test_colors.py b/lib/matplotlib/tests/test_colors.py index 19c60091e608..87b21763d4ce 100644 --- a/lib/matplotlib/tests/test_colors.py +++ b/lib/matplotlib/tests/test_colors.py @@ -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)