diff --git a/doc/users/next_whats_new/updated_scatter.rst b/doc/users/next_whats_new/updated_scatter.rst new file mode 100644 index 000000000000..d0b72b87c5b4 --- /dev/null +++ b/doc/users/next_whats_new/updated_scatter.rst @@ -0,0 +1,15 @@ +Enable configuration of empty markers in `~matplotlib.axes.Axes.scatter` +------------------------------------------------------------------------ + +`~matplotlib.axes.Axes.scatter` can now be configured to plot empty markers by setting ``facecolors`` to *'none'* and defining ``c``. In this case, ``c`` will be now used as ``edgecolor``. + +.. plot:: + :include-source: true + :alt: A simple scatter plot which illustrates the use of the *scatter* function to plot empty markers. + + import numpy as np + import matplotlib.pyplot as plt + + x = np.arange(0, 10) + plt.scatter(x, x, c=x, facecolors='none', marker='o') + plt.show() diff --git a/lib/matplotlib/axes/_axes.py b/lib/matplotlib/axes/_axes.py index 91aba7c1bdac..0bbf679785ed 100644 --- a/lib/matplotlib/axes/_axes.py +++ b/lib/matplotlib/axes/_axes.py @@ -4327,6 +4327,8 @@ def _parse_scatter_color_args(c, edgecolors, kwargs, xsize, Argument precedence for facecolors: + - kwargs['facecolor'] if 'none' + - kwargs['facecolors'] if 'none' - c (if not None) - kwargs['facecolor'] - kwargs['facecolors'] @@ -4338,6 +4340,7 @@ def _parse_scatter_color_args(c, edgecolors, kwargs, xsize, - kwargs['edgecolor'] - edgecolors (is an explicit kw argument in scatter()) - kwargs['color'] (==kwcolor) + - c (if not None) - 'face' if not in classic mode else None Parameters @@ -4371,7 +4374,7 @@ def _parse_scatter_color_args(c, edgecolors, kwargs, xsize, colors : array(N, 4) or None The facecolors as RGBA values, or *None* if a colormap is used. edgecolors - The edgecolor. + The edgecolor, or *None* if a colormap is used. """ facecolors = kwargs.pop('facecolors', None) @@ -4398,7 +4401,12 @@ def _parse_scatter_color_args(c, edgecolors, kwargs, xsize, if facecolors is None: facecolors = kwcolor - if edgecolors is None and not mpl.rcParams['_internal.classic_mode']: + edge_from_c = edgecolors is None and c is not None + + facecolors_none = cbook._str_lower_equal(facecolors, 'none') + if (edgecolors is None and not mpl.rcParams['_internal.classic_mode'] + and (not edge_from_c or not facecolors_none)): + edgecolors = mpl.rcParams['scatter.edgecolors'] c_was_none = c is None @@ -4449,6 +4457,8 @@ def invalid_shape_exception(csize, xsize): if not c_is_mapped: try: # Is 'c' acceptable as PathCollection facecolors? colors = mcolors.to_rgba_array(c) + if edge_from_c and facecolors is not None: + edgecolors = mcolors.to_rgba_array(c) except (TypeError, ValueError) as err: if "RGBA values should be within 0-1 range" in str(err): raise @@ -4467,6 +4477,8 @@ def invalid_shape_exception(csize, xsize): raise invalid_shape_exception(len(colors), xsize) else: colors = None # use cmap, norm after collection is created + if cbook._str_lower_equal(facecolors, 'none'): + colors = facecolors return c, colors, edgecolors @_preprocess_data(replace_names=["x", "y", "s", "linewidths", @@ -4627,10 +4639,22 @@ def scatter(self, x, y, s=None, c=None, marker=None, cmap=None, norm=None, c, edgecolors, kwargs, x.size, get_next_color_func=self._get_patches_for_fill.get_next_color) - if plotnonfinite and colors is None: + if cmap and norm and not orig_edgecolor: + # override edgecolor based on cmap and norm presence + edgecolors = 'face' + + if plotnonfinite and (colors is None and edgecolors is None): + c = np.ma.masked_invalid(c) + x, y, s, linewidths = \ + cbook._combine_masks(x, y, s, linewidths) + elif plotnonfinite and colors is None: c = np.ma.masked_invalid(c) x, y, s, edgecolors, linewidths = \ cbook._combine_masks(x, y, s, edgecolors, linewidths) + elif plotnonfinite and edgecolors is None: + c = np.ma.masked_invalid(c) + x, y, s, colors, linewidths = \ + cbook._combine_masks(x, y, s, colors, linewidths) else: x, y, s, c, colors, edgecolors, linewidths = \ cbook._combine_masks( @@ -4704,7 +4728,9 @@ def scatter(self, x, y, s=None, c=None, marker=None, cmap=None, norm=None, alpha=alpha, ) collection.set_transform(mtransforms.IdentityTransform()) - if colors is None: + + if ((colors is None or edgecolors is None) and not mcolors.is_color_like(c) and + (not isinstance(c, np.ndarray) or isinstance(c, np.ma.MaskedArray))): collection.set_array(c) collection.set_cmap(cmap) collection.set_norm(norm) diff --git a/lib/matplotlib/tests/test_axes.py b/lib/matplotlib/tests/test_axes.py index 0050f0b9c0ea..ad0b072f60ab 100644 --- a/lib/matplotlib/tests/test_axes.py +++ b/lib/matplotlib/tests/test_axes.py @@ -2749,6 +2749,8 @@ def get_next_color(): (dict(c='b', edgecolor='r', edgecolors='g'), 'r'), (dict(color='r'), 'r'), (dict(color='r', edgecolor='g'), 'g'), + (dict(facecolors='none'), None), + (dict(c='b', facecolors='none'), np.array([[0, 0, 1, 1]])) ]) def test_parse_scatter_color_args_edgecolors(kwargs, expected_edgecolors): def get_next_color(): @@ -2759,7 +2761,10 @@ def get_next_color(): _, _, result_edgecolors = \ mpl.axes.Axes._parse_scatter_color_args( c, edgecolors, kwargs, xsize=2, get_next_color_func=get_next_color) - assert result_edgecolors == expected_edgecolors + if isinstance(expected_edgecolors, np.ndarray): + assert_allclose(result_edgecolors, expected_edgecolors) + else: + assert result_edgecolors == expected_edgecolors def test_parse_scatter_color_args_error():