Skip to content

Commit

Permalink
ENH: Add bad, under, over kwargs to Colormap
Browse files Browse the repository at this point in the history
This is part of the effort for making Colormaps immutable (matplotlib#29141).
Obviously, we can only get immutable in the future if the
bad, under, over colors can already be set upon creation.
  • Loading branch information
timhoffm committed Jan 24, 2025
1 parent 3fb9c09 commit e1adce7
Show file tree
Hide file tree
Showing 6 changed files with 180 additions and 50 deletions.
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).
.. 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
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

0 comments on commit e1adce7

Please sign in to comment.