Skip to content

Fixes #25593, addresses comments and updates what's new #25749

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 13 commits into from
19 changes: 19 additions & 0 deletions doc/users/next_whats_new/updated_scatter.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
Enable configuration of empty markers in `~matplotlib.axes.Axes.scatter`
------------------------------------------------------------------------

`~matplotlib.axes.Axes.scatter` can now be configured to plot empty markers
without additional code. Setting ``facecolors`` to *'none'* and
defining ``c`` now draws only the edge colors for fillable markers. This
allows empty face colors and non-empty edge colors without disrupting
existing color-mapping capabilities.

.. 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()
34 changes: 30 additions & 4 deletions lib/matplotlib/axes/_axes.py
Original file line number Diff line number Diff line change
Expand Up @@ -4313,6 +4313,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']
Expand All @@ -4324,6 +4326,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
Expand Down Expand Up @@ -4357,7 +4360,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)
Expand All @@ -4384,7 +4387,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
Expand Down Expand Up @@ -4435,6 +4443,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
Expand All @@ -4453,6 +4463,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",
Expand Down Expand Up @@ -4613,10 +4625,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(
Expand Down Expand Up @@ -4690,7 +4714,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)
Expand Down
22 changes: 21 additions & 1 deletion lib/matplotlib/tests/test_axes.py
Original file line number Diff line number Diff line change
Expand Up @@ -2578,6 +2578,21 @@ def test_scatter_no_invalid_color(self, fig_test, fig_ref):
ax = fig_ref.subplots()
ax.scatter([0, 2], [0, 2], c=[1, 2], s=[1, 3], cmap=cmap)

@check_figures_equal(extensions=["png"])
def test_scatter_plotnonfinite_edgecolornone(self, fig_test, fig_ref):
ax = fig_test.subplots()
cmap = mpl.colormaps["viridis"].resampled(16)
cmap.set_bad("k", 1)
# Squelches warning in #25593 by testing a
# nonfinite plot with edgecolors set to None
ax.scatter(range(4), range(4),
c=[1, np.nan, 2, np.nan], s=[1, 2, 3, 4],
cmap=cmap, edgecolors=None, plotnonfinite=True)
ax = fig_ref.subplots()
cmap = mpl.colormaps["viridis"].resampled(16)
ax.scatter([0, 2], [0, 2], c=[1, 2], s=[1, 3], cmap=cmap)
ax.scatter([1, 3], [1, 3], s=[2, 4], color="k")

def test_scatter_norm_vminvmax(self):
"""Parameters vmin, vmax should error if norm is given."""
x = [1, 2, 3]
Expand Down Expand Up @@ -2749,6 +2764,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():
Expand All @@ -2759,7 +2776,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():
Expand Down