Skip to content

Commit 0ab0843

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 685ea2b commit 0ab0843

File tree

6 files changed

+149
-52
lines changed

6 files changed

+149
-52
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: 86 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -714,20 +714,26 @@ class Colormap:
714714
chain.
715715
"""
716716

717-
def __init__(self, name, N=256):
717+
def __init__(self, name, N=256, *, bad=None, under=None, over=None):
718718
"""
719719
Parameters
720720
----------
721721
name : str
722722
The name of the colormap.
723723
N : int
724724
The number of RGB quantization levels.
725+
bad : :mpltype:`color`, default: transparent
726+
The color for invalid values (NaN or masked).
727+
under : :mpltype:`color`, default: color of the lowest value
728+
The color for low out-of-range values.
729+
over : :mpltype:`color`, default: color of the highest value
730+
The color for high out-of-range values.
725731
"""
726732
self.name = name
727733
self.N = int(N) # ensure that N is always int
728-
self._rgba_bad = (0.0, 0.0, 0.0, 0.0) # If bad, don't paint anything.
729-
self._rgba_under = None
730-
self._rgba_over = None
734+
self._rgba_bad = (0.0, 0.0, 0.0, 0.0) if bad is None else to_rgba(bad)
735+
self._rgba_under = None if under is None else to_rgba(under)
736+
self._rgba_over = None if over is None else to_rgba(over)
731737
self._i_under = self.N
732738
self._i_over = self.N + 1
733739
self._i_bad = self.N + 2
@@ -1034,43 +1040,61 @@ class LinearSegmentedColormap(Colormap):
10341040
segments.
10351041
"""
10361042

1037-
def __init__(self, name, segmentdata, N=256, gamma=1.0):
1043+
def __init__(self, name, segmentdata, N=256, gamma=1.0, *,
1044+
bad=None, under=None, over=None):
10381045
"""
1039-
Create colormap from linear mapping segments
1040-
1041-
segmentdata argument is a dictionary with a red, green and blue
1042-
entries. Each entry should be a list of *x*, *y0*, *y1* tuples,
1043-
forming rows in a table. Entries for alpha are optional.
1044-
1045-
Example: suppose you want red to increase from 0 to 1 over
1046-
the bottom half, green to do the same over the middle half,
1047-
and blue over the top half. Then you would use::
1048-
1049-
cdict = {'red': [(0.0, 0.0, 0.0),
1050-
(0.5, 1.0, 1.0),
1051-
(1.0, 1.0, 1.0)],
1046+
Create colormap from linear mapping segments.
10521047
1053-
'green': [(0.0, 0.0, 0.0),
1054-
(0.25, 0.0, 0.0),
1055-
(0.75, 1.0, 1.0),
1056-
(1.0, 1.0, 1.0)],
1048+
Parameters
1049+
----------
1050+
name : str
1051+
The name of the colormap.
1052+
segmentdata : dict
1053+
A dictionary with keys "red", "green", "blue" for the color channels.
1054+
Each entry should be a list of *x*, *y0*, *y1* tuples, forming rows
1055+
in a table. Entries for alpha are optional.
1056+
1057+
Example: suppose you want red to increase from 0 to 1 over
1058+
the bottom half, green to do the same over the middle half,
1059+
and blue over the top half. Then you would use::
1060+
1061+
{
1062+
'red': [(0.0, 0.0, 0.0),
1063+
(0.5, 1.0, 1.0),
1064+
(1.0, 1.0, 1.0)],
1065+
'green': [(0.0, 0.0, 0.0),
1066+
(0.25, 0.0, 0.0),
1067+
(0.75, 1.0, 1.0),
1068+
(1.0, 1.0, 1.0)],
1069+
'blue': [(0.0, 0.0, 0.0),
1070+
(0.5, 0.0, 0.0),
1071+
(1.0, 1.0, 1.0)]
1072+
}
10571073
1058-
'blue': [(0.0, 0.0, 0.0),
1059-
(0.5, 0.0, 0.0),
1060-
(1.0, 1.0, 1.0)]}
1074+
Each row in the table for a given color is a sequence of
1075+
*x*, *y0*, *y1* tuples. In each sequence, *x* must increase
1076+
monotonically from 0 to 1. For any input value *z* falling
1077+
between *x[i]* and *x[i+1]*, the output value of a given color
1078+
will be linearly interpolated between *y1[i]* and *y0[i+1]*::
10611079
1062-
Each row in the table for a given color is a sequence of
1063-
*x*, *y0*, *y1* tuples. In each sequence, *x* must increase
1064-
monotonically from 0 to 1. For any input value *z* falling
1065-
between *x[i]* and *x[i+1]*, the output value of a given color
1066-
will be linearly interpolated between *y1[i]* and *y0[i+1]*::
1080+
row i: x y0 y1
1081+
/
1082+
/
1083+
row i+1: x y0 y1
10671084
1068-
row i: x y0 y1
1069-
/
1070-
/
1071-
row i+1: x y0 y1
1085+
Hence, y0 in the first row and y1 in the last row are never used.
10721086
1073-
Hence y0 in the first row and y1 in the last row are never used.
1087+
N : int
1088+
The number of RGB quantization levels.
1089+
gamma : float
1090+
Gamma correction factor for input distribution x of the mapping.
1091+
See also https://en.wikipedia.org/wiki/Gamma_correction.
1092+
bad : :mpltype:`color`, default: transparent
1093+
The color for invalid values (NaN or masked).
1094+
under : :mpltype:`color`, default: color of the lowest value
1095+
The color for low out-of-range values.
1096+
over : :mpltype:`color`, default: color of the highest value
1097+
The color for high out-of-range values.
10741098
10751099
See Also
10761100
--------
@@ -1080,7 +1104,7 @@ def __init__(self, name, segmentdata, N=256, gamma=1.0):
10801104
"""
10811105
# True only if all colors in map are identical; needed for contouring.
10821106
self.monochrome = False
1083-
super().__init__(name, N)
1107+
super().__init__(name, N, bad=bad, under=under, over=over)
10841108
self._segmentdata = segmentdata
10851109
self._gamma = gamma
10861110

@@ -1104,7 +1128,7 @@ def set_gamma(self, gamma):
11041128
self._init()
11051129

11061130
@staticmethod
1107-
def from_list(name, colors, N=256, gamma=1.0):
1131+
def from_list(name, colors, N=256, gamma=1.0, *, bad=None, under=None, over=None):
11081132
"""
11091133
Create a `LinearSegmentedColormap` from a list of colors.
11101134
@@ -1121,6 +1145,13 @@ def from_list(name, colors, N=256, gamma=1.0):
11211145
N : int
11221146
The number of RGB quantization levels.
11231147
gamma : float
1148+
1149+
bad : :mpltype:`color`, default: transparent
1150+
The color for invalid values (NaN or masked).
1151+
under : :mpltype:`color`, default: color of the lowest value
1152+
The color for low out-of-range values.
1153+
over : :mpltype:`color`, default: color of the highest value
1154+
The color for high out-of-range values.
11241155
"""
11251156
if not np.iterable(colors):
11261157
raise ValueError('colors must be iterable')
@@ -1140,7 +1171,8 @@ def from_list(name, colors, N=256, gamma=1.0):
11401171
"alpha": np.column_stack([vals, a, a]),
11411172
}
11421173

1143-
return LinearSegmentedColormap(name, cdict, N, gamma)
1174+
return LinearSegmentedColormap(name, cdict, N, gamma,
1175+
bad=bad, under=under, over=over)
11441176

11451177
def resampled(self, lutsize):
11461178
"""Return a new colormap with *lutsize* entries."""
@@ -1215,6 +1247,18 @@ class ListedColormap(Colormap):
12151247
N > len(colors)
12161248
12171249
the list will be extended by repetition.
1250+
1251+
.. deprecated:: 3.11
1252+
1253+
This parameter will be removed. Please instead ensure that
1254+
the list of passed colors is the required length.
1255+
1256+
bad : :mpltype:`color`, default: transparent
1257+
The color for invalid values (NaN or masked).
1258+
under : :mpltype:`color`, default: color of the lowest value
1259+
The color for low out-of-range values.
1260+
over : :mpltype:`color`, default: color of the highest value
1261+
The color for high out-of-range values.
12181262
"""
12191263

12201264
@_api.delete_parameter(
@@ -1223,7 +1267,8 @@ class ListedColormap(Colormap):
12231267
"and will be removed in %(removal)s. Please ensure the list "
12241268
"of passed colors is the required length instead."
12251269
)
1226-
def __init__(self, colors, name='from_list', N=None):
1270+
def __init__(self, colors, name='from_list', N=None, *,
1271+
bad=None, under=None, over=None):
12271272
if N is None:
12281273
self.colors = colors
12291274
N = len(colors)
@@ -1240,7 +1285,7 @@ def __init__(self, colors, name='from_list', N=None):
12401285
pass
12411286
else:
12421287
self.colors = [gray] * N
1243-
super().__init__(name, N)
1288+
super().__init__(name, N, bad=bad, under=under, over=over)
12441289

12451290
def _init(self):
12461291
self._lut = np.zeros((self.N + 3, 4), float)
@@ -3744,8 +3789,7 @@ def from_levels_and_colors(levels, colors, extend='neither'):
37443789
data_colors = colors[color_slice]
37453790
under_color = colors[0] if extend in ['min', 'both'] else 'none'
37463791
over_color = colors[-1] if extend in ['max', 'both'] else 'none'
3747-
cmap = ListedColormap(data_colors).with_extremes(
3748-
under=under_color, over=over_color)
3792+
cmap = ListedColormap(data_colors, under=under_color, over=over_color)
37493793

37503794
cmap.colorbar_extend = extend
37513795

lib/matplotlib/colors.pyi

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -120,19 +120,24 @@ class LinearSegmentedColormap(Colormap):
120120
],
121121
N: int = ...,
122122
gamma: float = ...,
123+
bad: ColorType | None = ...,
124+
under: ColorType | None = ...,
125+
over: ColorType | None = ...,
123126
) -> None: ...
124127
def set_gamma(self, gamma: float) -> None: ...
125128
@staticmethod
126129
def from_list(
127-
name: str, colors: ArrayLike | Sequence[tuple[float, ColorType]], N: int = ..., gamma: float = ...
130+
name: str, colors: ArrayLike | Sequence[tuple[float, ColorType]], N: int = ..., gamma: float = ...,
131+
bad: ColorType | None = ..., under: ColorType | None = ..., over: ColorType | None = ...,
128132
) -> LinearSegmentedColormap: ...
129133
def resampled(self, lutsize: int) -> LinearSegmentedColormap: ...
130134
def reversed(self, name: str | None = ...) -> LinearSegmentedColormap: ...
131135

132136
class ListedColormap(Colormap):
133137
colors: ArrayLike | ColorType
134138
def __init__(
135-
self, colors: ArrayLike | ColorType, name: str = ..., N: int | None = ...
139+
self, colors: ArrayLike | ColorType, name: str = ..., N: int | None = ...,
140+
bad: ColorType | None = ..., under: ColorType | None = ..., over: ColorType | None = ...
136141
) -> None: ...
137142
@property
138143
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)