Skip to content

ENH: Add bad, under, over kwargs to Colormap #29460

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Jan 24, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions doc/users/next_whats_new/colormap_bad_under_over.rst
Original file line number Diff line number Diff line change
@@ -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")
3 changes: 1 addition & 2 deletions galleries/examples/specialty_plots/leftventricle_bullseye.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
11 changes: 5 additions & 6 deletions galleries/users_explain/colors/colorbar_only.py
Original file line number Diff line number Diff line change
Expand Up @@ -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`:
#
Expand All @@ -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)

Expand All @@ -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)

Expand Down
146 changes: 107 additions & 39 deletions lib/matplotlib/colors.py
Original file line number Diff line number Diff line change
Expand Up @@ -718,20 +718,34 @@ class Colormap:
chain.
"""

def __init__(self, name, N=256):
def __init__(self, name, N=256, *, bad=None, under=None, over=None):
"""
Parameters
----------
name : str
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).
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
The color for invalid values (NaN or masked).
The color for invalid values (NaN or masked).
.. versionadded:: 3.11

? (Likewise in many locations below. Hence, maybe it is not required...)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done. It's a bit verbose, but OTOH it's good to be explicit about this.


.. 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
Expand Down Expand Up @@ -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
--------
Expand All @@ -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

Expand All @@ -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.

Expand All @@ -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')
Expand All @@ -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."""
Expand Down Expand Up @@ -1219,6 +1267,26 @@ class ListedColormap(Colormap):
N > len(colors)

the list will be extended by repetition.

.. deprecated:: 3.11
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This seems orthogonal to this PR? Or is this already deprecated elsewhere and this is just a doc cleanup? I'm not really following why we would need to do this.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The deprecation is in #29135, it's indeed unrelated. I just noticed that I haven't added this to the parameter docs, and didn't bother to make a separate PR since I was editing that position in the docstring anyway. Sorry for the confusion.


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(
Expand All @@ -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)
Expand All @@ -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)
Expand Down Expand Up @@ -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

Expand Down
20 changes: 17 additions & 3 deletions lib/matplotlib/colors.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -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 = ...
Expand Down Expand Up @@ -120,19 +128,25 @@ 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: ...

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: ...
Expand Down
27 changes: 27 additions & 0 deletions lib/matplotlib/tests/test_colors.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading