From 4a220f5cc071dcdddf5a4dae7349c6871171e36a Mon Sep 17 00:00:00 2001 From: Tim Hoffmann <2836374+timhoffm@users.noreply.github.com> Date: Tue, 12 Nov 2024 16:29:49 +0100 Subject: [PATCH 1/2] 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. --- .../deprecations/29135-TH.rst | 5 +++++ lib/matplotlib/cbook.py | 20 +++++++++++++++++++ lib/matplotlib/cbook.pyi | 2 ++ lib/matplotlib/colors.py | 13 +++++++++--- lib/matplotlib/contour.py | 15 +++++++++----- lib/matplotlib/tests/test_cbook.py | 17 ++++++++++++++++ lib/matplotlib/tests/test_colors.py | 4 ++-- 7 files changed, 66 insertions(+), 10 deletions(-) create mode 100644 doc/api/next_api_changes/deprecations/29135-TH.rst 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..f896266a05c5 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 process the list " + "of passed colors yourself if needed." + ) 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) From 359a6c826888f0afe3808800312551d62421bf74 Mon Sep 17 00:00:00 2001 From: Tim Hoffmann <2836374+timhoffm@users.noreply.github.com> Date: Sat, 16 Nov 2024 23:17:02 +0100 Subject: [PATCH 2/2] Update lib/matplotlib/colors.py Co-authored-by: Ruth Comer <10599679+rcomer@users.noreply.github.com> --- lib/matplotlib/colors.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/matplotlib/colors.py b/lib/matplotlib/colors.py index f896266a05c5..ffecb305bb0f 100644 --- a/lib/matplotlib/colors.py +++ b/lib/matplotlib/colors.py @@ -1194,8 +1194,8 @@ class ListedColormap(Colormap): @_api.delete_parameter( "3.11", "N", message="Passing 'N' to ListedColormap is deprecated since %(since)s " - "and will be removed in %(removal)s. Please process the list " - "of passed colors yourself if needed." + "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:
Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.
Alternative Proxies: