-
-
Notifications
You must be signed in to change notification settings - Fork 7.9k
ENH: support alpha arrays in collections #6268
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
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,31 @@ | ||
Transparency (alpha) can be set as an array in collections | ||
---------------------------------------------------------- | ||
Previously, the alpha value controlling tranparency in collections could be | ||
specified only as a scalar applied to all elements in the collection. | ||
For example, all the markers in a `~.Axes.scatter` plot, or all the | ||
quadrilaterals in a `~.Axes.pcolormesh` plot, would have the same alpha value. | ||
|
||
Now it is possible to supply alpha as an array with one value for each element | ||
(marker, quadrilateral, etc.) in a collection. | ||
|
||
.. plot:: | ||
|
||
x = np.arange(5, dtype=float) | ||
y = np.arange(5, dtype=float) | ||
# z and zalpha for demo pcolormesh | ||
z = x[1:, np.newaxis] + y[np.newaxis, 1:] | ||
zalpha = np.ones_like(z) | ||
zalpha[::2, ::2] = 0.3 # alternate patches are partly transparent | ||
# s and salpha for demo scatter | ||
s = x | ||
salpha = np.linspace(0.1, 0.9, len(x)) # just a ramp | ||
|
||
fig, axs = plt.subplots(2, 2, constrained_layout=True) | ||
axs[0, 0].pcolormesh(x, y, z, alpha=zalpha) | ||
axs[0, 0].set_title("pcolormesh") | ||
axs[0, 1].scatter(x, y, c=s, alpha=salpha) | ||
axs[0, 1].set_title("color-mapped") | ||
axs[1, 0].scatter(x, y, c='k', alpha=salpha) | ||
axs[1, 0].set_title("c='k'") | ||
axs[1, 1].scatter(x, y, c=['r', 'g', 'b', 'c', 'm'], alpha=salpha) | ||
axs[1, 1].set_title("c=['r', 'g', 'b', 'c', 'm']") |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -289,18 +289,40 @@ def to_rgba_array(c, alpha=None): | |
""" | ||
Convert *c* to a (n, 4) array of RGBA colors. | ||
|
||
If *alpha* is not ``None``, it forces the alpha value. If *c* is | ||
``"none"`` (case-insensitive) or an empty list, an empty array is returned. | ||
If *c* is a masked array, an ndarray is returned with a (0, 0, 0, 0) | ||
row for each masked value or row in *c*. | ||
Parameters | ||
---------- | ||
c : Matplotlib color or array of colors | ||
If *c* is a masked array, an ndarray is returned with a (0, 0, 0, 0) | ||
row for each masked value or row in *c*. | ||
|
||
alpha : float or sequence of floats, optional | ||
If *alpha* is not ``None``, it forces the alpha value, except if *c* is | ||
``"none"`` (case-insensitive), which always maps to ``(0, 0, 0, 0)``. | ||
If *alpha* is a sequence and *c* is a single color, *c* will be | ||
repeated to match the length of *alpha*. | ||
|
||
Returns | ||
------- | ||
array | ||
(n, 4) array of RGBA colors. | ||
|
||
""" | ||
# Special-case inputs that are already arrays, for performance. (If the | ||
# array has the wrong kind or shape, raise the error during one-at-a-time | ||
# conversion.) | ||
if np.iterable(alpha): | ||
alpha = np.asarray(alpha).ravel() | ||
if (isinstance(c, np.ndarray) and c.dtype.kind in "if" | ||
and c.ndim == 2 and c.shape[1] in [3, 4]): | ||
mask = c.mask.any(axis=1) if np.ma.is_masked(c) else None | ||
c = np.ma.getdata(c) | ||
if np.iterable(alpha): | ||
if c.shape[0] == 1 and alpha.shape[0] > 1: | ||
c = np.tile(c, (alpha.shape[0], 1)) | ||
elif c.shape[0] != alpha.shape[0]: | ||
raise ValueError("The number of colors must match the number" | ||
" of alpha values if there are more than one" | ||
" of each.") | ||
if c.shape[1] == 3: | ||
result = np.column_stack([c, np.zeros(len(c))]) | ||
result[:, -1] = alpha if alpha is not None else 1. | ||
|
@@ -320,7 +342,10 @@ def to_rgba_array(c, alpha=None): | |
if cbook._str_lower_equal(c, "none"): | ||
return np.zeros((0, 4), float) | ||
try: | ||
return np.array([to_rgba(c, alpha)], float) | ||
if np.iterable(alpha): | ||
return np.array([to_rgba(c, a) for a in alpha], float) | ||
else: | ||
return np.array([to_rgba(c, alpha)], float) | ||
except (ValueError, TypeError): | ||
pass | ||
|
||
|
@@ -332,7 +357,10 @@ def to_rgba_array(c, alpha=None): | |
if len(c) == 0: | ||
return np.zeros((0, 4), float) | ||
else: | ||
return np.array([to_rgba(cc, alpha) for cc in c]) | ||
if np.iterable(alpha): | ||
return np.array([to_rgba(cc, aa) for cc, aa in zip(c, alpha)]) | ||
else: | ||
return np.array([to_rgba(cc, alpha) for cc in c]) | ||
|
||
|
||
def to_rgb(c): | ||
|
@@ -539,8 +567,9 @@ def __call__(self, X, alpha=None, bytes=False): | |
return the RGBA values ``X*100`` percent along the Colormap line. | ||
For integers, X should be in the interval ``[0, Colormap.N)`` to | ||
return RGBA values *indexed* from the Colormap with index ``X``. | ||
alpha : float, None | ||
Alpha must be a scalar between 0 and 1, or None. | ||
alpha : float, array-like, None | ||
Alpha must be a scalar between 0 and 1, a sequence of such | ||
floats with shape matching X, or None. | ||
bytes : bool | ||
If False (default), the returned RGBA values will be floats in the | ||
interval ``[0, 1]`` otherwise they will be uint8s in the interval | ||
|
@@ -580,23 +609,29 @@ def __call__(self, X, alpha=None, bytes=False): | |
else: | ||
lut = self._lut.copy() # Don't let alpha modify original _lut. | ||
|
||
rgba = np.empty(shape=xa.shape + (4,), dtype=lut.dtype) | ||
lut.take(xa, axis=0, mode='clip', out=rgba) | ||
|
||
if alpha is not None: | ||
if np.iterable(alpha): | ||
alpha = np.asarray(alpha) | ||
if alpha.shape != xa.shape: | ||
raise ValueError("alpha is array-like but its shape" | ||
" %s doesn't match that of X %s" % | ||
(alpha.shape, xa.shape)) | ||
alpha = np.clip(alpha, 0, 1) | ||
if bytes: | ||
alpha = int(alpha * 255) | ||
if (lut[-1] == 0).all(): | ||
lut[:-1, -1] = alpha | ||
# All zeros is taken as a flag for the default bad | ||
# color, which is no color--fully transparent. We | ||
# don't want to override this. | ||
else: | ||
lut[:, -1] = alpha | ||
# If the bad value is set to have a color, then we | ||
# override its alpha just as for any other value. | ||
alpha = (alpha * 255).astype(np.uint8) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Stall a couple of code paths not tested? (L625 and 632) |
||
rgba[..., -1] = alpha | ||
|
||
# If the "bad" color is all zeros, then ignore alpha input. | ||
if (lut[-1] == 0).all() and np.any(mask_bad): | ||
if np.iterable(mask_bad) and mask_bad.shape == xa.shape: | ||
rgba[mask_bad] = (0, 0, 0, 0) | ||
else: | ||
rgba[..., :] = (0, 0, 0, 0) | ||
|
||
rgba = lut[xa] | ||
if not np.iterable(X): | ||
# Return a tuple if the input was a scalar | ||
rgba = tuple(rgba) | ||
return rgba | ||
|
||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -274,16 +274,12 @@ def set_alpha(self, alpha): | |
|
||
Parameters | ||
---------- | ||
alpha : float | ||
alpha : float or 2D array-like or None | ||
""" | ||
if alpha is not None and not isinstance(alpha, Number): | ||
alpha = np.asarray(alpha) | ||
if alpha.ndim != 2: | ||
raise TypeError('alpha must be a float, two-dimensional ' | ||
'array, or None') | ||
self._alpha = alpha | ||
self.pchanged() | ||
self.stale = True | ||
martist.Artist._set_alpha_for_array(self, alpha) | ||
if np.ndim(alpha) not in (0, 2): | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Do you not want to check this before calling the setter? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. No, I think it is better to have the more basic argument-checking from the setter before checking the number of dimensions. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Wouldn't that leave There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why would that matter? An exception is being raised either way. Look at self._A = cbook.safe_masked_invalid(A, copy=True) It looks like the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. To resolve this, can we easily write a test that shows how it works? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't see anything to resolve. I have already included a test for this case, raising an exception if the wrong number of dimensions is supplied for an image. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. FWIW, if we already have cases of inconsistent state on invalid input, then I'm slightly less concerned about adding one more. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It is impossible to do all validation before setting attributes, given independent setters, because valid alpha depends on data, and vice-versa. That's why one of the tests I added calls |
||
raise TypeError('alpha must be a float, two-dimensional ' | ||
'array, or None') | ||
self._imcache = None | ||
|
||
def _get_scalar_alpha(self): | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -543,11 +543,38 @@ def test_cap_and_joinstyle_image(): | |
def test_scatter_post_alpha(): | ||
fig, ax = plt.subplots() | ||
sc = ax.scatter(range(5), range(5), c=range(5)) | ||
# this needs to be here to update internal state | ||
fig.canvas.draw() | ||
sc.set_alpha(.1) | ||
|
||
|
||
def test_scatter_alpha_array(): | ||
x = np.arange(5) | ||
alpha = x / 5 | ||
# With color mapping. | ||
fig, (ax0, ax1) = plt.subplots(2) | ||
sc0 = ax0.scatter(x, x, c=x, alpha=alpha) | ||
sc1 = ax1.scatter(x, x, c=x) | ||
sc1.set_alpha(alpha) | ||
plt.draw() | ||
assert_array_equal(sc0.get_facecolors()[:, -1], alpha) | ||
assert_array_equal(sc1.get_facecolors()[:, -1], alpha) | ||
# Without color mapping. | ||
fig, (ax0, ax1) = plt.subplots(2) | ||
sc0 = ax0.scatter(x, x, color=['r', 'g', 'b', 'c', 'm'], alpha=alpha) | ||
sc1 = ax1.scatter(x, x, color='r', alpha=alpha) | ||
plt.draw() | ||
assert_array_equal(sc0.get_facecolors()[:, -1], alpha) | ||
assert_array_equal(sc1.get_facecolors()[:, -1], alpha) | ||
# Without color mapping, and set alpha afterward. | ||
fig, (ax0, ax1) = plt.subplots(2) | ||
sc0 = ax0.scatter(x, x, color=['r', 'g', 'b', 'c', 'm']) | ||
sc0.set_alpha(alpha) | ||
sc1 = ax1.scatter(x, x, color='r') | ||
sc1.set_alpha(alpha) | ||
plt.draw() | ||
assert_array_equal(sc0.get_facecolors()[:, -1], alpha) | ||
assert_array_equal(sc1.get_facecolors()[:, -1], alpha) | ||
|
||
|
||
def test_pathcollection_legend_elements(): | ||
np.random.seed(19680801) | ||
x, y = np.random.rand(2, 10) | ||
|
@@ -662,6 +689,39 @@ def test_quadmesh_set_array(): | |
assert np.array_equal(coll.get_array(), np.ones(9)) | ||
|
||
|
||
def test_quadmesh_alpha_array(): | ||
x = np.arange(4) | ||
y = np.arange(4) | ||
z = np.arange(9).reshape((3, 3)) | ||
alpha = z / z.max() | ||
alpha_flat = alpha.ravel() | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. So at somepoint someone will put some masked or NaN into alpha. What will happen, and what do you recommend? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Fail on nan; ignore mask. I don't think it is worthwhile for us to try to propagate nans and masked arrays from the alpha input. |
||
# Provide 2-D alpha: | ||
fig, (ax0, ax1) = plt.subplots(2) | ||
coll1 = ax0.pcolormesh(x, y, z, alpha=alpha) | ||
coll2 = ax1.pcolormesh(x, y, z) | ||
coll2.set_alpha(alpha) | ||
plt.draw() | ||
assert_array_equal(coll1.get_facecolors()[:, -1], alpha_flat) | ||
assert_array_equal(coll2.get_facecolors()[:, -1], alpha_flat) | ||
# Or provide 1-D alpha: | ||
fig, (ax0, ax1) = plt.subplots(2) | ||
coll1 = ax0.pcolormesh(x, y, z, alpha=alpha_flat) | ||
coll2 = ax1.pcolormesh(x, y, z) | ||
coll2.set_alpha(alpha_flat) | ||
plt.draw() | ||
assert_array_equal(coll1.get_facecolors()[:, -1], alpha_flat) | ||
assert_array_equal(coll2.get_facecolors()[:, -1], alpha_flat) | ||
|
||
|
||
def test_alpha_validation(): | ||
# Most of the relevant testing is in test_artist and test_colors. | ||
fig, ax = plt.subplots() | ||
pc = ax.pcolormesh(np.arange(12).reshape((3, 4))) | ||
with pytest.raises(ValueError, match="^Data array shape"): | ||
pc.set_alpha([0.5, 0.6]) | ||
pc.update_scalarmappable() | ||
|
||
|
||
def test_legend_inverse_size_label_relationship(): | ||
""" | ||
Ensure legend markers scale appropriately when label and size are | ||
|
Uh oh!
There was an error while loading. Please reload this page.