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 07c1c62

Browse filesBrowse files
committed
Refactor color parsing of Axes.scatter
1 parent cb0135c commit 07c1c62
Copy full SHA for 07c1c62

File tree

Expand file treeCollapse file tree

2 files changed

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

2 files changed

+212
-107
lines changed

‎lib/matplotlib/axes/_axes.py

Copy file name to clipboardExpand all lines: lib/matplotlib/axes/_axes.py
+152-107Lines changed: 152 additions & 107 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

@@ -4004,6 +4005,151 @@ def dopatch(xs, ys, **kwargs):
40044005
return dict(whiskers=whiskers, caps=caps, boxes=boxes,
40054006
medians=medians, fliers=fliers, means=means)
40064007

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

41584267
self._process_unit_info(xdata=x, ydata=y, kwargs=kwargs)
41594268
x = self.convert_xunits(x)
41604269
y = self.convert_yunits(y)
41614270

41624271
# np.ma.ravel yields an ndarray, not a masked array,
41634272
# unless its argument is a masked array.
4164-
xy_shape = (np.shape(x), np.shape(y))
4273+
xshape, yshape = np.shape(x), np.shape(y)
41654274
x = np.ma.ravel(x)
41664275
y = np.ma.ravel(y)
41674276
if x.size != y.size:
41684277
raise ValueError("x and y must be the same size")
41694278

41704279
if s is None:
4171-
if rcParams['_internal.classic_mode']:
4172-
s = 20
4173-
else:
4174-
s = rcParams['lines.markersize'] ** 2.0
4175-
4280+
s = (20 if rcParams['_internal.classic_mode'] else
4281+
rcParams['lines.markersize'] ** 2.0)
41764282
s = np.ma.ravel(s) # This doesn't have to match x, y in size.
41774283

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

42434288
# `delete_masked_points` only modifies arguments of the same length as
42444289
# `x`.

‎lib/matplotlib/tests/test_axes.py

Copy file name to clipboardExpand all lines: lib/matplotlib/tests/test_axes.py
+60Lines changed: 60 additions & 0 deletions
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
@@ -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.