Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings

Commit 1b68e69

Browse filesBrowse files
committed
Refactor color parsing of Axes.scatter
1 parent bed61ee commit 1b68e69
Copy full SHA for 1b68e69

File tree

Expand file treeCollapse file tree

2 files changed

+212
-109
lines changed
Filter options
Expand file treeCollapse file tree

2 files changed

+212
-109
lines changed

‎lib/matplotlib/axes/_axes.py

Copy file name to clipboardExpand all lines: lib/matplotlib/axes/_axes.py
+151-108Lines changed: 151 additions & 108 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import itertools
44
import logging
55
import math
6+
import operator
67
from numbers import Number
78
import warnings
89

@@ -4012,6 +4013,150 @@ def dopatch(xs, ys, **kwargs):
40124013
return dict(whiskers=whiskers, caps=caps, boxes=boxes,
40134014
medians=medians, fliers=fliers, means=means)
40144015

4016+
def _parse_scatter_color_args(self, c, edgecolors, kwargs, xshape, yshape):
4017+
"""
4018+
Helper function to process color related arguments of `.Axes.scatter`.
4019+
4020+
Argument precedence for facecolors:
4021+
4022+
- c (if not None)
4023+
- kwargs['facecolors']
4024+
- kwargs['facecolor']
4025+
- kwargs['color'] (==kwcolor)
4026+
- 'b' if in classic mode else next color from color cycle
4027+
4028+
Argument precedence for edgecolors:
4029+
4030+
- edgecolors (is an explicit kw argument in scatter())
4031+
- kwargs['edgecolor']
4032+
- kwargs['color'] (==kwcolor)
4033+
- 'face' if not in classic mode else None
4034+
4035+
Arguments
4036+
---------
4037+
c : color or sequence or sequence of color or None
4038+
See argument description of `.Axes.scatter`.
4039+
edgecolors : color or sequence of color or {'face', 'none'} or None
4040+
See argument description of `.Axes.scatter`.
4041+
kwargs : dict
4042+
Additional kwargs. If these keys exist, we pop and process them:
4043+
'facecolors', 'facecolor', 'edgecolor', 'color'
4044+
Note: The dict is modified by this function.
4045+
xshape, yshape : tuple of int
4046+
The shape of the x and y arrays passed to `.Axes.scatter`.
4047+
4048+
Returns
4049+
-------
4050+
c
4051+
The input *c* if it was not *None*, else some color specification
4052+
derived from the other inputs or defaults.
4053+
colors : array(N, 4) or None
4054+
The facecolors as RGBA values or *None* if a colormap is used.
4055+
edgecolors
4056+
The edgecolor specification.
4057+
4058+
"""
4059+
xsize = functools.reduce(operator.mul, xshape, 1)
4060+
ysize = functools.reduce(operator.mul, yshape, 1)
4061+
4062+
facecolors = kwargs.pop('facecolors', None)
4063+
facecolors = kwargs.pop('facecolor', facecolors)
4064+
edgecolors = kwargs.pop('edgecolor', edgecolors)
4065+
4066+
kwcolor = kwargs.pop('color', None)
4067+
4068+
if kwcolor is not None and c is not None:
4069+
raise ValueError("Supply a 'c' argument or a 'color'"
4070+
" kwarg but not both; they differ but"
4071+
" their functionalities overlap.")
4072+
4073+
if kwcolor is not None:
4074+
try:
4075+
mcolors.to_rgba_array(kwcolor)
4076+
except ValueError:
4077+
raise ValueError("'color' kwarg must be an mpl color"
4078+
" spec or sequence of color specs.\n"
4079+
"For a sequence of values to be color-mapped,"
4080+
" use the 'c' argument instead.")
4081+
if edgecolors is None:
4082+
edgecolors = kwcolor
4083+
if facecolors is None:
4084+
facecolors = kwcolor
4085+
4086+
if edgecolors is None and not rcParams['_internal.classic_mode']:
4087+
edgecolors = 'face'
4088+
4089+
c_was_none = c is None
4090+
if c is None:
4091+
c = (facecolors if facecolors is not None
4092+
else "b" if rcParams['_internal.classic_mode']
4093+
else self._get_patches_for_fill.get_next_color())
4094+
4095+
# After this block, c_array will be None unless
4096+
# c is an array for mapping. The potential ambiguity
4097+
# with a sequence of 3 or 4 numbers is resolved in
4098+
# favor of mapping, not rgb or rgba.
4099+
# Convenience vars to track shape mismatch *and* conversion failures.
4100+
valid_shape = True # will be put to the test!
4101+
n_elem = -1 # used only for (some) exceptions
4102+
4103+
if (c_was_none or
4104+
kwcolor is not None or
4105+
isinstance(c, str) or
4106+
(isinstance(c, collections.abc.Iterable) and
4107+
len(c) > 0 and
4108+
isinstance(cbook.safe_first_element(c), str))):
4109+
c_array = None
4110+
else:
4111+
try: # First, does 'c' look suitable for value-mapping?
4112+
c_array = np.asanyarray(c, dtype=float)
4113+
n_elem = c_array.shape[0]
4114+
if c_array.shape in [xshape, yshape]:
4115+
c = np.ma.ravel(c_array)
4116+
else:
4117+
if c_array.shape in ((3,), (4,)):
4118+
_log.warning(
4119+
"'c' argument looks like a single numeric RGB or "
4120+
"RGBA sequence, which should be avoided as value-"
4121+
"mapping will have precedence in case its length "
4122+
"matches with 'x' & 'y'. Please use a 2-D array "
4123+
"with a single row if you really want to specify "
4124+
"the same RGB or RGBA value for all points.")
4125+
# Wrong size; it must not be intended for mapping.
4126+
valid_shape = False
4127+
c_array = None
4128+
except ValueError:
4129+
# Failed to make a floating-point array; c must be color specs.
4130+
c_array = None
4131+
if c_array is None:
4132+
try: # Then is 'c' acceptable as PathCollection facecolors?
4133+
colors = mcolors.to_rgba_array(c)
4134+
n_elem = colors.shape[0]
4135+
if colors.shape[0] not in (0, 1, xsize, ysize):
4136+
# NB: remember that a single color is also acceptable.
4137+
# Besides *colors* will be an empty array if c == 'none'.
4138+
valid_shape = False
4139+
raise ValueError
4140+
except ValueError:
4141+
if not valid_shape: # but at least one conversion succeeded.
4142+
raise ValueError(
4143+
"'c' argument has {nc} elements, which is not "
4144+
"acceptable for use with 'x' with size {xs}, "
4145+
"'y' with size {ys}."
4146+
.format(nc=n_elem, xs=xsize, ys=ysize)
4147+
)
4148+
else:
4149+
# Both the mapping *and* the RGBA conversion failed: pretty
4150+
# severe failure => one may appreciate a verbose feedback.
4151+
raise ValueError(
4152+
"'c' argument must be a mpl color, a sequence of mpl "
4153+
"colors or a sequence of numbers, not {}."
4154+
.format(c) # note: could be long depending on c
4155+
)
4156+
else:
4157+
colors = None # use cmap, norm after collection is created
4158+
return c, colors, edgecolors
4159+
40154160
@_preprocess_data(replace_names=["x", "y", "s", "linewidths",
40164161
"edgecolors", "c", "facecolor",
40174162
"facecolors", "color"],
@@ -4125,129 +4270,27 @@ def scatter(self, x, y, s=None, c=None, marker=None, cmap=None, norm=None,
41254270
41264271
"""
41274272
# Process **kwargs to handle aliases, conflicts with explicit kwargs:
4128-
facecolors = None
4129-
edgecolors = kwargs.pop('edgecolor', edgecolors)
4130-
fc = kwargs.pop('facecolors', None)
4131-
fc = kwargs.pop('facecolor', fc)
4132-
if fc is not None:
4133-
facecolors = fc
4134-
co = kwargs.pop('color', None)
4135-
if co is not None:
4136-
try:
4137-
mcolors.to_rgba_array(co)
4138-
except ValueError:
4139-
raise ValueError("'color' kwarg must be an mpl color"
4140-
" spec or sequence of color specs.\n"
4141-
"For a sequence of values to be color-mapped,"
4142-
" use the 'c' argument instead.")
4143-
if edgecolors is None:
4144-
edgecolors = co
4145-
if facecolors is None:
4146-
facecolors = co
4147-
if c is not None:
4148-
raise ValueError("Supply a 'c' argument or a 'color'"
4149-
" kwarg but not both; they differ but"
4150-
" their functionalities overlap.")
4151-
if c is None:
4152-
if facecolors is not None:
4153-
c = facecolors
4154-
else:
4155-
if rcParams['_internal.classic_mode']:
4156-
c = 'b' # The original default
4157-
else:
4158-
c = self._get_patches_for_fill.get_next_color()
4159-
c_none = True
4160-
else:
4161-
c_none = False
4162-
4163-
if edgecolors is None and not rcParams['_internal.classic_mode']:
4164-
edgecolors = 'face'
41654273

41664274
self._process_unit_info(xdata=x, ydata=y, kwargs=kwargs)
41674275
x = self.convert_xunits(x)
41684276
y = self.convert_yunits(y)
41694277

41704278
# np.ma.ravel yields an ndarray, not a masked array,
41714279
# unless its argument is a masked array.
4172-
xy_shape = (np.shape(x), np.shape(y))
4280+
xshape, yshape = np.shape(x), np.shape(y)
41734281
x = np.ma.ravel(x)
41744282
y = np.ma.ravel(y)
41754283
if x.size != y.size:
41764284
raise ValueError("x and y must be the same size")
41774285

41784286
if s is None:
4179-
if rcParams['_internal.classic_mode']:
4180-
s = 20
4181-
else:
4182-
s = rcParams['lines.markersize'] ** 2.0
4183-
4287+
s = (20 if rcParams['_internal.classic_mode'] else
4288+
rcParams['lines.markersize'] ** 2.0)
41844289
s = np.ma.ravel(s) # This doesn't have to match x, y in size.
41854290

4186-
# After this block, c_array will be None unless
4187-
# c is an array for mapping. The potential ambiguity
4188-
# with a sequence of 3 or 4 numbers is resolved in
4189-
# favor of mapping, not rgb or rgba.
4190-
4191-
# Convenience vars to track shape mismatch *and* conversion failures.
4192-
valid_shape = True # will be put to the test!
4193-
n_elem = -1 # used only for (some) exceptions
4194-
4195-
if (c_none or
4196-
co is not None or
4197-
isinstance(c, str) or
4198-
(isinstance(c, collections.Iterable) and
4199-
len(c) > 0 and
4200-
isinstance(cbook.safe_first_element(c), str))):
4201-
c_array = None
4202-
else:
4203-
try: # First, does 'c' look suitable for value-mapping?
4204-
c_array = np.asanyarray(c, dtype=float)
4205-
n_elem = c_array.shape[0]
4206-
if c_array.shape in xy_shape:
4207-
c = np.ma.ravel(c_array)
4208-
else:
4209-
if c_array.shape in ((3,), (4,)):
4210-
_log.warning(
4211-
"'c' argument looks like a single numeric RGB or "
4212-
"RGBA sequence, which should be avoided as value-"
4213-
"mapping will have precedence in case its length "
4214-
"matches with 'x' & 'y'. Please use a 2-D array "
4215-
"with a single row if you really want to specify "
4216-
"the same RGB or RGBA value for all points.")
4217-
# Wrong size; it must not be intended for mapping.
4218-
valid_shape = False
4219-
c_array = None
4220-
except ValueError:
4221-
# Failed to make a floating-point array; c must be color specs.
4222-
c_array = None
4223-
4224-
if c_array is None:
4225-
try: # Then is 'c' acceptable as PathCollection facecolors?
4226-
colors = mcolors.to_rgba_array(c)
4227-
n_elem = colors.shape[0]
4228-
if colors.shape[0] not in (0, 1, x.size, y.size):
4229-
# NB: remember that a single color is also acceptable.
4230-
# Besides *colors* will be an empty array if c == 'none'.
4231-
valid_shape = False
4232-
raise ValueError
4233-
except ValueError:
4234-
if not valid_shape: # but at least one conversion succeeded.
4235-
raise ValueError(
4236-
"'c' argument has {nc} elements, which is not "
4237-
"acceptable for use with 'x' with size {xs}, "
4238-
"'y' with size {ys}."
4239-
.format(nc=n_elem, xs=x.size, ys=y.size)
4240-
)
4241-
# Both the mapping *and* the RGBA conversion failed: pretty
4242-
# severe failure => one may appreciate a verbose feedback.
4243-
raise ValueError(
4244-
"'c' argument must either be valid as mpl color(s) "
4245-
"or as numbers to be mapped to colors. "
4246-
"Here c = {}." # <- beware, could be long depending on c.
4247-
.format(c)
4248-
)
4249-
else:
4250-
colors = None # use cmap, norm after collection is created
4291+
c, colors, edgecolors = \
4292+
self._parse_scatter_color_args(c, edgecolors, kwargs,
4293+
xshape, yshape)
42514294

42524295
# `delete_masked_points` only modifies arguments of the same length as
42534296
# `x`.

‎lib/matplotlib/tests/test_axes.py

Copy file name to clipboardExpand all lines: lib/matplotlib/tests/test_axes.py
+61-1Lines changed: 61 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
from collections import namedtuple
12
from itertools import product
23
from distutils.version import LooseVersion
34
import io
@@ -1795,7 +1796,7 @@ def test_scatter_c(self, c_case, re_key):
17951796
# Additional checking of *c* (introduced in #11383).
17961797
REGEXP = {
17971798
"shape": "^'c' argument has [0-9]+ elements", # shape mismatch
1798-
"conversion": "^'c' argument must either be valid", # bad vals
1799+
"conversion": "^'c' argument must be a mpl color", # bad vals
17991800
}
18001801
x = y = [0, 1, 2, 3]
18011802
fig, ax = plt.subplots()
@@ -1807,6 +1808,65 @@ def test_scatter_c(self, c_case, re_key):
18071808
ax.scatter(x, y, c=c_case, edgecolors="black")
18081809

18091810

1811+
def _params(c=None, xshape=(2,), yshape=(2,), **kwargs):
1812+
edgecolors = kwargs.pop('edgecolors', None)
1813+
return (c, edgecolors, kwargs if kwargs is not None else {},
1814+
xshape, yshape)
1815+
_result = namedtuple('_result', 'c, colors')
1816+
1817+
1818+
@pytest.mark.parametrize('params, expected_result',
1819+
[(_params(),
1820+
_result(c='b', colors=np.array([[0, 0, 1, 1]]))),
1821+
(_params(c='r'),
1822+
_result(c='r', colors=np.array([[1, 0, 0, 1]]))),
1823+
(_params(c='r', colors='b'),
1824+
_result(c='r', colors=np.array([[1, 0, 0, 1]]))),
1825+
# color
1826+
(_params(color='b'),
1827+
_result(c='b', colors=np.array([[0, 0, 1, 1]]))),
1828+
(_params(color=['b', 'g']),
1829+
_result(c=['b', 'g'], colors=np.array([[0, 0, 1, 1], [0, .5, 0, 1]]))),
1830+
])
1831+
def test_parse_scatter_color_args(params, expected_result):
1832+
from matplotlib.axes import Axes
1833+
dummyself = 'UNUSED' # self is only used in one case, which we do not
1834+
# test. Therefore we can get away without costly
1835+
# creating an Axes instance.
1836+
c, colors, _edgecolors = Axes._parse_scatter_color_args(dummyself, *params)
1837+
assert c == expected_result.c
1838+
assert_allclose(colors, expected_result.colors)
1839+
1840+
del _params
1841+
del _result
1842+
1843+
1844+
@pytest.mark.parametrize('kwargs, expected_edgecolors',
1845+
[(dict(), None),
1846+
(dict(c='b'), None),
1847+
(dict(edgecolors='r'), 'r'),
1848+
(dict(edgecolors=['r', 'g']), ['r', 'g']),
1849+
(dict(edgecolor='r'), 'r'),
1850+
(dict(edgecolors='face'), 'face'),
1851+
(dict(edgecolors='none'), 'none'),
1852+
(dict(edgecolor='r', edgecolors='g'), 'r'),
1853+
(dict(c='b', edgecolor='r', edgecolors='g'), 'r'),
1854+
(dict(color='r'), 'r'),
1855+
(dict(color='r', edgecolor='g'), 'g'),
1856+
])
1857+
def test_parse_scatter_color_args_edgecolors(kwargs, expected_edgecolors):
1858+
from matplotlib.axes import Axes
1859+
dummyself = 'UNUSED' # self is only used in one case, which we do not
1860+
# test. Therefore we can get away without costly
1861+
# creating an Axes instance.
1862+
c = kwargs.pop('c', None)
1863+
edgecolors = kwargs.pop('edgecolors', None)
1864+
_, _, result_edgecolors = \
1865+
Axes._parse_scatter_color_args(dummyself, c, edgecolors, kwargs,
1866+
xshape=(2,), yshape=(2,))
1867+
assert result_edgecolors == expected_edgecolors
1868+
1869+
18101870
def test_as_mpl_axes_api():
18111871
# tests the _as_mpl_axes api
18121872
from matplotlib.projections.polar import PolarAxes

0 commit comments

Comments
0 (0)
Morty Proxy This is a proxified and sanitized view of the page, visit original site.