diff --git a/doc/users/next_whats_new/colormap_bad_under_over.rst b/doc/users/next_whats_new/colormap_bad_under_over.rst new file mode 100644 index 000000000000..772d47e4123d --- /dev/null +++ b/doc/users/next_whats_new/colormap_bad_under_over.rst @@ -0,0 +1,23 @@ +Colormaps support giving colors for bad, under and over values on creation +-------------------------------------------------------------------------- + +Colormaps gained keyword arguments ``bad``, ``under``, and ``over`` to +specify these values on creation. Previously, these values would have to +be set afterwards using one of `~.Colormap.set_bad`, `~.Colormap.set_under`, +`~.Colormap.set_bad`, `~.Colormap.set_extremes`, `~.Colormap.with_extremes`. + +It is recommended to use the new functionality, e.g.:: + + cmap = ListedColormap(colors, bad="red", under="darkblue", over="purple") + +instead of:: + + cmap = ListedColormap(colors).with_extremes( + bad="red", under="darkblue", over="purple") + +or:: + + cmap = ListedColormap(colors) + cmap.set_bad("red") + cmap.set_under("darkblue") + cmap.set_over("purple") diff --git a/galleries/examples/specialty_plots/leftventricle_bullseye.py b/galleries/examples/specialty_plots/leftventricle_bullseye.py index 427f45fde34a..3ad02edbc630 100644 --- a/galleries/examples/specialty_plots/leftventricle_bullseye.py +++ b/galleries/examples/specialty_plots/leftventricle_bullseye.py @@ -124,8 +124,7 @@ def bullseye_plot(ax, data, seg_bold=None, cmap="viridis", norm=None): # The second example illustrates the use of a ListedColormap, a # BoundaryNorm, and extended ends to show the "over" and "under" # value colors. -cmap3 = (mpl.colors.ListedColormap(['r', 'g', 'b', 'c']) - .with_extremes(over='0.35', under='0.75')) +cmap3 = mpl.colors.ListedColormap(['r', 'g', 'b', 'c'], over='0.35', under='0.75') # If a ListedColormap is used, the length of the bounds array must be # one greater than the length of the color list. The bounds must be # monotonically increasing. diff --git a/galleries/users_explain/colors/colorbar_only.py b/galleries/users_explain/colors/colorbar_only.py index 1a8988d4b7c9..a3f1d62042f4 100644 --- a/galleries/users_explain/colors/colorbar_only.py +++ b/galleries/users_explain/colors/colorbar_only.py @@ -75,8 +75,7 @@ # The following example still uses a `.BoundaryNorm` to describe discrete # interval boundaries, but now uses a `matplotlib.colors.ListedColormap` to # associate each interval with an arbitrary color (there must be as many -# intervals than there are colors). The "over" and "under" colors are set on -# the colormap using `.Colormap.with_extremes`. +# intervals than there are colors). # # We also pass additional arguments to `~.Figure.colorbar`: # @@ -90,8 +89,8 @@ fig, ax = plt.subplots(figsize=(6, 1), layout='constrained') -cmap = (mpl.colors.ListedColormap(['red', 'green', 'blue', 'cyan']) - .with_extremes(under='yellow', over='magenta')) +cmap = mpl.colors.ListedColormap( + ['red', 'green', 'blue', 'cyan'], under='yellow', over='magenta') bounds = [1, 2, 4, 7, 8] norm = mpl.colors.BoundaryNorm(bounds, cmap.N) @@ -112,8 +111,8 @@ fig, ax = plt.subplots(figsize=(6, 1), layout='constrained') -cmap = (mpl.colors.ListedColormap(['royalblue', 'cyan', 'yellow', 'orange']) - .with_extremes(over='red', under='blue')) +cmap = mpl.colors.ListedColormap( + ['royalblue', 'cyan', 'yellow', 'orange'], over='red', under='blue') bounds = [-1.0, -0.5, 0.0, 0.5, 1.0] norm = mpl.colors.BoundaryNorm(bounds, cmap.N) diff --git a/lib/matplotlib/colors.py b/lib/matplotlib/colors.py index 0462c733566a..d8954c8e50db 100644 --- a/lib/matplotlib/colors.py +++ b/lib/matplotlib/colors.py @@ -718,7 +718,7 @@ class Colormap: chain. """ - def __init__(self, name, N=256): + def __init__(self, name, N=256, *, bad=None, under=None, over=None): """ Parameters ---------- @@ -726,12 +726,26 @@ def __init__(self, name, N=256): The name of the colormap. N : int The number of RGB quantization levels. + bad : :mpltype:`color`, default: transparent + The color for invalid values (NaN or masked). + + .. versionadded:: 3.11 + + under : :mpltype:`color`, default: color of the lowest value + The color for low out-of-range values. + + .. versionadded:: 3.11 + + over : :mpltype:`color`, default: color of the highest value + The color for high out-of-range values. + + .. versionadded:: 3.11 """ self.name = name self.N = int(N) # ensure that N is always int - self._rgba_bad = (0.0, 0.0, 0.0, 0.0) # If bad, don't paint anything. - self._rgba_under = None - self._rgba_over = None + self._rgba_bad = (0.0, 0.0, 0.0, 0.0) if bad is None else to_rgba(bad) + self._rgba_under = None if under is None else to_rgba(under) + self._rgba_over = None if over is None else to_rgba(over) self._i_under = self.N self._i_over = self.N + 1 self._i_bad = self.N + 2 @@ -1038,43 +1052,69 @@ class LinearSegmentedColormap(Colormap): segments. """ - def __init__(self, name, segmentdata, N=256, gamma=1.0): + def __init__(self, name, segmentdata, N=256, gamma=1.0, *, + bad=None, under=None, over=None): """ - Create colormap from linear mapping segments + Create colormap from linear mapping segments. - segmentdata argument is a dictionary with a red, green and blue - entries. Each entry should be a list of *x*, *y0*, *y1* tuples, - forming rows in a table. Entries for alpha are optional. + Parameters + ---------- + name : str + The name of the colormap. + segmentdata : dict + A dictionary with keys "red", "green", "blue" for the color channels. + Each entry should be a list of *x*, *y0*, *y1* tuples, forming rows + in a table. Entries for alpha are optional. + + Example: suppose you want red to increase from 0 to 1 over + the bottom half, green to do the same over the middle half, + and blue over the top half. Then you would use:: + + { + 'red': [(0.0, 0.0, 0.0), + (0.5, 1.0, 1.0), + (1.0, 1.0, 1.0)], + 'green': [(0.0, 0.0, 0.0), + (0.25, 0.0, 0.0), + (0.75, 1.0, 1.0), + (1.0, 1.0, 1.0)], + 'blue': [(0.0, 0.0, 0.0), + (0.5, 0.0, 0.0), + (1.0, 1.0, 1.0)] + } - Example: suppose you want red to increase from 0 to 1 over - the bottom half, green to do the same over the middle half, - and blue over the top half. Then you would use:: + Each row in the table for a given color is a sequence of + *x*, *y0*, *y1* tuples. In each sequence, *x* must increase + monotonically from 0 to 1. For any input value *z* falling + between *x[i]* and *x[i+1]*, the output value of a given color + will be linearly interpolated between *y1[i]* and *y0[i+1]*:: - cdict = {'red': [(0.0, 0.0, 0.0), - (0.5, 1.0, 1.0), - (1.0, 1.0, 1.0)], + row i: x y0 y1 + / + / + row i+1: x y0 y1 - 'green': [(0.0, 0.0, 0.0), - (0.25, 0.0, 0.0), - (0.75, 1.0, 1.0), - (1.0, 1.0, 1.0)], + Hence, y0 in the first row and y1 in the last row are never used. - 'blue': [(0.0, 0.0, 0.0), - (0.5, 0.0, 0.0), - (1.0, 1.0, 1.0)]} + N : int + The number of RGB quantization levels. + gamma : float + Gamma correction factor for input distribution x of the mapping. + See also https://en.wikipedia.org/wiki/Gamma_correction. + bad : :mpltype:`color`, default: transparent + The color for invalid values (NaN or masked). + + .. versionadded:: 3.11 + + under : :mpltype:`color`, default: color of the lowest value + The color for low out-of-range values. - Each row in the table for a given color is a sequence of - *x*, *y0*, *y1* tuples. In each sequence, *x* must increase - monotonically from 0 to 1. For any input value *z* falling - between *x[i]* and *x[i+1]*, the output value of a given color - will be linearly interpolated between *y1[i]* and *y0[i+1]*:: + .. versionadded:: 3.11 - row i: x y0 y1 - / - / - row i+1: x y0 y1 + over : :mpltype:`color`, default: color of the highest value + The color for high out-of-range values. - Hence y0 in the first row and y1 in the last row are never used. + .. versionadded:: 3.11 See Also -------- @@ -1084,7 +1124,7 @@ def __init__(self, name, segmentdata, N=256, gamma=1.0): """ # True only if all colors in map are identical; needed for contouring. self.monochrome = False - super().__init__(name, N) + super().__init__(name, N, bad=bad, under=under, over=over) self._segmentdata = segmentdata self._gamma = gamma @@ -1108,7 +1148,7 @@ def set_gamma(self, gamma): self._init() @staticmethod - def from_list(name, colors, N=256, gamma=1.0): + def from_list(name, colors, N=256, gamma=1.0, *, bad=None, under=None, over=None): """ Create a `LinearSegmentedColormap` from a list of colors. @@ -1125,6 +1165,13 @@ def from_list(name, colors, N=256, gamma=1.0): N : int The number of RGB quantization levels. gamma : float + + bad : :mpltype:`color`, default: transparent + The color for invalid values (NaN or masked). + under : :mpltype:`color`, default: color of the lowest value + The color for low out-of-range values. + over : :mpltype:`color`, default: color of the highest value + The color for high out-of-range values. """ if not np.iterable(colors): raise ValueError('colors must be iterable') @@ -1144,7 +1191,8 @@ def from_list(name, colors, N=256, gamma=1.0): "alpha": np.column_stack([vals, a, a]), } - return LinearSegmentedColormap(name, cdict, N, gamma) + return LinearSegmentedColormap(name, cdict, N, gamma, + bad=bad, under=under, over=over) def resampled(self, lutsize): """Return a new colormap with *lutsize* entries.""" @@ -1219,6 +1267,26 @@ class ListedColormap(Colormap): N > len(colors) the list will be extended by repetition. + + .. deprecated:: 3.11 + + This parameter will be removed. Please instead ensure that + the list of passed colors is the required length. + + bad : :mpltype:`color`, default: transparent + The color for invalid values (NaN or masked). + + .. versionadded:: 3.11 + + under : :mpltype:`color`, default: color of the lowest value + The color for low out-of-range values. + + .. versionadded:: 3.11 + + over : :mpltype:`color`, default: color of the highest value + The color for high out-of-range values. + + .. versionadded:: 3.11 """ @_api.delete_parameter( @@ -1227,7 +1295,8 @@ class ListedColormap(Colormap): "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): + def __init__(self, colors, name='from_list', N=None, *, + bad=None, under=None, over=None): if N is None: self.colors = colors N = len(colors) @@ -1244,7 +1313,7 @@ def __init__(self, colors, name='from_list', N=None): pass else: self.colors = [gray] * N - super().__init__(name, N) + super().__init__(name, N, bad=bad, under=under, over=over) def _init(self): self._lut = np.zeros((self.N + 3, 4), float) @@ -3748,8 +3817,7 @@ def from_levels_and_colors(levels, colors, extend='neither'): data_colors = colors[color_slice] under_color = colors[0] if extend in ['min', 'both'] else 'none' over_color = colors[-1] if extend in ['max', 'both'] else 'none' - cmap = ListedColormap(data_colors).with_extremes( - under=under_color, over=over_color) + cmap = ListedColormap(data_colors, under=under_color, over=over_color) cmap.colorbar_extend = extend diff --git a/lib/matplotlib/colors.pyi b/lib/matplotlib/colors.pyi index 4ecf90164512..9fb758cb146c 100644 --- a/lib/matplotlib/colors.pyi +++ b/lib/matplotlib/colors.pyi @@ -68,7 +68,15 @@ class Colormap: name: str N: int colorbar_extend: bool - def __init__(self, name: str, N: int = ...) -> None: ... + def __init__( + self, + name: str, + N: int = ..., + *, + bad: ColorType | None = ..., + under: ColorType | None = ..., + over: ColorType | None = ... + ) -> None: ... @overload def __call__( self, X: Sequence[float] | np.ndarray, alpha: ArrayLike | None = ..., bytes: bool = ... @@ -120,11 +128,16 @@ class LinearSegmentedColormap(Colormap): ], N: int = ..., gamma: float = ..., + *, + bad: ColorType | None = ..., + under: ColorType | None = ..., + over: ColorType | None = ..., ) -> None: ... def set_gamma(self, gamma: float) -> None: ... @staticmethod def from_list( - name: str, colors: ArrayLike | Sequence[tuple[float, ColorType]], N: int = ..., gamma: float = ... + name: str, colors: ArrayLike | Sequence[tuple[float, ColorType]], N: int = ..., gamma: float = ..., + *, bad: ColorType | None = ..., under: ColorType | None = ..., over: ColorType | None = ..., ) -> LinearSegmentedColormap: ... def resampled(self, lutsize: int) -> LinearSegmentedColormap: ... def reversed(self, name: str | None = ...) -> LinearSegmentedColormap: ... @@ -132,7 +145,8 @@ class LinearSegmentedColormap(Colormap): class ListedColormap(Colormap): colors: ArrayLike | ColorType def __init__( - self, colors: ArrayLike | ColorType, name: str = ..., N: int | None = ... + self, colors: ArrayLike | ColorType, name: str = ..., N: int | None = ..., + *, bad: ColorType | None = ..., under: ColorType | None = ..., over: ColorType | None = ... ) -> None: ... @property def monochrome(self) -> bool: ... diff --git a/lib/matplotlib/tests/test_colors.py b/lib/matplotlib/tests/test_colors.py index 50e48f53c9e3..465d9d294a09 100644 --- a/lib/matplotlib/tests/test_colors.py +++ b/lib/matplotlib/tests/test_colors.py @@ -221,6 +221,33 @@ def test_colormap_return_types(): assert cmap(x2d).shape == x2d.shape + (4,) +def test_ListedColormap_bad_under_over(): + cmap = mcolors.ListedColormap(["r", "g", "b"], bad="c", under="m", over="y") + assert mcolors.same_color(cmap.get_bad(), "c") + assert mcolors.same_color(cmap.get_under(), "m") + assert mcolors.same_color(cmap.get_over(), "y") + + +def test_LinearSegmentedColormap_bad_under_over(): + cdict = { + 'red': [(0., 0., 0.), (0.5, 1., 1.), (1., 1., 1.)], + 'green': [(0., 0., 0.), (0.25, 0., 0.), (0.75, 1., 1.), (1., 1., 1.)], + 'blue': [(0., 0., 0.), (0.5, 0., 0.), (1., 1., 1.)], + } + cmap = mcolors.LinearSegmentedColormap("lsc", cdict, bad="c", under="m", over="y") + assert mcolors.same_color(cmap.get_bad(), "c") + assert mcolors.same_color(cmap.get_under(), "m") + assert mcolors.same_color(cmap.get_over(), "y") + + +def test_LinearSegmentedColormap_from_list_bad_under_over(): + cmap = mcolors.LinearSegmentedColormap.from_list( + "lsc", ["r", "g", "b"], bad="c", under="m", over="y") + assert mcolors.same_color(cmap.get_bad(), "c") + assert mcolors.same_color(cmap.get_under(), "m") + assert mcolors.same_color(cmap.get_over(), "y") + + def test_BoundaryNorm(): """ GitHub issue #1258: interpolation was failing with numpy