Alpha argument can be an array · matplotlib/matplotlib@20b894b · GitHub
Skip to content

Commit 20b894b

Browse files
committed
Alpha argument can be an array
Previously this was supported only for images. Now it works for collections, and when directly calling a colormap or to_rgba_array.
1 parent 855e3bb commit 20b894b

9 files changed

Lines changed: 259 additions & 36 deletions

File tree

Lines changed: 31 additions & 0 deletions

lib/matplotlib/artist.py

Lines changed: 29 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -954,10 +954,37 @@ def set_alpha(self, alpha):
954954
955955
Parameters
956956
----------
957-
alpha : float or None
957+
alpha : scalar or None
958+
*alpha* must be within the 0-1 range, inclusive.
958959
"""
959960
if alpha is not None and not isinstance(alpha, Number):
960-
raise TypeError('alpha must be a float or None')
961+
raise TypeError(
962+
f'alpha must be numeric or None, not {type(alpha)}')
963+
if alpha is not None and not (0 <= alpha <= 1):
964+
raise ValueError(f'alpha ({alpha}) is outside 0-1 range')
965+
self._alpha = alpha
966+
self.pchanged()
967+
self.stale = True
968+
969+
def _set_alpha_for_array(self, alpha):
970+
"""
971+
Set the alpha value used for blending - not supported on all backends.
972+
973+
Parameters
974+
----------
975+
alpha : array-like or scalar or None
976+
All values must be within the 0-1 range, inclusive.
977+
Masked values and nans are not supported.
978+
"""
979+
if isinstance(alpha, str):
980+
raise TypeError("alpha must be numeric or None, not a string")
981+
if not np.iterable(alpha):
982+
Artist.set_alpha(self, alpha)
983+
return
984+
alpha = np.asarray(alpha)
985+
if not (alpha.min() >= 0 and alpha.max() <= 1):
986+
raise ValueError('alpha must be between 0 and 1, inclusive, '
987+
f'but min is {alpha.min()}, max is {alpha.max()}')
961988
self._alpha = alpha
962989
self.pchanged()
963990
self.stale = True

lib/matplotlib/collections.py

Lines changed: 27 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -832,12 +832,24 @@ def set_edgecolor(self, c):
832832
self._set_edgecolor(c)
833833

834834
def set_alpha(self, alpha):
835-
# docstring inherited
836-
super().set_alpha(alpha)
835+
"""
836+
Set the transparency of the collection.
837+
838+
Parameters
839+
----------
840+
alpha: float or array of float
841+
If not None, *alpha* values must be between 0 and 1, inclusive.
842+
If an array is provided, it's length must match the number of
843+
elements in the collection. Masked values and nans are not
844+
supported.
845+
"""
846+
artist.Artist._set_alpha_for_array(self, alpha)
837847
self._update_dict['array'] = True
838848
self._set_facecolor(self._original_facecolor)
839849
self._set_edgecolor(self._original_edgecolor)
840850

851+
set_alpha.__doc__ = artist.Artist._set_alpha_for_array.__doc__
852+
841853
def get_linewidth(self):
842854
return self._linewidths
843855

@@ -848,11 +860,23 @@ def update_scalarmappable(self):
848860
"""Update colors from the scalar mappable array, if it is not None."""
849861
if self._A is None:
850862
return
851-
# QuadMesh can map 2d arrays
863+
# QuadMesh can map 2d arrays (but pcolormesh supplies 1d array)
852864
if self._A.ndim > 1 and not isinstance(self, QuadMesh):
853865
raise ValueError('Collections can only map rank 1 arrays')
854866
if not self._check_update("array"):
855867
return
868+
if np.iterable(self._alpha):
869+
if self._alpha.size != self._A.size:
870+
# This can occur with the deprecated behavior of 'flat'
871+
# pcolormesh shading. If we bring the current change in
872+
# before that deprecated behavior is removed, we need to
873+
# add the explanation to the message below.
874+
raise ValueError(f'Data array shape, {self._A.shape} '
875+
'is incompatible with alpha array shape, '
876+
f'{self._alpha.shape}.')
877+
# pcolormesh, scatter, maybe others flatten their _A
878+
self._alpha = self._alpha.reshape(self._A.shape)
879+
856880
if self._is_filled:
857881
self._facecolors = self.to_rgba(self._A, self._alpha)
858882
elif self._is_stroked:

lib/matplotlib/colors.py

Lines changed: 55 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -289,18 +289,40 @@ def to_rgba_array(c, alpha=None):
289289
"""
290290
Convert *c* to a (n, 4) array of RGBA colors.
291291
292-
If *alpha* is not ``None``, it forces the alpha value. If *c* is
293-
``"none"`` (case-insensitive) or an empty list, an empty array is returned.
294-
If *c* is a masked array, an ndarray is returned with a (0, 0, 0, 0)
295-
row for each masked value or row in *c*.
292+
Parameters
293+
----------
294+
c : Matplotlib color or array of colors
295+
If *c* is a masked array, an ndarray is returned with a (0, 0, 0, 0)
296+
row for each masked value or row in *c*.
297+
298+
alpha : float or sequence of floats, optional
299+
If *alpha* is not ``None``, it forces the alpha value, except if *c* is
300+
``"none"`` (case-insensitive), which always maps to ``(0, 0, 0, 0)``.
301+
If *alpha* is a sequence and *c* is a single color, *c* will be
302+
repeated to match the length of *alpha*.
303+
304+
Returns
305+
-------
306+
array
307+
(n, 4) array of RGBA colors.
308+
296309
"""
297310
# Special-case inputs that are already arrays, for performance. (If the
298311
# array has the wrong kind or shape, raise the error during one-at-a-time
299312
# conversion.)
313+
if np.iterable(alpha):
314+
alpha = np.asarray(alpha).ravel()
300315
if (isinstance(c, np.ndarray) and c.dtype.kind in "if"
301316
and c.ndim == 2 and c.shape[1] in [3, 4]):
302317
mask = c.mask.any(axis=1) if np.ma.is_masked(c) else None
303318
c = np.ma.getdata(c)
319+
if np.iterable(alpha):
320+
if c.shape[0] == 1 and alpha.shape[0] > 1:
321+
c = np.tile(c, (alpha.shape[0], 1))
322+
elif c.shape[0] != alpha.shape[0]:
323+
raise ValueError("The number of colors must match the number"
324+
" of alpha values if there are more than one"
325+
" of each.")
304326
if c.shape[1] == 3:
305327
result = np.column_stack([c, np.zeros(len(c))])
306328
result[:, -1] = alpha if alpha is not None else 1.
@@ -320,7 +342,10 @@ def to_rgba_array(c, alpha=None):
320342
if cbook._str_lower_equal(c, "none"):
321343
return np.zeros((0, 4), float)
322344
try:
323-
return np.array([to_rgba(c, alpha)], float)
345+
if np.iterable(alpha):
346+
return np.array([to_rgba(c, a) for a in alpha], float)
347+
else:
348+
return np.array([to_rgba(c, alpha)], float)
324349
except (ValueError, TypeError):
325350
pass
326351

@@ -332,7 +357,10 @@ def to_rgba_array(c, alpha=None):
332357
if len(c) == 0:
333358
return np.zeros((0, 4), float)
334359
else:
335-
return np.array([to_rgba(cc, alpha) for cc in c])
360+
if np.iterable(alpha):
361+
return np.array([to_rgba(cc, aa) for cc, aa in zip(c, alpha)])
362+
else:
363+
return np.array([to_rgba(cc, alpha) for cc in c])
336364

337365

338366
def to_rgb(c):
@@ -539,8 +567,9 @@ def __call__(self, X, alpha=None, bytes=False):
539567
return the RGBA values ``X*100`` percent along the Colormap line.
540568
For integers, X should be in the interval ``[0, Colormap.N)`` to
541569
return RGBA values *indexed* from the Colormap with index ``X``.
542-
alpha : float, None
543-
Alpha must be a scalar between 0 and 1, or None.
570+
alpha : float, array-like, None
571+
Alpha must be a scalar between 0 and 1, a sequence of such
572+
floats with shape matching X, or None.
544573
bytes : bool
545574
If False (default), the returned RGBA values will be floats in the
546575
interval ``[0, 1]`` otherwise they will be uint8s in the interval
@@ -580,23 +609,29 @@ def __call__(self, X, alpha=None, bytes=False):
580609
else:
581610
lut = self._lut.copy() # Don't let alpha modify original _lut.
582611

612+
rgba = np.empty(shape=xa.shape + (4,), dtype=lut.dtype)
613+
lut.take(xa, axis=0, mode='clip', out=rgba)
614+
583615
if alpha is not None:
616+
if np.iterable(alpha):
617+
alpha = np.asarray(alpha)
618+
if not (alpha.shape == xa.shape):
619+
raise ValueError("alpha is array-like but it's shape"
620+
" %s doesn't match that of X %s" %
621+
(alpha.shape, xa.shape))
622+
584623
alpha = np.clip(alpha, 0, 1)
585624
if bytes:
586-
alpha = int(alpha * 255)
587-
if (lut[-1] == 0).all():
588-
lut[:-1, -1] = alpha
589-
# All zeros is taken as a flag for the default bad
590-
# color, which is no color--fully transparent. We
591-
# don't want to override this.
592-
else:
593-
lut[:, -1] = alpha
594-
# If the bad value is set to have a color, then we
595-
# override its alpha just as for any other value.
625+
alpha = (alpha * 255).astype(np.uint8)
626+
rgba[..., -1] = alpha
627+
628+
if (lut[-1] == 0).all() and mask_bad is not None:
629+
if mask_bad.shape == xa.shape:
630+
rgba[mask_bad] = (0, 0, 0, 0)
631+
elif mask_bad:
632+
rgba[..., :] = (0, 0, 0, 0)
596633

597-
rgba = lut[xa]
598634
if not np.iterable(X):
599-
# Return a tuple if the input was a scalar
600635
rgba = tuple(rgba)
601636
return rgba
602637

lib/matplotlib/image.py

Lines changed: 5 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -274,16 +274,12 @@ def set_alpha(self, alpha):
274274
275275
Parameters
276276
----------
277-
alpha : float
277+
alpha : float or 2-d array or None
278278
"""
279-
if alpha is not None and not isinstance(alpha, Number):
280-
alpha = np.asarray(alpha)
281-
if alpha.ndim != 2:
282-
raise TypeError('alpha must be a float, two-dimensional '
283-
'array, or None')
284-
self._alpha = alpha
285-
self.pchanged()
286-
self.stale = True
279+
martist.Artist._set_alpha_for_array(self, alpha)
280+
if np.ndim(alpha) not in (0, 2):
281+
raise TypeError('alpha must be a float, two-dimensional '
282+
'array, or None')
287283
self._imcache = None
288284

289285
def _get_scalar_alpha(self):

lib/matplotlib/tests/test_artist.py

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -277,3 +277,29 @@ def test_artist_inspector_get_aliases():
277277
ai = martist.ArtistInspector(mlines.Line2D)
278278
aliases = ai.get_aliases()
279279
assert aliases["linewidth"] == {"lw"}
280+
281+
282+
def test_set_alpha():
283+
art = martist.Artist()
284+
with pytest.raises(TypeError, match='^alpha must be numeric or None'):
285+
art.set_alpha('string')
286+
with pytest.raises(TypeError, match='^alpha must be numeric or None'):
287+
art.set_alpha([1, 2, 3])
288+
with pytest.raises(ValueError, match="outside 0-1 range"):
289+
art.set_alpha(1.1)
290+
with pytest.raises(ValueError, match="outside 0-1 range"):
291+
art.set_alpha(np.nan)
292+
293+
294+
def test_set_alpha_for_array():
295+
art = martist.Artist()
296+
with pytest.raises(TypeError, match='^alpha must be numeric or None'):
297+
art._set_alpha_for_array('string')
298+
with pytest.raises(ValueError, match="outside 0-1 range"):
299+
art._set_alpha_for_array(1.1)
300+
with pytest.raises(ValueError, match="outside 0-1 range"):
301+
art._set_alpha_for_array(np.nan)
302+
with pytest.raises(ValueError, match="alpha must be between 0 and 1"):
303+
art._set_alpha_for_array([0.5, 1.1])
304+
with pytest.raises(ValueError, match="alpha must be between 0 and 1"):
305+
art._set_alpha_for_array([0.5, np.nan])

lib/matplotlib/tests/test_collections.py

Lines changed: 62 additions & 2 deletions

0 commit comments

Comments
 (0)