Skip to content

Commit e3a8004

Browse files
authored
Merge pull request #11383 from afvincent/fix_scatter_error_color_shape_11373
ENH: Improve *c* (color) kwarg checking in scatter and the related exceptions
2 parents 0545020 + e9cdfc0 commit e3a8004

File tree

2 files changed

+155
-65
lines changed

2 files changed

+155
-65
lines changed

lib/matplotlib/axes/_axes.py

+45-12
Original file line numberDiff line numberDiff line change
@@ -3792,7 +3792,9 @@ def scatter(self, x, y, s=None, c=None, marker=None, cmap=None, norm=None,
37923792
Note that *c* should not be a single numeric RGB or RGBA sequence
37933793
because that is indistinguishable from an array of values to be
37943794
colormapped. If you want to specify the same RGB or RGBA value for
3795-
all points, use a 2-D array with a single row.
3795+
all points, use a 2-D array with a single row. Otherwise, value-
3796+
matching will have precedence in case of a size matching with *x*
3797+
and *y*.
37963798
37973799
marker : `~matplotlib.markers.MarkerStyle`, optional, default: 'o'
37983800
The marker style. *marker* can be either an instance of the class
@@ -3876,15 +3878,15 @@ def scatter(self, x, y, s=None, c=None, marker=None, cmap=None, norm=None,
38763878
except ValueError:
38773879
raise ValueError("'color' kwarg must be an mpl color"
38783880
" spec or sequence of color specs.\n"
3879-
"For a sequence of values to be"
3880-
" color-mapped, use the 'c' kwarg instead.")
3881+
"For a sequence of values to be color-mapped,"
3882+
" use the 'c' argument instead.")
38813883
if edgecolors is None:
38823884
edgecolors = co
38833885
if facecolors is None:
38843886
facecolors = co
38853887
if c is not None:
3886-
raise ValueError("Supply a 'c' kwarg or a 'color' kwarg"
3887-
" but not both; they differ but"
3888+
raise ValueError("Supply a 'c' argument or a 'color'"
3889+
" kwarg but not both; they differ but"
38883890
" their functionalities overlap.")
38893891
if c is None:
38903892
if facecolors is not None:
@@ -3925,29 +3927,60 @@ def scatter(self, x, y, s=None, c=None, marker=None, cmap=None, norm=None,
39253927
# c is an array for mapping. The potential ambiguity
39263928
# with a sequence of 3 or 4 numbers is resolved in
39273929
# favor of mapping, not rgb or rgba.
3930+
3931+
# Convenience vars to track shape mismatch *and* conversion failures.
3932+
valid_shape = True # will be put to the test!
3933+
n_elem = -1 # used only for (some) exceptions
3934+
39283935
if c_none or co is not None:
39293936
c_array = None
39303937
else:
3931-
try:
3938+
try: # First, does 'c' look suitable for value-mapping?
39323939
c_array = np.asanyarray(c, dtype=float)
3940+
n_elem = c_array.shape[0]
39333941
if c_array.shape in xy_shape:
39343942
c = np.ma.ravel(c_array)
39353943
else:
3944+
if c_array.shape in ((3,), (4,)):
3945+
_log.warning(
3946+
"'c' argument looks like a single numeric RGB or "
3947+
"RGBA sequence, which should be avoided as value-"
3948+
"mapping will have precedence in case its length "
3949+
"matches with 'x' & 'y'. Please use a 2-D array "
3950+
"with a single row if you really want to specify "
3951+
"the same RGB or RGBA value for all points.")
39363952
# Wrong size; it must not be intended for mapping.
3953+
valid_shape = False
39373954
c_array = None
39383955
except ValueError:
39393956
# Failed to make a floating-point array; c must be color specs.
39403957
c_array = None
39413958

39423959
if c_array is None:
3943-
try:
3944-
# must be acceptable as PathCollection facecolors
3960+
try: # Then is 'c' acceptable as PathCollection facecolors?
39453961
colors = mcolors.to_rgba_array(c)
3962+
n_elem = colors.shape[0]
3963+
if colors.shape[0] not in (0, 1, x.size, y.size):
3964+
# NB: remember that a single color is also acceptable.
3965+
# Besides *colors* will be an empty array if c == 'none'.
3966+
valid_shape = False
3967+
raise ValueError
39463968
except ValueError:
3947-
# c not acceptable as PathCollection facecolor
3948-
raise ValueError("c of shape {} not acceptable as a color "
3949-
"sequence for x with size {}, y with size {}"
3950-
.format(c.shape, x.size, y.size))
3969+
if not valid_shape: # but at least one conversion succeeded.
3970+
raise ValueError(
3971+
"'c' argument has {nc} elements, which is not "
3972+
"acceptable for use with 'x' with size {xs}, "
3973+
"'y' with size {ys}."
3974+
.format(nc=n_elem, xs=x.size, ys=y.size)
3975+
)
3976+
# Both the mapping *and* the RGBA conversion failed: pretty
3977+
# severe failure => one may appreciate a verbose feedback.
3978+
raise ValueError(
3979+
"'c' argument must either be valid as mpl color(s) "
3980+
"or as numbers to be mapped to colors. "
3981+
"Here c = {}." # <- beware, could be long depending on c.
3982+
.format(c)
3983+
)
39513984
else:
39523985
colors = None # use cmap, norm after collection is created
39533986

lib/matplotlib/tests/test_axes.py

+110-53
Original file line numberDiff line numberDiff line change
@@ -1676,63 +1676,120 @@ def test_hist2d_transpose():
16761676
ax.hist2d(x, y, bins=10, rasterized=True)
16771677

16781678

1679-
@image_comparison(baseline_images=['scatter', 'scatter'])
1680-
def test_scatter_plot():
1681-
fig, ax = plt.subplots()
1682-
data = {"x": [3, 4, 2, 6], "y": [2, 5, 2, 3], "c": ['r', 'y', 'b', 'lime'],
1683-
"s": [24, 15, 19, 29]}
1684-
1685-
ax.scatter(data["x"], data["y"], c=data["c"], s=data["s"])
1686-
1687-
# Reuse testcase from above for a labeled data test
1688-
fig, ax = plt.subplots()
1689-
ax.scatter("x", "y", c="c", s="s", data=data)
1679+
class TestScatter(object):
1680+
@image_comparison(baseline_images=['scatter', 'scatter'])
1681+
def test_scatter_plot(self):
1682+
fig, ax = plt.subplots()
1683+
data = {"x": [3, 4, 2, 6], "y": [2, 5, 2, 3],
1684+
"c": ['r', 'y', 'b', 'lime'], "s": [24, 15, 19, 29]}
16901685

1686+
ax.scatter(data["x"], data["y"], c=data["c"], s=data["s"])
16911687

1692-
@image_comparison(baseline_images=['scatter_marker'], remove_text=True,
1693-
extensions=['png'])
1694-
def test_scatter_marker():
1695-
fig, (ax0, ax1, ax2) = plt.subplots(ncols=3)
1696-
ax0.scatter([3, 4, 2, 6], [2, 5, 2, 3],
1697-
c=[(1, 0, 0), 'y', 'b', 'lime'],
1698-
s=[60, 50, 40, 30],
1699-
edgecolors=['k', 'r', 'g', 'b'],
1700-
marker='s')
1701-
ax1.scatter([3, 4, 2, 6], [2, 5, 2, 3],
1702-
c=[(1, 0, 0), 'y', 'b', 'lime'],
1703-
s=[60, 50, 40, 30],
1704-
edgecolors=['k', 'r', 'g', 'b'],
1705-
marker=mmarkers.MarkerStyle('o', fillstyle='top'))
1706-
# unit area ellipse
1707-
rx, ry = 3, 1
1708-
area = rx * ry * np.pi
1709-
theta = np.linspace(0, 2 * np.pi, 21)
1710-
verts = np.column_stack([np.cos(theta) * rx / area,
1711-
np.sin(theta) * ry / area])
1712-
ax2.scatter([3, 4, 2, 6], [2, 5, 2, 3],
1713-
c=[(1, 0, 0), 'y', 'b', 'lime'],
1714-
s=[60, 50, 40, 30],
1715-
edgecolors=['k', 'r', 'g', 'b'],
1716-
marker=verts)
1717-
1718-
1719-
@image_comparison(baseline_images=['scatter_2D'], remove_text=True,
1720-
extensions=['png'])
1721-
def test_scatter_2D():
1722-
x = np.arange(3)
1723-
y = np.arange(2)
1724-
x, y = np.meshgrid(x, y)
1725-
z = x + y
1726-
fig, ax = plt.subplots()
1727-
ax.scatter(x, y, c=z, s=200, edgecolors='face')
1688+
# Reuse testcase from above for a labeled data test
1689+
fig, ax = plt.subplots()
1690+
ax.scatter("x", "y", c="c", s="s", data=data)
1691+
1692+
@image_comparison(baseline_images=['scatter_marker'], remove_text=True,
1693+
extensions=['png'])
1694+
def test_scatter_marker(self):
1695+
fig, (ax0, ax1, ax2) = plt.subplots(ncols=3)
1696+
ax0.scatter([3, 4, 2, 6], [2, 5, 2, 3],
1697+
c=[(1, 0, 0), 'y', 'b', 'lime'],
1698+
s=[60, 50, 40, 30],
1699+
edgecolors=['k', 'r', 'g', 'b'],
1700+
marker='s')
1701+
ax1.scatter([3, 4, 2, 6], [2, 5, 2, 3],
1702+
c=[(1, 0, 0), 'y', 'b', 'lime'],
1703+
s=[60, 50, 40, 30],
1704+
edgecolors=['k', 'r', 'g', 'b'],
1705+
marker=mmarkers.MarkerStyle('o', fillstyle='top'))
1706+
# unit area ellipse
1707+
rx, ry = 3, 1
1708+
area = rx * ry * np.pi
1709+
theta = np.linspace(0, 2 * np.pi, 21)
1710+
verts = np.column_stack([np.cos(theta) * rx / area,
1711+
np.sin(theta) * ry / area])
1712+
ax2.scatter([3, 4, 2, 6], [2, 5, 2, 3],
1713+
c=[(1, 0, 0), 'y', 'b', 'lime'],
1714+
s=[60, 50, 40, 30],
1715+
edgecolors=['k', 'r', 'g', 'b'],
1716+
verts=verts)
1717+
1718+
@image_comparison(baseline_images=['scatter_2D'], remove_text=True,
1719+
extensions=['png'])
1720+
def test_scatter_2D(self):
1721+
x = np.arange(3)
1722+
y = np.arange(2)
1723+
x, y = np.meshgrid(x, y)
1724+
z = x + y
1725+
fig, ax = plt.subplots()
1726+
ax.scatter(x, y, c=z, s=200, edgecolors='face')
1727+
1728+
def test_scatter_color(self):
1729+
# Try to catch cases where 'c' kwarg should have been used.
1730+
with pytest.raises(ValueError):
1731+
plt.scatter([1, 2], [1, 2], color=[0.1, 0.2])
1732+
with pytest.raises(ValueError):
1733+
plt.scatter([1, 2, 3], [1, 2, 3], color=[1, 2, 3])
1734+
1735+
# Parameters for *test_scatter_c*. NB: assuming that the
1736+
# scatter plot will have 4 elements. The tuple scheme is:
1737+
# (*c* parameter case, exception regexp key or None if no exception)
1738+
params_test_scatter_c = [
1739+
# Single letter-sequences
1740+
("rgby", None),
1741+
("rgb", "shape"),
1742+
("rgbrgb", "shape"),
1743+
(["rgby"], "conversion"),
1744+
# Special cases
1745+
("red", None),
1746+
("none", None),
1747+
(None, None),
1748+
(["r", "g", "b", "none"], None),
1749+
# Non-valid color spec (FWIW, 'jaune' means yellow in French)
1750+
("jaune", "conversion"),
1751+
(["jaune"], "conversion"), # wrong type before wrong size
1752+
(["jaune"]*4, "conversion"),
1753+
# Value-mapping like
1754+
([0.5]*3, None), # should emit a warning for user's eyes though
1755+
([0.5]*4, None), # NB: no warning as matching size allows mapping
1756+
([0.5]*5, "shape"),
1757+
# RGB values
1758+
([[1, 0, 0]], None),
1759+
([[1, 0, 0]]*3, "shape"),
1760+
([[1, 0, 0]]*4, None),
1761+
([[1, 0, 0]]*5, "shape"),
1762+
# RGBA values
1763+
([[1, 0, 0, 0.5]], None),
1764+
([[1, 0, 0, 0.5]]*3, "shape"),
1765+
([[1, 0, 0, 0.5]]*4, None),
1766+
([[1, 0, 0, 0.5]]*5, "shape"),
1767+
# Mix of valid color specs
1768+
([[1, 0, 0, 0.5]]*3 + [[1, 0, 0]], None),
1769+
([[1, 0, 0, 0.5], "red", "0.0"], "shape"),
1770+
([[1, 0, 0, 0.5], "red", "0.0", "C5"], None),
1771+
([[1, 0, 0, 0.5], "red", "0.0", "C5", [0, 1, 0]], "shape"),
1772+
# Mix of valid and non valid color specs
1773+
([[1, 0, 0, 0.5], "red", "jaune"], "conversion"),
1774+
([[1, 0, 0, 0.5], "red", "0.0", "jaune"], "conversion"),
1775+
([[1, 0, 0, 0.5], "red", "0.0", "C5", "jaune"], "conversion"),
1776+
]
17281777

1778+
@pytest.mark.parametrize('c_case, re_key', params_test_scatter_c)
1779+
def test_scatter_c(self, c_case, re_key):
1780+
# Additional checking of *c* (introduced in #11383).
1781+
REGEXP = {
1782+
"shape": "^'c' argument has [0-9]+ elements", # shape mismatch
1783+
"conversion": "^'c' argument must either be valid", # bad vals
1784+
}
1785+
x = y = [0, 1, 2, 3]
1786+
fig, ax = plt.subplots()
17291787

1730-
def test_scatter_color():
1731-
# Try to catch cases where 'c' kwarg should have been used.
1732-
with pytest.raises(ValueError):
1733-
plt.scatter([1, 2], [1, 2], color=[0.1, 0.2])
1734-
with pytest.raises(ValueError):
1735-
plt.scatter([1, 2, 3], [1, 2, 3], color=[1, 2, 3])
1788+
if re_key is None:
1789+
ax.scatter(x, y, c=c_case, edgecolors="black")
1790+
else:
1791+
with pytest.raises(ValueError, match=REGEXP[re_key]):
1792+
ax.scatter(x, y, c=c_case, edgecolors="black")
17361793

17371794

17381795
def test_as_mpl_axes_api():

0 commit comments

Comments
 (0)