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 b4bc63b

Browse filesBrowse files
committed
Refactor color parsing of Axes.scatter
1 parent 677a3b2 commit b4bc63b
Copy full SHA for b4bc63b

File tree

Expand file treeCollapse file tree

2 files changed

+205
-103
lines changed
Filter options
Expand file treeCollapse file tree

2 files changed

+205
-103
lines changed

‎lib/matplotlib/axes/_axes.py

Copy file name to clipboardExpand all lines: lib/matplotlib/axes/_axes.py
+147-103Lines changed: 147 additions & 103 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
import itertools
33
import logging
44
import math
5+
import operator
56
from numbers import Number
67
import warnings
78

@@ -3760,6 +3761,146 @@ def dopatch(xs, ys, **kwargs):
37603761
return dict(whiskers=whiskers, caps=caps, boxes=boxes,
37613762
medians=medians, fliers=fliers, means=means)
37623763

3764+
def _parse_scatter_color_args(self, c, edgecolors, kwargs, xshape, yshape):
3765+
"""
3766+
Helper function to process color related arguments of `.Axes.scatter`.
3767+
3768+
Argument precedence for facecolors:
3769+
3770+
- c (if not None)
3771+
- kwargs['facecolors']
3772+
- kwargs['facecolor']
3773+
- kwargs['color'] (==kwcolor)
3774+
- 'b' if in classic mode else next color from color cycle
3775+
3776+
Argument precedence for edgecolors:
3777+
3778+
- edgecolors (is an explicit kw argument in scatter())
3779+
- kwargs['edgecolor']
3780+
- kwargs['color'] (==kwcolor)
3781+
- 'face' if not in classic mode else None
3782+
3783+
Arguments
3784+
---------
3785+
c : color or sequence or sequence of color or None
3786+
See argument description of `.Axes.scatter`.
3787+
edgecolors : color or sequence of color or {'face', 'none'} or None
3788+
See argument description of `.Axes.scatter`.
3789+
kwargs : dict
3790+
Additional kwargs. If these keys exist, we pop and process them:
3791+
'facecolors', 'facecolor', 'edgecolor', 'color'
3792+
Note: The dict is modified by this function.
3793+
xshape, yshape : tuple of int
3794+
The shape of the x and y arrays passed to `.Axes.scatter`.
3795+
3796+
Returns
3797+
-------
3798+
c
3799+
The input *c* if it was not *None*, else some color specification
3800+
derived from the other inputs or defaults.
3801+
colors : array(N, 4) or None
3802+
The facecolors as RGBA values or *None* if a colormap is used.
3803+
edgecolors
3804+
The edgecolor specification.
3805+
3806+
"""
3807+
xsize = functools.reduce(operator.mul, xshape, 1)
3808+
ysize = functools.reduce(operator.mul, yshape, 1)
3809+
3810+
facecolors = kwargs.pop('facecolors', None)
3811+
facecolors = kwargs.pop('facecolor', facecolors)
3812+
edgecolors = kwargs.pop('edgecolor', edgecolors)
3813+
3814+
kwcolor = kwargs.pop('color', None)
3815+
3816+
if kwcolor is not None and c is not None:
3817+
raise ValueError("Supply a 'c' argument or a 'color'"
3818+
" kwarg but not both; they differ but"
3819+
" their functionalities overlap.")
3820+
3821+
if kwcolor is not None:
3822+
try:
3823+
mcolors.to_rgba_array(kwcolor)
3824+
except ValueError:
3825+
raise ValueError("'color' kwarg must be an mpl color"
3826+
" spec or sequence of color specs.\n"
3827+
"For a sequence of values to be color-mapped,"
3828+
" use the 'c' argument instead.")
3829+
if edgecolors is None:
3830+
edgecolors = kwcolor
3831+
if facecolors is None:
3832+
facecolors = kwcolor
3833+
3834+
if edgecolors is None and not rcParams['_internal.classic_mode']:
3835+
edgecolors = 'face'
3836+
3837+
c_is_none = c is None
3838+
if c is None:
3839+
if facecolors is not None:
3840+
c = facecolors
3841+
else:
3842+
c = ('b' if rcParams['_internal.classic_mode'] else
3843+
self._get_patches_for_fill.get_next_color())
3844+
3845+
# After this block, c_array will be None unless
3846+
# c is an array for mapping. The potential ambiguity
3847+
# with a sequence of 3 or 4 numbers is resolved in
3848+
# favor of mapping, not rgb or rgba.
3849+
# Convenience vars to track shape mismatch *and* conversion failures.
3850+
valid_shape = True # will be put to the test!
3851+
n_elem = -1 # used only for (some) exceptions
3852+
if c_is_none or kwcolor is not None:
3853+
c_array = None
3854+
else:
3855+
try: # First, does 'c' look suitable for value-mapping?
3856+
c_array = np.asanyarray(c, dtype=float)
3857+
n_elem = c_array.shape[0]
3858+
if c_array.shape in [xshape, yshape]:
3859+
c = np.ma.ravel(c_array)
3860+
else:
3861+
if c_array.shape in ((3,), (4,)):
3862+
_log.warning(
3863+
"'c' argument looks like a single numeric RGB or "
3864+
"RGBA sequence, which should be avoided as value-"
3865+
"mapping will have precedence in case its length "
3866+
"matches with 'x' & 'y'. Please use a 2-D array "
3867+
"with a single row if you really want to specify "
3868+
"the same RGB or RGBA value for all points.")
3869+
# Wrong size; it must not be intended for mapping.
3870+
valid_shape = False
3871+
c_array = None
3872+
except ValueError:
3873+
# Failed to make a floating-point array; c must be color specs.
3874+
c_array = None
3875+
if c_array is None:
3876+
try: # Then is 'c' acceptable as PathCollection facecolors?
3877+
colors = mcolors.to_rgba_array(c)
3878+
n_elem = colors.shape[0]
3879+
if colors.shape[0] not in (0, 1, xsize, ysize):
3880+
# NB: remember that a single color is also acceptable.
3881+
# Besides *colors* will be an empty array if c == 'none'.
3882+
valid_shape = False
3883+
raise ValueError
3884+
except ValueError:
3885+
if not valid_shape: # but at least one conversion succeeded.
3886+
raise ValueError(
3887+
"'c' argument has {nc} elements, which is not "
3888+
"acceptable for use with 'x' with size {xs}, "
3889+
"'y' with size {ys}."
3890+
.format(nc=n_elem, xs=xsize, ys=ysize)
3891+
)
3892+
# Both the mapping *and* the RGBA conversion failed: pretty
3893+
# severe failure => one may appreciate a verbose feedback.
3894+
raise ValueError(
3895+
"'c' argument must either be valid as mpl color(s) "
3896+
"or as numbers to be mapped to colors. "
3897+
"Here c = {}." # <- beware, could be long depending on c.
3898+
.format(c)
3899+
)
3900+
else:
3901+
colors = None # use cmap, norm after collection is created
3902+
return c, colors, edgecolors
3903+
37633904
@_preprocess_data(replace_names=["x", "y", "s", "linewidths",
37643905
"edgecolors", "c", "facecolor",
37653906
"facecolors", "color"],
@@ -3865,124 +4006,27 @@ def scatter(self, x, y, s=None, c=None, marker=None, cmap=None, norm=None,
38654006
38664007
"""
38674008
# Process **kwargs to handle aliases, conflicts with explicit kwargs:
3868-
facecolors = None
3869-
edgecolors = kwargs.pop('edgecolor', edgecolors)
3870-
fc = kwargs.pop('facecolors', None)
3871-
fc = kwargs.pop('facecolor', fc)
3872-
if fc is not None:
3873-
facecolors = fc
3874-
co = kwargs.pop('color', None)
3875-
if co is not None:
3876-
try:
3877-
mcolors.to_rgba_array(co)
3878-
except ValueError:
3879-
raise ValueError("'color' kwarg must be an mpl color"
3880-
" spec or sequence of color specs.\n"
3881-
"For a sequence of values to be color-mapped,"
3882-
" use the 'c' argument instead.")
3883-
if edgecolors is None:
3884-
edgecolors = co
3885-
if facecolors is None:
3886-
facecolors = co
3887-
if c is not None:
3888-
raise ValueError("Supply a 'c' argument or a 'color'"
3889-
" kwarg but not both; they differ but"
3890-
" their functionalities overlap.")
3891-
if c is None:
3892-
if facecolors is not None:
3893-
c = facecolors
3894-
else:
3895-
if rcParams['_internal.classic_mode']:
3896-
c = 'b' # The original default
3897-
else:
3898-
c = self._get_patches_for_fill.get_next_color()
3899-
c_none = True
3900-
else:
3901-
c_none = False
3902-
3903-
if edgecolors is None and not rcParams['_internal.classic_mode']:
3904-
edgecolors = 'face'
39054009

39064010
self._process_unit_info(xdata=x, ydata=y, kwargs=kwargs)
39074011
x = self.convert_xunits(x)
39084012
y = self.convert_yunits(y)
39094013

39104014
# np.ma.ravel yields an ndarray, not a masked array,
39114015
# unless its argument is a masked array.
3912-
xy_shape = (np.shape(x), np.shape(y))
4016+
xshape, yshape = np.shape(x), np.shape(y)
39134017
x = np.ma.ravel(x)
39144018
y = np.ma.ravel(y)
39154019
if x.size != y.size:
39164020
raise ValueError("x and y must be the same size")
39174021

39184022
if s is None:
3919-
if rcParams['_internal.classic_mode']:
3920-
s = 20
3921-
else:
3922-
s = rcParams['lines.markersize'] ** 2.0
3923-
4023+
s = (20 if rcParams['_internal.classic_mode'] else
4024+
rcParams['lines.markersize'] ** 2.0)
39244025
s = np.ma.ravel(s) # This doesn't have to match x, y in size.
39254026

3926-
# After this block, c_array will be None unless
3927-
# c is an array for mapping. The potential ambiguity
3928-
# with a sequence of 3 or 4 numbers is resolved in
3929-
# 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-
3935-
if c_none or co is not None:
3936-
c_array = None
3937-
else:
3938-
try: # First, does 'c' look suitable for value-mapping?
3939-
c_array = np.asanyarray(c, dtype=float)
3940-
n_elem = c_array.shape[0]
3941-
if c_array.shape in xy_shape:
3942-
c = np.ma.ravel(c_array)
3943-
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.")
3952-
# Wrong size; it must not be intended for mapping.
3953-
valid_shape = False
3954-
c_array = None
3955-
except ValueError:
3956-
# Failed to make a floating-point array; c must be color specs.
3957-
c_array = None
3958-
3959-
if c_array is None:
3960-
try: # Then is 'c' acceptable as PathCollection facecolors?
3961-
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
3968-
except ValueError:
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-
)
3984-
else:
3985-
colors = None # use cmap, norm after collection is created
4027+
c, colors, edgecolors = \
4028+
self._parse_scatter_color_args(c, edgecolors, kwargs,
4029+
xshape, yshape)
39864030

39874031
# `delete_masked_points` only modifies arguments of the same length as
39884032
# `x`.

‎lib/matplotlib/tests/test_axes.py

Copy file name to clipboardExpand all lines: lib/matplotlib/tests/test_axes.py
+58Lines changed: 58 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
@@ -1792,6 +1793,63 @@ def test_scatter_c(self, c_case, re_key):
17921793
ax.scatter(x, y, c=c_case, edgecolors="black")
17931794

17941795

1796+
def _params(c=None, xshape=(2,), yshape=(2,), **kwargs):
1797+
edgecolors = kwargs.pop('edgecolors', None)
1798+
return (c, edgecolors, kwargs if kwargs is not None else {},
1799+
xshape, yshape)
1800+
_result = namedtuple('_result', 'c, colors')
1801+
@pytest.mark.parametrize('params, expected_result',
1802+
[(_params(),
1803+
_result(c='b', colors=np.array([[0, 0, 1, 1]]))),
1804+
(_params(c='r'),
1805+
_result(c='r', colors=np.array([[1, 0, 0, 1]]))),
1806+
(_params(c='r', colors='b'),
1807+
_result(c='r', colors=np.array([[1, 0, 0, 1]]))),
1808+
# color
1809+
(_params(color='b'),
1810+
_result(c='b', colors=np.array([[0, 0, 1, 1]]))),
1811+
(_params(color=['b', 'g']),
1812+
_result(c=['b', 'g'], colors=np.array([[0, 0, 1, 1], [0, .5, 0, 1]]))),
1813+
])
1814+
def test_parse_scatter_color_args(params, expected_result):
1815+
from matplotlib.axes import Axes
1816+
dummyself = 'UNUSED' # self is only used in one case, which we do not
1817+
# test. Therefore we can get away without costly
1818+
# creating an Axes instance.
1819+
c, colors, _edgecolors = Axes._parse_scatter_color_args(dummyself, *params)
1820+
assert c == expected_result.c
1821+
assert_allclose(colors, expected_result.colors)
1822+
1823+
del _params
1824+
del _result
1825+
1826+
1827+
@pytest.mark.parametrize('kwargs, expected_edgecolors',
1828+
[(dict(), None),
1829+
(dict(c='b'), None),
1830+
(dict(edgecolors='r'), 'r'),
1831+
(dict(edgecolors=['r', 'g']), ['r', 'g']),
1832+
(dict(edgecolor='r'), 'r'),
1833+
(dict(edgecolors='face'), 'face'),
1834+
(dict(edgecolors='none'), 'none'),
1835+
(dict(edgecolor='r', edgecolors='g'), 'r'),
1836+
(dict(c='b', edgecolor='r', edgecolors='g'), 'r'),
1837+
(dict(color='r'), 'r'),
1838+
(dict(color='r', edgecolor='g'), 'g'),
1839+
])
1840+
def test_parse_scatter_color_args_edgecolors(kwargs, expected_edgecolors):
1841+
from matplotlib.axes import Axes
1842+
dummyself = 'UNUSED' # self is only used in one case, which we do not
1843+
# test. Therefore we can get away without costly
1844+
# creating an Axes instance.
1845+
c = kwargs.pop('c', None)
1846+
edgecolors = kwargs.pop('edgecolors', None)
1847+
_, _, result_edgecolors = \
1848+
Axes._parse_scatter_color_args(dummyself, c, edgecolors, kwargs,
1849+
xshape=(2,), yshape=(2,))
1850+
assert result_edgecolors == expected_edgecolors
1851+
1852+
17951853
def test_as_mpl_axes_api():
17961854
# tests the _as_mpl_axes api
17971855
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.