Skip to content

Commit e1adce7

Browse files
committed
ENH: Add bad, under, over kwargs to Colormap
This is part of the effort for making Colormaps immutable (#29141). Obviously, we can only get immutable in the future if the bad, under, over colors can already be set upon creation.
1 parent 3fb9c09 commit e1adce7

File tree

6 files changed

+180
-50
lines changed

6 files changed

+180
-50
lines changed
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
Colormaps support giving colors for bad, under and over values on creation
2+
--------------------------------------------------------------------------
3+
4+
Colormaps gained keyword arguments ``bad``, ``under``, and ``over`` to
5+
specify these values on creation. Previously, these values would have to
6+
be set afterwards using one of `~.Colormap.set_bad`, `~.Colormap.set_under`,
7+
`~.Colormap.set_bad`, `~.Colormap.set_extremes`, `~.Colormap.with_extremes`.
8+
9+
It is recommended to use the new functionality, e.g.::
10+
11+
cmap = ListedColormap(colors, bad="red", under="darkblue", over="purple")
12+
13+
instead of::
14+
15+
cmap = ListedColormap(colors).with_extremes(
16+
bad="red", under="darkblue", over="purple")
17+
18+
or::
19+
20+
cmap = ListedColormap(colors)
21+
cmap.set_bad("red")
22+
cmap.set_under("darkblue")
23+
cmap.set_over("purple")

galleries/examples/specialty_plots/leftventricle_bullseye.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -124,8 +124,7 @@ def bullseye_plot(ax, data, seg_bold=None, cmap="viridis", norm=None):
124124
# The second example illustrates the use of a ListedColormap, a
125125
# BoundaryNorm, and extended ends to show the "over" and "under"
126126
# value colors.
127-
cmap3 = (mpl.colors.ListedColormap(['r', 'g', 'b', 'c'])
128-
.with_extremes(over='0.35', under='0.75'))
127+
cmap3 = mpl.colors.ListedColormap(['r', 'g', 'b', 'c'], over='0.35', under='0.75')
129128
# If a ListedColormap is used, the length of the bounds array must be
130129
# one greater than the length of the color list. The bounds must be
131130
# monotonically increasing.

galleries/users_explain/colors/colorbar_only.py

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -75,8 +75,7 @@
7575
# The following example still uses a `.BoundaryNorm` to describe discrete
7676
# interval boundaries, but now uses a `matplotlib.colors.ListedColormap` to
7777
# associate each interval with an arbitrary color (there must be as many
78-
# intervals than there are colors). The "over" and "under" colors are set on
79-
# the colormap using `.Colormap.with_extremes`.
78+
# intervals than there are colors).
8079
#
8180
# We also pass additional arguments to `~.Figure.colorbar`:
8281
#
@@ -90,8 +89,8 @@
9089

9190
fig, ax = plt.subplots(figsize=(6, 1), layout='constrained')
9291

93-
cmap = (mpl.colors.ListedColormap(['red', 'green', 'blue', 'cyan'])
94-
.with_extremes(under='yellow', over='magenta'))
92+
cmap = mpl.colors.ListedColormap(
93+
['red', 'green', 'blue', 'cyan'], under='yellow', over='magenta')
9594
bounds = [1, 2, 4, 7, 8]
9695
norm = mpl.colors.BoundaryNorm(bounds, cmap.N)
9796

@@ -112,8 +111,8 @@
112111

113112
fig, ax = plt.subplots(figsize=(6, 1), layout='constrained')
114113

115-
cmap = (mpl.colors.ListedColormap(['royalblue', 'cyan', 'yellow', 'orange'])
116-
.with_extremes(over='red', under='blue'))
114+
cmap = mpl.colors.ListedColormap(
115+
['royalblue', 'cyan', 'yellow', 'orange'], over='red', under='blue')
117116
bounds = [-1.0, -0.5, 0.0, 0.5, 1.0]
118117
norm = mpl.colors.BoundaryNorm(bounds, cmap.N)
119118

lib/matplotlib/colors.py

Lines changed: 107 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -718,20 +718,34 @@ class Colormap:
718718
chain.
719719
"""
720720

721-
def __init__(self, name, N=256):
721+
def __init__(self, name, N=256, *, bad=None, under=None, over=None):
722722
"""
723723
Parameters
724724
----------
725725
name : str
726726
The name of the colormap.
727727
N : int
728728
The number of RGB quantization levels.
729+
bad : :mpltype:`color`, default: transparent
730+
The color for invalid values (NaN or masked).
731+
732+
.. versionadded:: 3.11
733+
734+
under : :mpltype:`color`, default: color of the lowest value
735+
The color for low out-of-range values.
736+
737+
.. versionadded:: 3.11
738+
739+
over : :mpltype:`color`, default: color of the highest value
740+
The color for high out-of-range values.
741+
742+
.. versionadded:: 3.11
729743
"""
730744
self.name = name
731745
self.N = int(N) # ensure that N is always int
732-
self._rgba_bad = (0.0, 0.0, 0.0, 0.0) # If bad, don't paint anything.
733-
self._rgba_under = None
734-
self._rgba_over = None
746+
self._rgba_bad = (0.0, 0.0, 0.0, 0.0) if bad is None else to_rgba(bad)
747+
self._rgba_under = None if under is None else to_rgba(under)
748+
self._rgba_over = None if over is None else to_rgba(over)
735749
self._i_under = self.N
736750
self._i_over = self.N + 1
737751
self._i_bad = self.N + 2
@@ -1038,43 +1052,69 @@ class LinearSegmentedColormap(Colormap):
10381052
segments.
10391053
"""
10401054

1041-
def __init__(self, name, segmentdata, N=256, gamma=1.0):
1055+
def __init__(self, name, segmentdata, N=256, gamma=1.0, *,
1056+
bad=None, under=None, over=None):
10421057
"""
1043-
Create colormap from linear mapping segments
1058+
Create colormap from linear mapping segments.
10441059
1045-
segmentdata argument is a dictionary with a red, green and blue
1046-
entries. Each entry should be a list of *x*, *y0*, *y1* tuples,
1047-
forming rows in a table. Entries for alpha are optional.
1060+
Parameters
1061+
----------
1062+
name : str
1063+
The name of the colormap.
1064+
segmentdata : dict
1065+
A dictionary with keys "red", "green", "blue" for the color channels.
1066+
Each entry should be a list of *x*, *y0*, *y1* tuples, forming rows
1067+
in a table. Entries for alpha are optional.
1068+
1069+
Example: suppose you want red to increase from 0 to 1 over
1070+
the bottom half, green to do the same over the middle half,
1071+
and blue over the top half. Then you would use::
1072+
1073+
{
1074+
'red': [(0.0, 0.0, 0.0),
1075+
(0.5, 1.0, 1.0),
1076+
(1.0, 1.0, 1.0)],
1077+
'green': [(0.0, 0.0, 0.0),
1078+
(0.25, 0.0, 0.0),
1079+
(0.75, 1.0, 1.0),
1080+
(1.0, 1.0, 1.0)],
1081+
'blue': [(0.0, 0.0, 0.0),
1082+
(0.5, 0.0, 0.0),
1083+
(1.0, 1.0, 1.0)]
1084+
}
10481085
1049-
Example: suppose you want red to increase from 0 to 1 over
1050-
the bottom half, green to do the same over the middle half,
1051-
and blue over the top half. Then you would use::
1086+
Each row in the table for a given color is a sequence of
1087+
*x*, *y0*, *y1* tuples. In each sequence, *x* must increase
1088+
monotonically from 0 to 1. For any input value *z* falling
1089+
between *x[i]* and *x[i+1]*, the output value of a given color
1090+
will be linearly interpolated between *y1[i]* and *y0[i+1]*::
10521091
1053-
cdict = {'red': [(0.0, 0.0, 0.0),
1054-
(0.5, 1.0, 1.0),
1055-
(1.0, 1.0, 1.0)],
1092+
row i: x y0 y1
1093+
/
1094+
/
1095+
row i+1: x y0 y1
10561096
1057-
'green': [(0.0, 0.0, 0.0),
1058-
(0.25, 0.0, 0.0),
1059-
(0.75, 1.0, 1.0),
1060-
(1.0, 1.0, 1.0)],
1097+
Hence, y0 in the first row and y1 in the last row are never used.
10611098
1062-
'blue': [(0.0, 0.0, 0.0),
1063-
(0.5, 0.0, 0.0),
1064-
(1.0, 1.0, 1.0)]}
1099+
N : int
1100+
The number of RGB quantization levels.
1101+
gamma : float
1102+
Gamma correction factor for input distribution x of the mapping.
1103+
See also https://en.wikipedia.org/wiki/Gamma_correction.
1104+
bad : :mpltype:`color`, default: transparent
1105+
The color for invalid values (NaN or masked).
1106+
1107+
.. versionadded:: 3.11
1108+
1109+
under : :mpltype:`color`, default: color of the lowest value
1110+
The color for low out-of-range values.
10651111
1066-
Each row in the table for a given color is a sequence of
1067-
*x*, *y0*, *y1* tuples. In each sequence, *x* must increase
1068-
monotonically from 0 to 1. For any input value *z* falling
1069-
between *x[i]* and *x[i+1]*, the output value of a given color
1070-
will be linearly interpolated between *y1[i]* and *y0[i+1]*::
1112+
.. versionadded:: 3.11
10711113
1072-
row i: x y0 y1
1073-
/
1074-
/
1075-
row i+1: x y0 y1
1114+
over : :mpltype:`color`, default: color of the highest value
1115+
The color for high out-of-range values.
10761116
1077-
Hence y0 in the first row and y1 in the last row are never used.
1117+
.. versionadded:: 3.11
10781118
10791119
See Also
10801120
--------
@@ -1084,7 +1124,7 @@ def __init__(self, name, segmentdata, N=256, gamma=1.0):
10841124
"""
10851125
# True only if all colors in map are identical; needed for contouring.
10861126
self.monochrome = False
1087-
super().__init__(name, N)
1127+
super().__init__(name, N, bad=bad, under=under, over=over)
10881128
self._segmentdata = segmentdata
10891129
self._gamma = gamma
10901130

@@ -1108,7 +1148,7 @@ def set_gamma(self, gamma):
11081148
self._init()
11091149

11101150
@staticmethod
1111-
def from_list(name, colors, N=256, gamma=1.0):
1151+
def from_list(name, colors, N=256, gamma=1.0, *, bad=None, under=None, over=None):
11121152
"""
11131153
Create a `LinearSegmentedColormap` from a list of colors.
11141154
@@ -1125,6 +1165,13 @@ def from_list(name, colors, N=256, gamma=1.0):
11251165
N : int
11261166
The number of RGB quantization levels.
11271167
gamma : float
1168+
1169+
bad : :mpltype:`color`, default: transparent
1170+
The color for invalid values (NaN or masked).
1171+
under : :mpltype:`color`, default: color of the lowest value
1172+
The color for low out-of-range values.
1173+
over : :mpltype:`color`, default: color of the highest value
1174+
The color for high out-of-range values.
11281175
"""
11291176
if not np.iterable(colors):
11301177
raise ValueError('colors must be iterable')
@@ -1144,7 +1191,8 @@ def from_list(name, colors, N=256, gamma=1.0):
11441191
"alpha": np.column_stack([vals, a, a]),
11451192
}
11461193

1147-
return LinearSegmentedColormap(name, cdict, N, gamma)
1194+
return LinearSegmentedColormap(name, cdict, N, gamma,
1195+
bad=bad, under=under, over=over)
11481196

11491197
def resampled(self, lutsize):
11501198
"""Return a new colormap with *lutsize* entries."""
@@ -1219,6 +1267,26 @@ class ListedColormap(Colormap):
12191267
N > len(colors)
12201268
12211269
the list will be extended by repetition.
1270+
1271+
.. deprecated:: 3.11
1272+
1273+
This parameter will be removed. Please instead ensure that
1274+
the list of passed colors is the required length.
1275+
1276+
bad : :mpltype:`color`, default: transparent
1277+
The color for invalid values (NaN or masked).
1278+
1279+
.. versionadded:: 3.11
1280+
1281+
under : :mpltype:`color`, default: color of the lowest value
1282+
The color for low out-of-range values.
1283+
1284+
.. versionadded:: 3.11
1285+
1286+
over : :mpltype:`color`, default: color of the highest value
1287+
The color for high out-of-range values.
1288+
1289+
.. versionadded:: 3.11
12221290
"""
12231291

12241292
@_api.delete_parameter(
@@ -1227,7 +1295,8 @@ class ListedColormap(Colormap):
12271295
"and will be removed in %(removal)s. Please ensure the list "
12281296
"of passed colors is the required length instead."
12291297
)
1230-
def __init__(self, colors, name='from_list', N=None):
1298+
def __init__(self, colors, name='from_list', N=None, *,
1299+
bad=None, under=None, over=None):
12311300
if N is None:
12321301
self.colors = colors
12331302
N = len(colors)
@@ -1244,7 +1313,7 @@ def __init__(self, colors, name='from_list', N=None):
12441313
pass
12451314
else:
12461315
self.colors = [gray] * N
1247-
super().__init__(name, N)
1316+
super().__init__(name, N, bad=bad, under=under, over=over)
12481317

12491318
def _init(self):
12501319
self._lut = np.zeros((self.N + 3, 4), float)
@@ -3748,8 +3817,7 @@ def from_levels_and_colors(levels, colors, extend='neither'):
37483817
data_colors = colors[color_slice]
37493818
under_color = colors[0] if extend in ['min', 'both'] else 'none'
37503819
over_color = colors[-1] if extend in ['max', 'both'] else 'none'
3751-
cmap = ListedColormap(data_colors).with_extremes(
3752-
under=under_color, over=over_color)
3820+
cmap = ListedColormap(data_colors, under=under_color, over=over_color)
37533821

37543822
cmap.colorbar_extend = extend
37553823

lib/matplotlib/colors.pyi

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,15 @@ class Colormap:
6868
name: str
6969
N: int
7070
colorbar_extend: bool
71-
def __init__(self, name: str, N: int = ...) -> None: ...
71+
def __init__(
72+
self,
73+
name: str,
74+
N: int = ...,
75+
*,
76+
bad: ColorType | None = ...,
77+
under: ColorType | None = ...,
78+
over: ColorType | None = ...
79+
) -> None: ...
7280
@overload
7381
def __call__(
7482
self, X: Sequence[float] | np.ndarray, alpha: ArrayLike | None = ..., bytes: bool = ...
@@ -120,19 +128,25 @@ class LinearSegmentedColormap(Colormap):
120128
],
121129
N: int = ...,
122130
gamma: float = ...,
131+
*,
132+
bad: ColorType | None = ...,
133+
under: ColorType | None = ...,
134+
over: ColorType | None = ...,
123135
) -> None: ...
124136
def set_gamma(self, gamma: float) -> None: ...
125137
@staticmethod
126138
def from_list(
127-
name: str, colors: ArrayLike | Sequence[tuple[float, ColorType]], N: int = ..., gamma: float = ...
139+
name: str, colors: ArrayLike | Sequence[tuple[float, ColorType]], N: int = ..., gamma: float = ...,
140+
*, bad: ColorType | None = ..., under: ColorType | None = ..., over: ColorType | None = ...,
128141
) -> LinearSegmentedColormap: ...
129142
def resampled(self, lutsize: int) -> LinearSegmentedColormap: ...
130143
def reversed(self, name: str | None = ...) -> LinearSegmentedColormap: ...
131144

132145
class ListedColormap(Colormap):
133146
colors: ArrayLike | ColorType
134147
def __init__(
135-
self, colors: ArrayLike | ColorType, name: str = ..., N: int | None = ...
148+
self, colors: ArrayLike | ColorType, name: str = ..., N: int | None = ...,
149+
*, bad: ColorType | None = ..., under: ColorType | None = ..., over: ColorType | None = ...
136150
) -> None: ...
137151
@property
138152
def monochrome(self) -> bool: ...

lib/matplotlib/tests/test_colors.py

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -221,6 +221,33 @@ def test_colormap_return_types():
221221
assert cmap(x2d).shape == x2d.shape + (4,)
222222

223223

224+
def test_ListedColormap_bad_under_over():
225+
cmap = mcolors.ListedColormap(["r", "g", "b"], bad="c", under="m", over="y")
226+
assert mcolors.same_color(cmap.get_bad(), "c")
227+
assert mcolors.same_color(cmap.get_under(), "m")
228+
assert mcolors.same_color(cmap.get_over(), "y")
229+
230+
231+
def test_LinearSegmentedColormap_bad_under_over():
232+
cdict = {
233+
'red': [(0., 0., 0.), (0.5, 1., 1.), (1., 1., 1.)],
234+
'green': [(0., 0., 0.), (0.25, 0., 0.), (0.75, 1., 1.), (1., 1., 1.)],
235+
'blue': [(0., 0., 0.), (0.5, 0., 0.), (1., 1., 1.)],
236+
}
237+
cmap = mcolors.LinearSegmentedColormap("lsc", cdict, bad="c", under="m", over="y")
238+
assert mcolors.same_color(cmap.get_bad(), "c")
239+
assert mcolors.same_color(cmap.get_under(), "m")
240+
assert mcolors.same_color(cmap.get_over(), "y")
241+
242+
243+
def test_LinearSegmentedColormap_from_list_bad_under_over():
244+
cmap = mcolors.LinearSegmentedColormap.from_list(
245+
"lsc", ["r", "g", "b"], bad="c", under="m", over="y")
246+
assert mcolors.same_color(cmap.get_bad(), "c")
247+
assert mcolors.same_color(cmap.get_under(), "m")
248+
assert mcolors.same_color(cmap.get_over(), "y")
249+
250+
224251
def test_BoundaryNorm():
225252
"""
226253
GitHub issue #1258: interpolation was failing with numpy

0 commit comments

Comments
 (0)