Skip to content

Fix inability to use empty markers in scatter #25593

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

Open
wants to merge 12 commits into
base: main
Choose a base branch
from
15 changes: 15 additions & 0 deletions doc/users/next_whats_new/updated_scatter.rst
Original file line number Diff line number Diff line change
@@ -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()
34 changes: 30 additions & 4 deletions lib/matplotlib/axes/_axes.py
Original file line number Diff line number Diff line change
Expand Up @@ -4327,6 +4327,8 @@

Argument precedence for facecolors:

- kwargs['facecolor'] if 'none'
- kwargs['facecolors'] if 'none'
- c (if not None)
- kwargs['facecolor']
- kwargs['facecolors']
Expand All @@ -4338,6 +4340,7 @@
- kwargs['edgecolor']
- edgecolors (is an explicit kw argument in scatter())
- kwargs['color'] (==kwcolor)
- c (if not None)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this is only true if facecolors is "none". Otherwise I would expect the test case (dict(c='b'), None), from the edited test to instead be (dict(c='b'), np.array([[0, 0, 1, 1]])), similar to the last test case added... I think, at least?

- 'face' if not in classic mode else None

Parameters
Expand Down Expand Up @@ -4371,7 +4374,7 @@
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 @@ -4398,7 +4401,12 @@
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 @@ -4449,6 +4457,8 @@
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 @@ -4467,6 +4477,8 @@
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 @@ -4627,10 +4639,22 @@
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)
Comment on lines +4648 to +4649
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
x, y, s, linewidths = \
cbook._combine_masks(x, y, s, linewidths)
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 = \

Check warning on line 4656 in lib/matplotlib/axes/_axes.py

View check run for this annotation

Codecov / codecov/patch

lib/matplotlib/axes/_axes.py#L4655-L4656

Added lines #L4655 - L4656 were not covered by tests
cbook._combine_masks(x, y, s, colors, linewidths)
Comment on lines +4655 to +4657
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
c = np.ma.masked_invalid(c)
x, y, s, colors, linewidths = \
cbook._combine_masks(x, y, s, colors, linewidths)
c = np.ma.masked_invalid(c)
x, y, s, colors, linewidths = cbook._combine_masks(x, y, s, colors, linewidths)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This also needs a test case that will cover this line.

else:
x, y, s, c, colors, edgecolors, linewidths = \
cbook._combine_masks(
Expand Down Expand Up @@ -4704,7 +4728,9 @@
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))):
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why are we gating on the type here? Will our normalization above ensure other iterables will be converted to numpy arrays?

collection.set_array(c)
collection.set_cmap(cmap)
collection.set_norm(norm)
Expand Down
7 changes: 6 additions & 1 deletion lib/matplotlib/tests/test_axes.py
Original file line number Diff line number Diff line change
Expand Up @@ -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]]))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we need to cover the case of colormapping + empty here as well.

])
def test_parse_scatter_color_args_edgecolors(kwargs, expected_edgecolors):
def get_next_color():
Expand All @@ -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():
Expand Down