From 408f55dd5876a55ba72eaa8b101acf9b58811959 Mon Sep 17 00:00:00 2001 From: Antony Lee Date: Wed, 14 Nov 2018 01:32:40 +0100 Subject: [PATCH 01/16] Make check_figures_equal work on methods too. --- lib/matplotlib/testing/decorators.py | 44 ++++++++++++++++++++-------- 1 file changed, 31 insertions(+), 13 deletions(-) diff --git a/lib/matplotlib/testing/decorators.py b/lib/matplotlib/testing/decorators.py index 989cc566bf5f..548a4b54909b 100644 --- a/lib/matplotlib/testing/decorators.py +++ b/lib/matplotlib/testing/decorators.py @@ -452,19 +452,37 @@ def decorator(func): _, result_dir = map(Path, _image_directories(func)) - @pytest.mark.parametrize("ext", extensions) - def wrapper(ext): - fig_test = plt.figure("test") - fig_ref = plt.figure("reference") - func(fig_test, fig_ref) - test_image_path = str( - result_dir / (func.__name__ + "." + ext)) - ref_image_path = str( - result_dir / (func.__name__ + "-expected." + ext)) - fig_test.savefig(test_image_path) - fig_ref.savefig(ref_image_path) - _raise_on_image_difference( - ref_image_path, test_image_path, tol=tol) + if len(inspect.signature(func).parameters) == 2: + # Free-standing function. + @pytest.mark.parametrize("ext", extensions) + def wrapper(ext): + fig_test = plt.figure("test") + fig_ref = plt.figure("reference") + func(fig_test, fig_ref) + test_image_path = str( + result_dir / (func.__name__ + "." + ext)) + ref_image_path = str( + result_dir / (func.__name__ + "-expected." + ext)) + fig_test.savefig(test_image_path) + fig_ref.savefig(ref_image_path) + _raise_on_image_difference( + ref_image_path, test_image_path, tol=tol) + + elif len(inspect.signature(func).parameters) == 3: + # Method. + @pytest.mark.parametrize("ext", extensions) + def wrapper(self, ext): + fig_test = plt.figure("test") + fig_ref = plt.figure("reference") + func(self, fig_test, fig_ref) + test_image_path = str( + result_dir / (func.__name__ + "." + ext)) + ref_image_path = str( + result_dir / (func.__name__ + "-expected." + ext)) + fig_test.savefig(test_image_path) + fig_ref.savefig(ref_image_path) + _raise_on_image_difference( + ref_image_path, test_image_path, tol=tol) return wrapper From 6fa824953fa95f91a035dbf779e39a009ace7e3f Mon Sep 17 00:00:00 2001 From: QiCuiHub Date: Fri, 16 Mar 2018 01:19:57 -0400 Subject: [PATCH 02/16] fix scatter not showing points w/ valid x/y invalid color #4354 and added test --- lib/matplotlib/axes/_axes.py | 24 +++++++++++++++++------- lib/matplotlib/tests/test_axes.py | 16 ++++++++++++++++ 2 files changed, 33 insertions(+), 7 deletions(-) diff --git a/lib/matplotlib/axes/_axes.py b/lib/matplotlib/axes/_axes.py index 8bb104487702..69b169774f71 100644 --- a/lib/matplotlib/axes/_axes.py +++ b/lib/matplotlib/axes/_axes.py @@ -4180,7 +4180,7 @@ def _parse_scatter_color_args(c, edgecolors, kwargs, xshape, yshape, label_namer="y") def scatter(self, x, y, s=None, c=None, marker=None, cmap=None, norm=None, vmin=None, vmax=None, alpha=None, linewidths=None, - verts=None, edgecolors=None, + verts=None, edgecolors=None, masked=False, **kwargs): """ A scatter plot of *y* vs *x* with varying marker size and/or color. @@ -4257,6 +4257,10 @@ def scatter(self, x, y, s=None, c=None, marker=None, cmap=None, norm=None, For non-filled markers, the *edgecolors* kwarg is ignored and forced to 'face' internally. + masked : boolean, optional, default: False + Set to plot valid points with invalid color, in conjunction with + `~matplotlib.colors.Colormap.set_bad`. + Returns ------- paths : `~matplotlib.collections.PathCollection` @@ -4310,11 +4314,12 @@ def scatter(self, x, y, s=None, c=None, marker=None, cmap=None, norm=None, c, edgecolors, kwargs, xshape, yshape, get_next_color_func=self._get_patches_for_fill.get_next_color) - # `delete_masked_points` only modifies arguments of the same length as - # `x`. - x, y, s, c, colors, edgecolors, linewidths =\ - cbook.delete_masked_points( - x, y, s, c, colors, edgecolors, linewidths) + if masked is False: + # `delete_masked_points` only modifies arguments of the same length + # as `x`. + x, y, s, c, colors, edgecolors, linewidths =\ + cbook.delete_masked_points( + x, y, s, c, colors, edgecolors, linewidths) scales = s # Renamed for readability below. @@ -4358,7 +4363,12 @@ def scatter(self, x, y, s=None, c=None, marker=None, cmap=None, norm=None, if norm is not None and not isinstance(norm, mcolors.Normalize): raise ValueError( "'norm' must be an instance of 'mcolors.Normalize'") - collection.set_array(np.asarray(c)) + + if masked is False: + collection.set_array(c) + else: + collection.set_array(ma.masked_invalid(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 f386e2f03881..a171a1e8d2f4 100644 --- a/lib/matplotlib/tests/test_axes.py +++ b/lib/matplotlib/tests/test_axes.py @@ -1749,6 +1749,22 @@ def test_scatter_color(self): with pytest.raises(ValueError): plt.scatter([1, 2, 3], [1, 2, 3], color=[1, 2, 3]) + @check_figures_equal(extensions=["png"]) + def test_scatter_invalid_color(self, fig_test, fig_ref): + ax = fig_test.subplots() + cmap = plt.get_cmap("viridis", 16) + cmap.set_bad("k", 1) + # Set a nonuniform size to prevent the last call to `scatter` (plotting + # the invalid points separately in fig_ref) from using the marker + # stamping fast path, which would result in slightly offset markers. + ax.scatter(range(4), range(4), + c=[1, np.nan, 2, np.nan], s=[1, 2, 3, 4], + cmap=cmap, masked=True) + ax = fig_ref.subplots() + cmap = plt.get_cmap("viridis", 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") + # Parameters for *test_scatter_c*. NB: assuming that the # scatter plot will have 4 elements. The tuple scheme is: # (*c* parameter case, exception regexp key or None if no exception) From 0eed25a9ba8c1290601b3fa697f6bfba1347c6c7 Mon Sep 17 00:00:00 2001 From: btang02 Date: Fri, 16 Mar 2018 02:07:59 -0400 Subject: [PATCH 03/16] Fix for scatter not showing points with valid x/y but invalid color - updated - Fixed ambiguous kwarg to a more appropriate, less ambiguous name -> plotinvalid --- lib/matplotlib/axes/_axes.py | 8 ++++---- lib/matplotlib/tests/test_axes.py | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/lib/matplotlib/axes/_axes.py b/lib/matplotlib/axes/_axes.py index 69b169774f71..763d5fe5ca8e 100644 --- a/lib/matplotlib/axes/_axes.py +++ b/lib/matplotlib/axes/_axes.py @@ -4180,7 +4180,7 @@ def _parse_scatter_color_args(c, edgecolors, kwargs, xshape, yshape, label_namer="y") def scatter(self, x, y, s=None, c=None, marker=None, cmap=None, norm=None, vmin=None, vmax=None, alpha=None, linewidths=None, - verts=None, edgecolors=None, masked=False, + verts=None, edgecolors=None, plotinvalid=False, **kwargs): """ A scatter plot of *y* vs *x* with varying marker size and/or color. @@ -4257,7 +4257,7 @@ def scatter(self, x, y, s=None, c=None, marker=None, cmap=None, norm=None, For non-filled markers, the *edgecolors* kwarg is ignored and forced to 'face' internally. - masked : boolean, optional, default: False + plotinvalid : boolean, optional, default: False Set to plot valid points with invalid color, in conjunction with `~matplotlib.colors.Colormap.set_bad`. @@ -4314,7 +4314,7 @@ def scatter(self, x, y, s=None, c=None, marker=None, cmap=None, norm=None, c, edgecolors, kwargs, xshape, yshape, get_next_color_func=self._get_patches_for_fill.get_next_color) - if masked is False: + if plotinvalid is False: # `delete_masked_points` only modifies arguments of the same length # as `x`. x, y, s, c, colors, edgecolors, linewidths =\ @@ -4364,7 +4364,7 @@ def scatter(self, x, y, s=None, c=None, marker=None, cmap=None, norm=None, raise ValueError( "'norm' must be an instance of 'mcolors.Normalize'") - if masked is False: + if plotinvalid is False: collection.set_array(c) else: collection.set_array(ma.masked_invalid(c)) diff --git a/lib/matplotlib/tests/test_axes.py b/lib/matplotlib/tests/test_axes.py index a171a1e8d2f4..8a123ff66bb8 100644 --- a/lib/matplotlib/tests/test_axes.py +++ b/lib/matplotlib/tests/test_axes.py @@ -1759,7 +1759,7 @@ def test_scatter_invalid_color(self, fig_test, fig_ref): # stamping fast path, which would result in slightly offset markers. ax.scatter(range(4), range(4), c=[1, np.nan, 2, np.nan], s=[1, 2, 3, 4], - cmap=cmap, masked=True) + cmap=cmap, plotinvalid=True) ax = fig_ref.subplots() cmap = plt.get_cmap("viridis", 16) ax.scatter([0, 2], [0, 2], c=[1, 2], s=[1, 3], cmap=cmap) From 27d829adf3a35ef00a7023198f732b5319b51bd4 Mon Sep 17 00:00:00 2001 From: Eric Firing Date: Sat, 6 Oct 2018 09:46:57 -1000 Subject: [PATCH 04/16] Handle invalid positions regardless of the plotinvalid kwarg. --- lib/matplotlib/axes/_axes.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/lib/matplotlib/axes/_axes.py b/lib/matplotlib/axes/_axes.py index 763d5fe5ca8e..fd6be9a9960a 100644 --- a/lib/matplotlib/axes/_axes.py +++ b/lib/matplotlib/axes/_axes.py @@ -4314,9 +4314,14 @@ def scatter(self, x, y, s=None, c=None, marker=None, cmap=None, norm=None, c, edgecolors, kwargs, xshape, yshape, get_next_color_func=self._get_patches_for_fill.get_next_color) - if plotinvalid is False: - # `delete_masked_points` only modifies arguments of the same length - # as `x`. + if plotinvalid and colors == None: + # Do full color mapping; don't remove invalid c entries. + ind = np.arange(len(c)) + x, y, s, ind, colors, edgecolors, linewidths =\ + cbook.delete_masked_points( + x, y, s, ind, colors, edgecolors, linewidths) + c = np.ma.masked_invalid(c[ind]) + else: x, y, s, c, colors, edgecolors, linewidths =\ cbook.delete_masked_points( x, y, s, c, colors, edgecolors, linewidths) @@ -4363,12 +4368,7 @@ def scatter(self, x, y, s=None, c=None, marker=None, cmap=None, norm=None, if norm is not None and not isinstance(norm, mcolors.Normalize): raise ValueError( "'norm' must be an instance of 'mcolors.Normalize'") - - if plotinvalid is False: - collection.set_array(c) - else: - collection.set_array(ma.masked_invalid(c)) - + collection.set_array(c) collection.set_cmap(cmap) collection.set_norm(norm) From 921100a15fb30222ea519783fc5c2f9204b254c8 Mon Sep 17 00:00:00 2001 From: Eric Firing Date: Sat, 6 Oct 2018 13:42:44 -1000 Subject: [PATCH 05/16] Alternative approach; addresses #10381 --- lib/matplotlib/axes/_axes.py | 24 ++++++++---- lib/matplotlib/cbook/__init__.py | 63 +++++++++++++++++++++++++++++++ lib/matplotlib/tests/test_axes.py | 27 ++++++------- 3 files changed, 93 insertions(+), 21 deletions(-) diff --git a/lib/matplotlib/axes/_axes.py b/lib/matplotlib/axes/_axes.py index fd6be9a9960a..d475bd1d1e3f 100644 --- a/lib/matplotlib/axes/_axes.py +++ b/lib/matplotlib/axes/_axes.py @@ -4314,17 +4314,25 @@ def scatter(self, x, y, s=None, c=None, marker=None, cmap=None, norm=None, c, edgecolors, kwargs, xshape, yshape, get_next_color_func=self._get_patches_for_fill.get_next_color) + # if plotinvalid and colors == None: + # # Do full color mapping; don't remove invalid c entries. + # ind = np.arange(len(c)) + # x, y, s, ind, colors, edgecolors, linewidths =\ + # cbook.delete_masked_points( + # x, y, s, ind, colors, edgecolors, linewidths) + # c = np.ma.masked_invalid(c[ind]) + # else: + # x, y, s, c, colors, edgecolors, linewidths =\ + # cbook.delete_masked_points( + # x, y, s, c, colors, edgecolors, linewidths) + if plotinvalid and colors == None: - # Do full color mapping; don't remove invalid c entries. - ind = np.arange(len(c)) - x, y, s, ind, colors, edgecolors, linewidths =\ - cbook.delete_masked_points( - x, y, s, ind, colors, edgecolors, linewidths) - c = np.ma.masked_invalid(c[ind]) + c = np.ma.masked_invalid(c) + x, y, s, colors, edgecolors, linewidths =\ + cbook.combine_masks(x, y, s, colors, edgecolors, linewidths) else: x, y, s, c, colors, edgecolors, linewidths =\ - cbook.delete_masked_points( - x, y, s, c, colors, edgecolors, linewidths) + cbook.combine_masks(x, y, s, c, colors, edgecolors, linewidths) scales = s # Renamed for readability below. diff --git a/lib/matplotlib/cbook/__init__.py b/lib/matplotlib/cbook/__init__.py index c02a3c455263..8dba79a60a0b 100644 --- a/lib/matplotlib/cbook/__init__.py +++ b/lib/matplotlib/cbook/__init__.py @@ -1081,6 +1081,69 @@ def delete_masked_points(*args): return margs +def combine_masks(*args): + """ + Find all masked and/or non-finite points in a set of arguments, + and return the arguments as masked arrays with a common mask. + + Arguments can be in any of 5 categories: + + 1) 1-D masked arrays + 2) 1-D ndarrays + 3) ndarrays with more than one dimension + 4) other non-string iterables + 5) anything else + + The first argument must be in one of the first four categories; + any argument with a length differing from that of the first + argument (and hence anything in category 5) then will be + passed through unchanged. + + Masks are obtained from all arguments of the correct length + in categories 1, 2, and 4; a point is bad if masked in a masked + array or if it is a nan or inf. No attempt is made to + extract a mask from categories 2, 3, and 4 if :meth:`np.isfinite` + does not yield a Boolean array. + + All input arguments that are not passed unchanged are returned + as masked arrays if any masked points are found, otherwise as + ndarrays. + + """ + if not len(args): + return () + if is_scalar_or_string(args[0]): + raise ValueError("First argument must be a sequence") + nrecs = len(args[0]) + margs = [] + seqlist = [False] * len(args) + for i, x in enumerate(args): + if not isinstance(x, str) and np.iterable(x) and len(x) == nrecs: + if isinstance(x, np.ma.MaskedArray): + if x.ndim > 1: + raise ValueError("Masked arrays must be 1-D") + x = np.asanyarray(x) + if x.ndim == 1 and x.dtype.kind == 'f': + x = np.ma.masked_invalid(x) + seqlist[i] = True + margs.append(x) + masks = [] # list of masks that are True where bad + for i, x in enumerate(margs): + if seqlist[i]: + if x.ndim > 1: + continue # Don't try to get nan locations unless 1-D. + if np.ma.is_masked(x): + masks.append(np.ma.getmaskarray(x)) + if len(masks): + mask = np.logical_or.reduce(masks) + if mask.any(): + for i, x in enumerate(margs): + if seqlist[i]: + margs[i] = np.ma.array(x) + margs[i][mask] = np.ma.masked + return margs + + def boxplot_stats(X, whis=1.5, bootstrap=None, labels=None, autorange=False): """ diff --git a/lib/matplotlib/tests/test_axes.py b/lib/matplotlib/tests/test_axes.py index 8a123ff66bb8..ba6b95e5d911 100644 --- a/lib/matplotlib/tests/test_axes.py +++ b/lib/matplotlib/tests/test_axes.py @@ -5759,19 +5759,20 @@ def test_color_length_mismatch(): ax.scatter(x, y, c=[c_rgb] * N) -def test_scatter_color_masking(): - x = np.array([1, 2, 3]) - y = np.array([1, np.nan, 3]) - colors = np.array(['k', 'w', 'k']) - linewidths = np.array([1, 2, 3]) - s = plt.scatter(x, y, color=colors, linewidths=linewidths) - - facecolors = s.get_facecolors() - linecolors = s.get_edgecolors() - linewidths = s.get_linewidths() - assert_array_equal(facecolors[1], np.array([0, 0, 0, 1])) - assert_array_equal(linecolors[1], np.array([0, 0, 0, 1])) - assert linewidths[1] == 3 +# The following test is based on the old behavior of deleting bad points. +# def test_scatter_color_masking(): +# x = np.array([1, 2, 3]) +# y = np.array([1, np.nan, 3]) +# colors = np.array(['k', 'w', 'k']) +# linewidths = np.array([1, 2, 3]) +# s = plt.scatter(x, y, color=colors, linewidths=linewidths) +# +# facecolors = s.get_facecolors() +# linecolors = s.get_edgecolors() +# linewidths = s.get_linewidths() +# assert_array_equal(facecolors[1], np.array([0, 0, 0, 1])) +# assert_array_equal(linecolors[1], np.array([0, 0, 0, 1])) +# assert linewidths[1] == 3 def test_eventplot_legend(): From 3f664a943d2e8b135259effd425fec9d6cfd21f3 Mon Sep 17 00:00:00 2001 From: Eric Firing Date: Sat, 6 Oct 2018 14:15:45 -1000 Subject: [PATCH 06/16] Fix test_colorbar_single_scatter. --- lib/matplotlib/tests/test_colorbar.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/lib/matplotlib/tests/test_colorbar.py b/lib/matplotlib/tests/test_colorbar.py index 1c358c09ae78..24d9547c755f 100644 --- a/lib/matplotlib/tests/test_colorbar.py +++ b/lib/matplotlib/tests/test_colorbar.py @@ -197,9 +197,8 @@ def test_colorbar_single_scatter(): # the norm scaling within the colorbar must ensure a # finite range, otherwise a zero denominator will occur in _locate. plt.figure() - x = np.arange(4) - y = x.copy() - z = np.ma.masked_greater(np.arange(50, 54), 50) + x = y = [0] + z = [50] cmap = plt.get_cmap('jet', 16) cs = plt.scatter(x, y, z, c=z, cmap=cmap) plt.colorbar(cs) From 8c0a24eec342734880d86da062c97c36dded8f38 Mon Sep 17 00:00:00 2001 From: Eric Firing Date: Sat, 6 Oct 2018 14:21:18 -1000 Subject: [PATCH 07/16] Update pyplot.py via boilerplate.py. --- lib/matplotlib/pyplot.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/matplotlib/pyplot.py b/lib/matplotlib/pyplot.py index 92bbf6c7b112..81b44b73b352 100644 --- a/lib/matplotlib/pyplot.py +++ b/lib/matplotlib/pyplot.py @@ -2835,12 +2835,12 @@ def quiverkey(Q, X, Y, U, label, **kw): def scatter( x, y, s=None, c=None, marker=None, cmap=None, norm=None, vmin=None, vmax=None, alpha=None, linewidths=None, verts=None, - edgecolors=None, *, data=None, **kwargs): + edgecolors=None, plotinvalid=False, *, data=None, **kwargs): __ret = gca().scatter( x, y, s=s, c=c, marker=marker, cmap=cmap, norm=norm, vmin=vmin, vmax=vmax, alpha=alpha, linewidths=linewidths, - verts=verts, edgecolors=edgecolors, **({"data": data} if data - is not None else {}), **kwargs) + verts=verts, edgecolors=edgecolors, plotinvalid=plotinvalid, + **({"data": data} if data is not None else {}), **kwargs) sci(__ret) return __ret From 6105980306289c7d75e987ddfcdfae9fd6a12ed6 Mon Sep 17 00:00:00 2001 From: Antony Lee Date: Wed, 14 Nov 2018 01:04:28 +0100 Subject: [PATCH 08/16] Additional fixes for improved nonfinite scatter support. --- .../2018-11-14-AL-scatter.rst | 14 +++++++++++ lib/matplotlib/axes/_axes.py | 25 ++++++------------- lib/matplotlib/cbook/__init__.py | 2 +- lib/matplotlib/pyplot.py | 2 +- lib/matplotlib/tests/test_colorbar.py | 2 +- 5 files changed, 24 insertions(+), 21 deletions(-) create mode 100644 doc/api/next_api_changes/2018-11-14-AL-scatter.rst diff --git a/doc/api/next_api_changes/2018-11-14-AL-scatter.rst b/doc/api/next_api_changes/2018-11-14-AL-scatter.rst new file mode 100644 index 000000000000..4018ce1ef2b2 --- /dev/null +++ b/doc/api/next_api_changes/2018-11-14-AL-scatter.rst @@ -0,0 +1,14 @@ +PathCollections created with `~.Axes.scatter` now keep track of invalid points +`````````````````````````````````````````````````````````````````````````````` + +Previously, points with nonfinite (infinite or nan) coordinates would not be +included in the offsets (as returned by `PathCollection.get_offsets`) of a +`PathCollection` created by `~.Axes.scatter`, and points with nonfinite values +(as specified by the *c* kwarg) would not be included in the array (as returned +by `PathCollection.get_array`) + +Such points are now included, but masked out by returning a masked array. + +If the *plotinvalid* kwarg to `~.Axes.scatter` is set, then points with +nonfinite values are plotted using the bad color of the `PathCollection`\ 's +colormap (as set by `Colormap.set_bad`). diff --git a/lib/matplotlib/axes/_axes.py b/lib/matplotlib/axes/_axes.py index d475bd1d1e3f..cd5e2a88b616 100644 --- a/lib/matplotlib/axes/_axes.py +++ b/lib/matplotlib/axes/_axes.py @@ -4180,7 +4180,7 @@ def _parse_scatter_color_args(c, edgecolors, kwargs, xshape, yshape, label_namer="y") def scatter(self, x, y, s=None, c=None, marker=None, cmap=None, norm=None, vmin=None, vmax=None, alpha=None, linewidths=None, - verts=None, edgecolors=None, plotinvalid=False, + verts=None, edgecolors=None, *, plotinvalid=False, **kwargs): """ A scatter plot of *y* vs *x* with varying marker size and/or color. @@ -4314,25 +4314,14 @@ def scatter(self, x, y, s=None, c=None, marker=None, cmap=None, norm=None, c, edgecolors, kwargs, xshape, yshape, get_next_color_func=self._get_patches_for_fill.get_next_color) - # if plotinvalid and colors == None: - # # Do full color mapping; don't remove invalid c entries. - # ind = np.arange(len(c)) - # x, y, s, ind, colors, edgecolors, linewidths =\ - # cbook.delete_masked_points( - # x, y, s, ind, colors, edgecolors, linewidths) - # c = np.ma.masked_invalid(c[ind]) - # else: - # x, y, s, c, colors, edgecolors, linewidths =\ - # cbook.delete_masked_points( - # x, y, s, c, colors, edgecolors, linewidths) - - if plotinvalid and colors == None: + if plotinvalid and colors is None: c = np.ma.masked_invalid(c) - x, y, s, colors, edgecolors, linewidths =\ - cbook.combine_masks(x, y, s, colors, edgecolors, linewidths) + x, y, s, colors, edgecolors, linewidths = \ + cbook._combine_masks(x, y, s, colors, edgecolors, linewidths) else: - x, y, s, c, colors, edgecolors, linewidths =\ - cbook.combine_masks(x, y, s, c, colors, edgecolors, linewidths) + x, y, s, c, colors, edgecolors, linewidths = \ + cbook._combine_masks( + x, y, s, c, colors, edgecolors, linewidths) scales = s # Renamed for readability below. diff --git a/lib/matplotlib/cbook/__init__.py b/lib/matplotlib/cbook/__init__.py index 8dba79a60a0b..a66812201c78 100644 --- a/lib/matplotlib/cbook/__init__.py +++ b/lib/matplotlib/cbook/__init__.py @@ -1081,7 +1081,7 @@ def delete_masked_points(*args): return margs -def combine_masks(*args): +def _combine_masks(*args): """ Find all masked and/or non-finite points in a set of arguments, and return the arguments as masked arrays with a common mask. diff --git a/lib/matplotlib/pyplot.py b/lib/matplotlib/pyplot.py index 81b44b73b352..d278ef11a202 100644 --- a/lib/matplotlib/pyplot.py +++ b/lib/matplotlib/pyplot.py @@ -2835,7 +2835,7 @@ def quiverkey(Q, X, Y, U, label, **kw): def scatter( x, y, s=None, c=None, marker=None, cmap=None, norm=None, vmin=None, vmax=None, alpha=None, linewidths=None, verts=None, - edgecolors=None, plotinvalid=False, *, data=None, **kwargs): + edgecolors=None, *, plotinvalid=False, data=None, **kwargs): __ret = gca().scatter( x, y, s=s, c=c, marker=marker, cmap=cmap, norm=norm, vmin=vmin, vmax=vmax, alpha=alpha, linewidths=linewidths, diff --git a/lib/matplotlib/tests/test_colorbar.py b/lib/matplotlib/tests/test_colorbar.py index 24d9547c755f..f8d43f8e942f 100644 --- a/lib/matplotlib/tests/test_colorbar.py +++ b/lib/matplotlib/tests/test_colorbar.py @@ -197,7 +197,7 @@ def test_colorbar_single_scatter(): # the norm scaling within the colorbar must ensure a # finite range, otherwise a zero denominator will occur in _locate. plt.figure() - x = y = [0] + x = y = [0] z = [50] cmap = plt.get_cmap('jet', 16) cs = plt.scatter(x, y, z, c=z, cmap=cmap) From c932a9e9008cb9a80ae2b26bec6290801465db37 Mon Sep 17 00:00:00 2001 From: Antony Lee Date: Wed, 14 Nov 2018 01:48:11 +0100 Subject: [PATCH 09/16] :s/plotinvalid/plotnonfinite. The name is a reference to the standard isfinite() function. --- doc/api/next_api_changes/2018-11-14-AL-scatter.rst | 2 +- lib/matplotlib/axes/_axes.py | 8 ++++---- lib/matplotlib/pyplot.py | 7 ++++--- lib/matplotlib/tests/test_axes.py | 2 +- 4 files changed, 10 insertions(+), 9 deletions(-) diff --git a/doc/api/next_api_changes/2018-11-14-AL-scatter.rst b/doc/api/next_api_changes/2018-11-14-AL-scatter.rst index 4018ce1ef2b2..97c6404dfc95 100644 --- a/doc/api/next_api_changes/2018-11-14-AL-scatter.rst +++ b/doc/api/next_api_changes/2018-11-14-AL-scatter.rst @@ -9,6 +9,6 @@ by `PathCollection.get_array`) Such points are now included, but masked out by returning a masked array. -If the *plotinvalid* kwarg to `~.Axes.scatter` is set, then points with +If the *plotnonfinite* kwarg to `~.Axes.scatter` is set, then points with nonfinite values are plotted using the bad color of the `PathCollection`\ 's colormap (as set by `Colormap.set_bad`). diff --git a/lib/matplotlib/axes/_axes.py b/lib/matplotlib/axes/_axes.py index cd5e2a88b616..3739dd531427 100644 --- a/lib/matplotlib/axes/_axes.py +++ b/lib/matplotlib/axes/_axes.py @@ -4180,7 +4180,7 @@ def _parse_scatter_color_args(c, edgecolors, kwargs, xshape, yshape, label_namer="y") def scatter(self, x, y, s=None, c=None, marker=None, cmap=None, norm=None, vmin=None, vmax=None, alpha=None, linewidths=None, - verts=None, edgecolors=None, *, plotinvalid=False, + verts=None, edgecolors=None, *, plotnonfinite=False, **kwargs): """ A scatter plot of *y* vs *x* with varying marker size and/or color. @@ -4257,8 +4257,8 @@ def scatter(self, x, y, s=None, c=None, marker=None, cmap=None, norm=None, For non-filled markers, the *edgecolors* kwarg is ignored and forced to 'face' internally. - plotinvalid : boolean, optional, default: False - Set to plot valid points with invalid color, in conjunction with + plotnonfinite : boolean, optional, default: False + Set to plot points with nonfinite *c*, in conjunction with `~matplotlib.colors.Colormap.set_bad`. Returns @@ -4314,7 +4314,7 @@ def scatter(self, x, y, s=None, c=None, marker=None, cmap=None, norm=None, c, edgecolors, kwargs, xshape, yshape, get_next_color_func=self._get_patches_for_fill.get_next_color) - if plotinvalid and colors is None: + if plotnonfinite and colors is None: c = np.ma.masked_invalid(c) x, y, s, colors, edgecolors, linewidths = \ cbook._combine_masks(x, y, s, colors, edgecolors, linewidths) diff --git a/lib/matplotlib/pyplot.py b/lib/matplotlib/pyplot.py index d278ef11a202..73c43302cbce 100644 --- a/lib/matplotlib/pyplot.py +++ b/lib/matplotlib/pyplot.py @@ -2835,12 +2835,13 @@ def quiverkey(Q, X, Y, U, label, **kw): def scatter( x, y, s=None, c=None, marker=None, cmap=None, norm=None, vmin=None, vmax=None, alpha=None, linewidths=None, verts=None, - edgecolors=None, *, plotinvalid=False, data=None, **kwargs): + edgecolors=None, *, plotnonfinite=False, data=None, **kwargs): __ret = gca().scatter( x, y, s=s, c=c, marker=marker, cmap=cmap, norm=norm, vmin=vmin, vmax=vmax, alpha=alpha, linewidths=linewidths, - verts=verts, edgecolors=edgecolors, plotinvalid=plotinvalid, - **({"data": data} if data is not None else {}), **kwargs) + verts=verts, edgecolors=edgecolors, + plotnonfinite=plotnonfinite, **({"data": data} if data is not + None else {}), **kwargs) sci(__ret) return __ret diff --git a/lib/matplotlib/tests/test_axes.py b/lib/matplotlib/tests/test_axes.py index ba6b95e5d911..07ceb9f4d037 100644 --- a/lib/matplotlib/tests/test_axes.py +++ b/lib/matplotlib/tests/test_axes.py @@ -1759,7 +1759,7 @@ def test_scatter_invalid_color(self, fig_test, fig_ref): # stamping fast path, which would result in slightly offset markers. ax.scatter(range(4), range(4), c=[1, np.nan, 2, np.nan], s=[1, 2, 3, 4], - cmap=cmap, plotinvalid=True) + cmap=cmap, plotnonfinite=True) ax = fig_ref.subplots() cmap = plt.get_cmap("viridis", 16) ax.scatter([0, 2], [0, 2], c=[1, 2], s=[1, 3], cmap=cmap) From 85b1f962cea5d983328e6725e53d4d6548ac2464 Mon Sep 17 00:00:00 2001 From: Eric Firing Date: Tue, 13 Nov 2018 20:16:45 -1000 Subject: [PATCH 10/16] Remove unneeded input/output pair from a method call. --- lib/matplotlib/axes/_axes.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/matplotlib/axes/_axes.py b/lib/matplotlib/axes/_axes.py index 3739dd531427..e3402ac94b0e 100644 --- a/lib/matplotlib/axes/_axes.py +++ b/lib/matplotlib/axes/_axes.py @@ -4316,8 +4316,8 @@ def scatter(self, x, y, s=None, c=None, marker=None, cmap=None, norm=None, if plotnonfinite and colors is None: c = np.ma.masked_invalid(c) - x, y, s, colors, edgecolors, linewidths = \ - cbook._combine_masks(x, y, s, colors, edgecolors, linewidths) + x, y, s, edgecolors, linewidths = \ + cbook._combine_masks(x, y, s, edgecolors, linewidths) else: x, y, s, c, colors, edgecolors, linewidths = \ cbook._combine_masks( From e959692e42f08c6e157632cf208a6fd19d4228b1 Mon Sep 17 00:00:00 2001 From: Eric Firing Date: Sun, 18 Nov 2018 20:36:09 -1000 Subject: [PATCH 11/16] Fix bugs in scatter with invalid points and with integer arrays. --- lib/matplotlib/axes/_axes.py | 2 +- lib/matplotlib/cbook/__init__.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/matplotlib/axes/_axes.py b/lib/matplotlib/axes/_axes.py index e3402ac94b0e..f7ac8a9d7d66 100644 --- a/lib/matplotlib/axes/_axes.py +++ b/lib/matplotlib/axes/_axes.py @@ -4347,7 +4347,7 @@ def scatter(self, x, y, s=None, c=None, marker=None, cmap=None, norm=None, edgecolors = 'face' linewidths = rcParams['lines.linewidth'] - offsets = np.column_stack([x, y]) + offsets = np.ma.column_stack([x, y]) collection = mcoll.PathCollection( (path,), scales, diff --git a/lib/matplotlib/cbook/__init__.py b/lib/matplotlib/cbook/__init__.py index a66812201c78..83e57adc9d14 100644 --- a/lib/matplotlib/cbook/__init__.py +++ b/lib/matplotlib/cbook/__init__.py @@ -1123,8 +1123,8 @@ def _combine_masks(*args): if x.ndim > 1: raise ValueError("Masked arrays must be 1-D") x = np.asanyarray(x) - if x.ndim == 1 and x.dtype.kind == 'f': - x = np.ma.masked_invalid(x) + if x.ndim == 1: + x = safe_masked_invalid(x) seqlist[i] = True margs.append(x) masks = [] # list of masks that are True where bad From f979b1f0e0be3e36b1e1a5712914239242db4e6a Mon Sep 17 00:00:00 2001 From: Eric Firing Date: Sun, 18 Nov 2018 20:52:40 -1000 Subject: [PATCH 12/16] Test both values of new scatter kwarg, plotnonfinite. --- lib/matplotlib/tests/test_axes.py | 28 ++++++++++++---------------- 1 file changed, 12 insertions(+), 16 deletions(-) diff --git a/lib/matplotlib/tests/test_axes.py b/lib/matplotlib/tests/test_axes.py index 07ceb9f4d037..ed7e0ac8168e 100644 --- a/lib/matplotlib/tests/test_axes.py +++ b/lib/matplotlib/tests/test_axes.py @@ -1765,6 +1765,18 @@ def test_scatter_invalid_color(self, fig_test, fig_ref): 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") + @check_figures_equal(extensions=["png"]) + def test_scatter_no_invalid_color(self, fig_test, fig_ref): + # With plotninfinite=False we plot only 2 points. + ax = fig_test.subplots() + cmap = plt.get_cmap("viridis", 16) + cmap.set_bad("k", 1) + ax.scatter(range(4), range(4), + c=[1, np.nan, 2, np.nan], s=[1, 2, 3, 4], + cmap=cmap, plotnonfinite=False) + ax = fig_ref.subplots() + ax.scatter([0, 2], [0, 2], c=[1, 2], s=[1, 3], cmap=cmap) + # Parameters for *test_scatter_c*. NB: assuming that the # scatter plot will have 4 elements. The tuple scheme is: # (*c* parameter case, exception regexp key or None if no exception) @@ -5759,22 +5771,6 @@ def test_color_length_mismatch(): ax.scatter(x, y, c=[c_rgb] * N) -# The following test is based on the old behavior of deleting bad points. -# def test_scatter_color_masking(): -# x = np.array([1, 2, 3]) -# y = np.array([1, np.nan, 3]) -# colors = np.array(['k', 'w', 'k']) -# linewidths = np.array([1, 2, 3]) -# s = plt.scatter(x, y, color=colors, linewidths=linewidths) -# -# facecolors = s.get_facecolors() -# linecolors = s.get_edgecolors() -# linewidths = s.get_linewidths() -# assert_array_equal(facecolors[1], np.array([0, 0, 0, 1])) -# assert_array_equal(linecolors[1], np.array([0, 0, 0, 1])) -# assert linewidths[1] == 3 - - def test_eventplot_legend(): plt.eventplot([1.0], label='Label') plt.legend() From a23a30b0290df6f1a9458bf7e0826dce2202189b Mon Sep 17 00:00:00 2001 From: Eric Firing Date: Thu, 22 Nov 2018 11:07:22 -1000 Subject: [PATCH 13/16] Fix units_scatter.py example, which was broken before this PR. Prior to this, examples/units/basic_units.py was not actually handling masked arrays, and units_scatter.py was not plotting its third panel correctly. --- examples/units/basic_units.py | 20 ++++++++++++++++++-- examples/units/units_scatter.py | 5 ++--- 2 files changed, 20 insertions(+), 5 deletions(-) diff --git a/examples/units/basic_units.py b/examples/units/basic_units.py index 4f8b514e0de5..cd8daf6e2cf4 100644 --- a/examples/units/basic_units.py +++ b/examples/units/basic_units.py @@ -174,7 +174,10 @@ def get_compressed_copy(self, mask): def convert_to(self, unit): if unit == self.unit or not unit: return self - new_value = self.unit.convert_value_to(self.value, unit) + try: + new_value = self.unit.convert_value_to(self.value, unit) + except AttributeError: + new_value = self return TaggedValue(new_value, unit) def get_value(self): @@ -345,7 +348,20 @@ def convert(val, unit, axis): if units.ConversionInterface.is_numlike(val): return val if np.iterable(val): - return [thisval.convert_to(unit).get_value() for thisval in val] + if np.ma.isMaskedArray(val): + val = val.astype(float).filled(np.nan) + out = np.empty((len(val),), dtype=float) + for i, thisval in enumerate(val): + if np.ma.is_masked(thisval): + out[i] = np.nan + else: + try: + out[i] = thisval.convert_to(unit).get_value() + except AttributeError: + out[i] = thisval + return out + if np.ma.is_masked(val): + return np.nan else: return val.convert_to(unit).get_value() diff --git a/examples/units/units_scatter.py b/examples/units/units_scatter.py index 7850adef7e33..095065815f4a 100644 --- a/examples/units/units_scatter.py +++ b/examples/units/units_scatter.py @@ -27,9 +27,8 @@ ax2.scatter(xsecs, xsecs, yunits=hertz) ax2.axis([0, 10, 0, 1]) -ax3.scatter(xsecs, xsecs, yunits=hertz) -ax3.yaxis.set_units(minutes) -ax3.axis([0, 10, 0, 1]) +ax3.scatter(xsecs, xsecs, yunits=minutes) +ax3.axis([0, 10, 0, 0.2]) fig.tight_layout() plt.show() From 09ff3df88c8a66333aa223211f8663fb872aee1a Mon Sep 17 00:00:00 2001 From: Eric Firing Date: Mon, 10 Dec 2018 20:42:23 -1000 Subject: [PATCH 14/16] Simplify _combine_masks --- lib/matplotlib/cbook/__init__.py | 24 ++++++++++-------------- 1 file changed, 10 insertions(+), 14 deletions(-) diff --git a/lib/matplotlib/cbook/__init__.py b/lib/matplotlib/cbook/__init__.py index 83e57adc9d14..8e5e26103d6a 100644 --- a/lib/matplotlib/cbook/__init__.py +++ b/lib/matplotlib/cbook/__init__.py @@ -1102,8 +1102,10 @@ def _combine_masks(*args): Masks are obtained from all arguments of the correct length in categories 1, 2, and 4; a point is bad if masked in a masked array or if it is a nan or inf. No attempt is made to - extract a mask from categories 2, 3, and 4 if :meth:`np.isfinite` - does not yield a Boolean array. + extract a mask from categories 2 and 4 if :meth:`np.isfinite` + does not yield a Boolean array. Category 3 is included to + support RGB or RGBA ndarrays, which are assumed to have only + valid values and which are passed through unchanged. All input arguments that are not passed unchanged are returned as masked arrays if any masked points are found, otherwise as @@ -1117,6 +1119,7 @@ def _combine_masks(*args): nrecs = len(args[0]) margs = [] seqlist = [False] * len(args) + masks = [] # list of masks that are True where bad for i, x in enumerate(args): if not isinstance(x, str) and np.iterable(x) and len(x) == nrecs: if isinstance(x, np.ma.MaskedArray): @@ -1126,21 +1129,14 @@ def _combine_masks(*args): if x.ndim == 1: x = safe_masked_invalid(x) seqlist[i] = True + if np.ma.is_masked(x): + masks.append(np.ma.getmaskarray(x)) margs.append(x) - masks = [] # list of masks that are True where bad - for i, x in enumerate(margs): - if seqlist[i]: - if x.ndim > 1: - continue # Don't try to get nan locations unless 1-D. - if np.ma.is_masked(x): - masks.append(np.ma.getmaskarray(x)) if len(masks): mask = np.logical_or.reduce(masks) - if mask.any(): - for i, x in enumerate(margs): - if seqlist[i]: - margs[i] = np.ma.array(x) - margs[i][mask] = np.ma.masked + for i, x in enumerate(margs): + if seqlist[i]: + margs[i] = np.ma.array(x, mask=mask) return margs From ce98e8f7bb0b338ed8da6fc25c0724c6030cadec Mon Sep 17 00:00:00 2001 From: Eric Firing Date: Tue, 11 Dec 2018 09:19:35 -1000 Subject: [PATCH 15/16] Improve readability of _combine_masks() --- lib/matplotlib/cbook/__init__.py | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/lib/matplotlib/cbook/__init__.py b/lib/matplotlib/cbook/__init__.py index 8e5e26103d6a..8f7e7398e4a2 100644 --- a/lib/matplotlib/cbook/__init__.py +++ b/lib/matplotlib/cbook/__init__.py @@ -1117,21 +1117,22 @@ def _combine_masks(*args): if is_scalar_or_string(args[0]): raise ValueError("First argument must be a sequence") nrecs = len(args[0]) - margs = [] - seqlist = [False] * len(args) - masks = [] # list of masks that are True where bad + margs = [] # Output args; some may be modified. + seqlist = [False] * len(args) # Flags: True if output will be masked. + masks = [] # List of masks. for i, x in enumerate(args): - if not isinstance(x, str) and np.iterable(x) and len(x) == nrecs: - if isinstance(x, np.ma.MaskedArray): - if x.ndim > 1: - raise ValueError("Masked arrays must be 1-D") + if is_scalar_or_string(x) or len(x) != nrecs: + margs.append(x) # Leave it unmodified. + else: + if isinstance(x, np.ma.MaskedArray) and x.ndim > 1: + raise ValueError("Masked arrays must be 1-D") x = np.asanyarray(x) if x.ndim == 1: x = safe_masked_invalid(x) seqlist[i] = True if np.ma.is_masked(x): masks.append(np.ma.getmaskarray(x)) - margs.append(x) + margs.append(x) # Possibly modified. if len(masks): mask = np.logical_or.reduce(masks) for i, x in enumerate(margs): From a2cec14c6ea94ae90c175ac550ed0af2cd2159fd Mon Sep 17 00:00:00 2001 From: Eric Firing Date: Tue, 11 Dec 2018 11:31:39 -1000 Subject: [PATCH 16/16] Minor code cleanups in basic_units example --- examples/units/basic_units.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/units/basic_units.py b/examples/units/basic_units.py index cd8daf6e2cf4..49eb823ffbe2 100644 --- a/examples/units/basic_units.py +++ b/examples/units/basic_units.py @@ -348,9 +348,9 @@ def convert(val, unit, axis): if units.ConversionInterface.is_numlike(val): return val if np.iterable(val): - if np.ma.isMaskedArray(val): + if isinstance(val, np.ma.MaskedArray): val = val.astype(float).filled(np.nan) - out = np.empty((len(val),), dtype=float) + out = np.empty(len(val)) for i, thisval in enumerate(val): if np.ma.is_masked(thisval): out[i] = np.nan