diff --git a/doc/users/next_whats_new/pcolormesh_alpha_improvement.rst b/doc/users/next_whats_new/pcolormesh_alpha_improvement.rst new file mode 100644 index 000000000000..8b9ec98e7b85 --- /dev/null +++ b/doc/users/next_whats_new/pcolormesh_alpha_improvement.rst @@ -0,0 +1,40 @@ +pcolormesh has improved transparency handling by enabling snapping +------------------------------------------------------------------ + +Due to how the snapping keyword argument was getting passed to the AGG backend, +previous versions of Matplotlib would appear to show lines between the grid +edges of a mesh with transparency. This version now applies snapping +by default. To restore the old behavior (e.g., for test images), you may set +:rc:`pcolormesh.snap` to `False`. + +.. plot:: + + import matplotlib.pyplot as plt + import numpy as np + + # Use old pcolormesh snapping values + plt.rcParams['pcolormesh.snap'] = False + fig, ax = plt.subplots() + xx, yy = np.meshgrid(np.arange(10), np.arange(10)) + z = (xx + 1) * (yy + 1) + mesh = ax.pcolormesh(xx, yy, z, shading='auto', alpha=0.5) + fig.colorbar(mesh, orientation='vertical') + ax.set_title('Before (pcolormesh.snap = False)') + +Note that there are lines between the grid boundaries of the main plot which +are not the same transparency. The colorbar also shows these lines when a +transparency is added to the colormap because internally it uses pcolormesh +to draw the colorbar. With snapping on by default (below), the lines +at the grid boundaries disappear. + +.. plot:: + + import matplotlib.pyplot as plt + import numpy as np + + fig, ax = plt.subplots() + xx, yy = np.meshgrid(np.arange(10), np.arange(10)) + z = (xx + 1) * (yy + 1) + mesh = ax.pcolormesh(xx, yy, z, shading='auto', alpha=0.5) + fig.colorbar(mesh, orientation='vertical') + ax.set_title('After (default: pcolormesh.snap = True)') diff --git a/lib/matplotlib/axes/_axes.py b/lib/matplotlib/axes/_axes.py index 00ecf9b9b1d2..3eddb7002b3c 100644 --- a/lib/matplotlib/axes/_axes.py +++ b/lib/matplotlib/axes/_axes.py @@ -6089,6 +6089,8 @@ def pcolormesh(self, *args, alpha=None, norm=None, cmap=None, vmin=None, collection = mcoll.QuadMesh(Nx - 1, Ny - 1, coords, antialiased=antialiased, shading=shading, **kwargs) + snap = kwargs.get('snap', rcParams['pcolormesh.snap']) + collection.set_snap(snap) collection.set_alpha(alpha) collection.set_array(C) collection.set_cmap(cmap) diff --git a/lib/matplotlib/collections.py b/lib/matplotlib/collections.py index ad281f6c24d7..2dfa3efe6530 100644 --- a/lib/matplotlib/collections.py +++ b/lib/matplotlib/collections.py @@ -2036,6 +2036,7 @@ def draw(self, renderer): transOffset = transOffset.get_affine() gc = renderer.new_gc() + gc.set_snap(self.get_snap()) self._set_gc_clip(gc) gc.set_linewidth(self.get_linewidth()[0]) diff --git a/lib/matplotlib/rcsetup.py b/lib/matplotlib/rcsetup.py index 09e30e1a903c..7245f487e2bd 100644 --- a/lib/matplotlib/rcsetup.py +++ b/lib/matplotlib/rcsetup.py @@ -1090,6 +1090,7 @@ def _convert_validator_spec(key, conv): ## pcolor(mesh) props: "pcolor.shading": ["auto", "flat", "nearest", "gouraud"], + "pcolormesh.snap": validate_bool, ## patch props "patch.linewidth": validate_float, # line width in points diff --git a/lib/matplotlib/tests/test_agg_filter.py b/lib/matplotlib/tests/test_agg_filter.py index 913034636603..fd54a6b4aa9e 100644 --- a/lib/matplotlib/tests/test_agg_filter.py +++ b/lib/matplotlib/tests/test_agg_filter.py @@ -7,6 +7,9 @@ @image_comparison(baseline_images=['agg_filter_alpha'], extensions=['png', 'pdf']) def test_agg_filter_alpha(): + # Remove this line when this test image is regenerated. + plt.rcParams['pcolormesh.snap'] = False + ax = plt.axes() x, y = np.mgrid[0:7, 0:8] data = x**2 - y**2 diff --git a/lib/matplotlib/tests/test_axes.py b/lib/matplotlib/tests/test_axes.py index aed1c610ed43..9a503f0bd9a0 100644 --- a/lib/matplotlib/tests/test_axes.py +++ b/lib/matplotlib/tests/test_axes.py @@ -731,6 +731,10 @@ def test_hexbin_pickable(): @image_comparison(['hexbin_log.png'], style='mpl20') def test_hexbin_log(): # Issue #1636 (and also test log scaled colorbar) + + # Remove this line when this test image is regenerated. + plt.rcParams['pcolormesh.snap'] = False + np.random.seed(19680801) n = 100000 x = np.random.standard_normal(n) @@ -992,6 +996,9 @@ def test_pcolorargs_5205(): @image_comparison(['pcolormesh'], remove_text=True) def test_pcolormesh(): + # Remove this line when this test image is regenerated. + plt.rcParams['pcolormesh.snap'] = False + n = 12 x = np.linspace(-1.5, 1.5, n) y = np.linspace(-1.5, 1.5, n*2) @@ -1014,6 +1021,9 @@ def test_pcolormesh(): @image_comparison(['pcolormesh_alpha'], extensions=["png", "pdf"], remove_text=True) def test_pcolormesh_alpha(): + # Remove this line when this test image is regenerated. + plt.rcParams['pcolormesh.snap'] = False + n = 12 X, Y = np.meshgrid( np.linspace(-1.5, 1.5, n), @@ -1046,6 +1056,9 @@ def test_pcolormesh_alpha(): @image_comparison(['pcolormesh_datetime_axis.png'], remove_text=False, style='mpl20') def test_pcolormesh_datetime_axis(): + # Remove this line when this test image is regenerated. + plt.rcParams['pcolormesh.snap'] = False + fig = plt.figure() fig.subplots_adjust(hspace=0.4, top=0.98, bottom=.15) base = datetime.datetime(2013, 1, 1) @@ -1745,6 +1758,9 @@ def test_contour_hatching(): @image_comparison(['contour_colorbar'], style='mpl20') def test_contour_colorbar(): + # Remove this line when this test image is regenerated. + plt.rcParams['pcolormesh.snap'] = False + x, y, z = contour_dat() fig = plt.figure() @@ -1768,6 +1784,9 @@ def test_contour_colorbar(): @image_comparison(['hist2d', 'hist2d'], remove_text=True, style='mpl20') def test_hist2d(): + # Remove this line when this test image is regenerated. + plt.rcParams['pcolormesh.snap'] = False + np.random.seed(0) # make it not symmetric in case we switch x and y axis x = np.random.randn(100)*2+5 @@ -1785,6 +1804,9 @@ def test_hist2d(): @image_comparison(['hist2d_transpose'], remove_text=True, style='mpl20') def test_hist2d_transpose(): + # Remove this line when this test image is regenerated. + plt.rcParams['pcolormesh.snap'] = False + np.random.seed(0) # make sure the output from np.histogram is transposed before # passing to pcolorfast diff --git a/lib/matplotlib/tests/test_colorbar.py b/lib/matplotlib/tests/test_colorbar.py index 042c9ab419d1..8307fceb3384 100644 --- a/lib/matplotlib/tests/test_colorbar.py +++ b/lib/matplotlib/tests/test_colorbar.py @@ -101,6 +101,9 @@ def _colorbar_extension_length(spacing): 'colorbar_extensions_shape_proportional.png']) def test_colorbar_extension_shape(): """Test rectangular colorbar extensions.""" + # Remove this line when this test image is regenerated. + plt.rcParams['pcolormesh.snap'] = False + # Create figures for uniform and proportionally spaced colorbars. _colorbar_extension_shape('uniform') _colorbar_extension_shape('proportional') @@ -110,6 +113,9 @@ def test_colorbar_extension_shape(): 'colorbar_extensions_proportional.png']) def test_colorbar_extension_length(): """Test variable length colorbar extensions.""" + # Remove this line when this test image is regenerated. + plt.rcParams['pcolormesh.snap'] = False + # Create figures for uniform and proportionally spaced colorbars. _colorbar_extension_length('uniform') _colorbar_extension_length('proportional') @@ -124,6 +130,9 @@ def test_colorbar_extension_length(): extensions=['png'], remove_text=True, savefig_kwarg={'dpi': 40}) def test_colorbar_positioning(use_gridspec): + # Remove this line when this test image is regenerated. + plt.rcParams['pcolormesh.snap'] = False + data = np.arange(1200).reshape(30, 40) levels = [0, 200, 400, 600, 800, 1000, 1200] @@ -232,6 +241,9 @@ def test_colorbarbase(): @image_comparison(['colorbar_closed_patch'], remove_text=True) def test_colorbar_closed_patch(): + # Remove this line when this test image is regenerated. + plt.rcParams['pcolormesh.snap'] = False + fig = plt.figure(figsize=(8, 6)) ax1 = fig.add_axes([0.05, 0.85, 0.9, 0.1]) ax2 = fig.add_axes([0.1, 0.65, 0.75, 0.1]) diff --git a/lib/matplotlib/tests/test_colors.py b/lib/matplotlib/tests/test_colors.py index ba1192d16ac3..562579bce7f2 100644 --- a/lib/matplotlib/tests/test_colors.py +++ b/lib/matplotlib/tests/test_colors.py @@ -601,6 +601,9 @@ def _mask_tester(norm_instance, vals): @image_comparison(['levels_and_colors.png']) def test_cmap_and_norm_from_levels_and_colors(): + # Remove this line when this test image is regenerated. + plt.rcParams['pcolormesh.snap'] = False + data = np.linspace(-2, 4, 49).reshape(7, 7) levels = [-1, 2, 2.5, 3] colors = ['red', 'green', 'blue', 'yellow', 'black'] @@ -618,6 +621,8 @@ def test_cmap_and_norm_from_levels_and_colors(): @image_comparison(baseline_images=['boundarynorm_and_colorbar'], extensions=['png']) def test_boundarynorm_and_colorbarbase(): + # Remove this line when this test image is regenerated. + plt.rcParams['pcolormesh.snap'] = False # Make a figure and axes with dimensions as desired. fig = plt.figure() diff --git a/lib/matplotlib/tests/test_constrainedlayout.py b/lib/matplotlib/tests/test_constrainedlayout.py index 46e6b9663ef3..dbd8a3552ad0 100644 --- a/lib/matplotlib/tests/test_constrainedlayout.py +++ b/lib/matplotlib/tests/test_constrainedlayout.py @@ -51,6 +51,9 @@ def test_constrained_layout2(): @image_comparison(['constrained_layout3.png']) def test_constrained_layout3(): """Test constrained_layout for colorbars with subplots""" + # Remove this line when this test image is regenerated. + plt.rcParams['pcolormesh.snap'] = False + fig, axs = plt.subplots(2, 2, constrained_layout=True) for nn, ax in enumerate(axs.flat): pcm = example_pcolor(ax, fontsize=24) @@ -64,6 +67,9 @@ def test_constrained_layout3(): @image_comparison(['constrained_layout4']) def test_constrained_layout4(): """Test constrained_layout for a single colorbar with subplots""" + # Remove this line when this test image is regenerated. + plt.rcParams['pcolormesh.snap'] = False + fig, axs = plt.subplots(2, 2, constrained_layout=True) for ax in axs.flat: pcm = example_pcolor(ax, fontsize=24) @@ -76,6 +82,9 @@ def test_constrained_layout5(): Test constrained_layout for a single colorbar with subplots, colorbar bottom """ + # Remove this line when this test image is regenerated. + plt.rcParams['pcolormesh.snap'] = False + fig, axs = plt.subplots(2, 2, constrained_layout=True) for ax in axs.flat: pcm = example_pcolor(ax, fontsize=24) @@ -87,6 +96,9 @@ def test_constrained_layout5(): @image_comparison(['constrained_layout6.png']) def test_constrained_layout6(): """Test constrained_layout for nested gridspecs""" + # Remove this line when this test image is regenerated. + plt.rcParams['pcolormesh.snap'] = False + fig = plt.figure(constrained_layout=True) gs = fig.add_gridspec(1, 2, figure=fig) gsl = gs[0].subgridspec(2, 2) @@ -126,6 +138,9 @@ def test_constrained_layout7(): @image_comparison(['constrained_layout8.png']) def test_constrained_layout8(): """Test for gridspecs that are not completely full""" + # Remove this line when this test image is regenerated. + plt.rcParams['pcolormesh.snap'] = False + fig = plt.figure(figsize=(10, 5), constrained_layout=True) gs = gridspec.GridSpec(3, 5, figure=fig) axs = [] @@ -153,6 +168,9 @@ def test_constrained_layout8(): @image_comparison(['constrained_layout9.png']) def test_constrained_layout9(): """Test for handling suptitle and for sharex and sharey""" + # Remove this line when this test image is regenerated. + plt.rcParams['pcolormesh.snap'] = False + fig, axs = plt.subplots(2, 2, constrained_layout=True, sharex=False, sharey=False) for ax in axs.flat: @@ -176,6 +194,9 @@ def test_constrained_layout10(): @image_comparison(['constrained_layout11.png']) def test_constrained_layout11(): """Test for multiple nested gridspecs""" + # Remove this line when this test image is regenerated. + plt.rcParams['pcolormesh.snap'] = False + fig = plt.figure(constrained_layout=True, figsize=(13, 3)) gs0 = gridspec.GridSpec(1, 2, figure=fig) gsl = gridspec.GridSpecFromSubplotSpec(1, 2, gs0[0]) @@ -195,6 +216,9 @@ def test_constrained_layout11(): @image_comparison(['constrained_layout11rat.png']) def test_constrained_layout11rat(): """Test for multiple nested gridspecs with width_ratios""" + # Remove this line when this test image is regenerated. + plt.rcParams['pcolormesh.snap'] = False + fig = plt.figure(constrained_layout=True, figsize=(10, 3)) gs0 = gridspec.GridSpec(1, 2, figure=fig, width_ratios=[6, 1]) gsl = gridspec.GridSpecFromSubplotSpec(1, 2, gs0[0]) @@ -236,6 +260,9 @@ def test_constrained_layout12(): @image_comparison(['constrained_layout13.png'], tol=2.e-2) def test_constrained_layout13(): """Test that padding works.""" + # Remove this line when this test image is regenerated. + plt.rcParams['pcolormesh.snap'] = False + fig, axs = plt.subplots(2, 2, constrained_layout=True) for ax in axs.flat: pcm = example_pcolor(ax, fontsize=12) @@ -246,6 +273,9 @@ def test_constrained_layout13(): @image_comparison(['constrained_layout14.png']) def test_constrained_layout14(): """Test that padding works.""" + # Remove this line when this test image is regenerated. + plt.rcParams['pcolormesh.snap'] = False + fig, axs = plt.subplots(2, 2, constrained_layout=True) for ax in axs.flat: pcm = example_pcolor(ax, fontsize=12) @@ -374,6 +404,8 @@ def test_colorbar_location(): Test that colorbar handling is as expected for various complicated cases... """ + # Remove this line when this test image is regenerated. + plt.rcParams['pcolormesh.snap'] = False fig, axs = plt.subplots(4, 5, constrained_layout=True) for ax in axs.flat: diff --git a/lib/matplotlib/tests/test_contour.py b/lib/matplotlib/tests/test_contour.py index 446d9f3a7ff7..813e9ac2f614 100644 --- a/lib/matplotlib/tests/test_contour.py +++ b/lib/matplotlib/tests/test_contour.py @@ -136,6 +136,9 @@ def test_contour_labels_size_color(): @image_comparison(['contour_manual_colors_and_levels.png'], remove_text=True) def test_given_colors_levels_and_extends(): + # Remove this line when this test image is regenerated. + plt.rcParams['pcolormesh.snap'] = False + _, axs = plt.subplots(2, 4) data = np.arange(12).reshape(3, 4) @@ -316,6 +319,9 @@ def test_clabel_zorder(use_clabeltext, contour_zorder, clabel_zorder): @image_comparison(['contour_log_extension.png'], remove_text=True, style='mpl20') def test_contourf_log_extension(): + # Remove this line when this test image is regenerated. + plt.rcParams['pcolormesh.snap'] = False + # Test that contourf with lognorm is extended correctly fig = plt.figure(figsize=(10, 5)) fig.subplots_adjust(left=0.05, right=0.95) @@ -355,6 +361,9 @@ def test_contourf_log_extension(): # tolerance is because image changed minutely when tick finding on # colorbars was cleaned up... def test_contour_addlines(): + # Remove this line when this test image is regenerated. + plt.rcParams['pcolormesh.snap'] = False + fig, ax = plt.subplots() np.random.seed(19680812) X = np.random.rand(10, 10)*10000 @@ -369,6 +378,9 @@ def test_contour_addlines(): @image_comparison(baseline_images=['contour_uneven'], extensions=['png'], remove_text=True, style='mpl20') def test_contour_uneven(): + # Remove this line when this test image is regenerated. + plt.rcParams['pcolormesh.snap'] = False + z = np.arange(24).reshape(4, 6) fig, axs = plt.subplots(1, 2) ax = axs[0] diff --git a/lib/matplotlib/tests/test_image.py b/lib/matplotlib/tests/test_image.py index 9eb1b45e49df..9605ff0d2a56 100644 --- a/lib/matplotlib/tests/test_image.py +++ b/lib/matplotlib/tests/test_image.py @@ -796,6 +796,9 @@ def test_image_preserve_size2(): @image_comparison(['mask_image_over_under.png'], remove_text=True) def test_mask_image_over_under(): + # Remove this line when this test image is regenerated. + plt.rcParams['pcolormesh.snap'] = False + delta = 0.025 x = y = np.arange(-3.0, 3.0, delta) X, Y = np.meshgrid(x, y) diff --git a/lib/matplotlib/tests/test_pickle.py b/lib/matplotlib/tests/test_pickle.py index 6a78526b5a64..e2255eea2068 100644 --- a/lib/matplotlib/tests/test_pickle.py +++ b/lib/matplotlib/tests/test_pickle.py @@ -43,6 +43,9 @@ def test_simple(): @image_comparison(['multi_pickle.png'], remove_text=True, style='mpl20', tol={'aarch64': 0.082}.get(platform.machine(), 0.0)) def test_complete(): + # Remove this line when this test image is regenerated. + plt.rcParams['pcolormesh.snap'] = False + fig = plt.figure('Figure with a label?', figsize=(10, 6)) plt.suptitle('Can you fit any more in a figure?') diff --git a/lib/matplotlib/tests/test_streamplot.py b/lib/matplotlib/tests/test_streamplot.py index d2218f6f288d..95fa7f5cc478 100644 --- a/lib/matplotlib/tests/test_streamplot.py +++ b/lib/matplotlib/tests/test_streamplot.py @@ -42,6 +42,9 @@ def test_startpoints(): @image_comparison(['streamplot_colormap'], tol=.04, remove_text=True, style='mpl20') def test_colormap(): + # Remove this line when this test image is regenerated. + plt.rcParams['pcolormesh.snap'] = False + X, Y, U, V = velocity_field() plt.streamplot(X, Y, U, V, color=U, density=0.6, linewidth=2, cmap=plt.cm.autumn) diff --git a/lib/matplotlib/tests/test_transforms.py b/lib/matplotlib/tests/test_transforms.py index 47f5a90a2b68..512366bb6988 100644 --- a/lib/matplotlib/tests/test_transforms.py +++ b/lib/matplotlib/tests/test_transforms.py @@ -73,6 +73,10 @@ def test_pre_transform_plotting(): # a catch-all for as many as possible plot layouts which handle # pre-transforming the data NOTE: The axis range is important in this # plot. It should be x10 what the data suggests it should be + + # Remove this line when this test image is regenerated. + plt.rcParams['pcolormesh.snap'] = False + ax = plt.axes() times10 = mtransforms.Affine2D().scale(10) diff --git a/lib/mpl_toolkits/tests/test_axes_grid.py b/lib/mpl_toolkits/tests/test_axes_grid.py index 58358b686651..2c2a9e785a96 100644 --- a/lib/mpl_toolkits/tests/test_axes_grid.py +++ b/lib/mpl_toolkits/tests/test_axes_grid.py @@ -16,6 +16,9 @@ @image_comparison(['imagegrid_cbar_mode.png'], remove_text=True, style='mpl20', tol=0.3) def test_imagegrid_cbar_mode_edge(legacy_colorbar): + # Remove this line when this test image is regenerated. + plt.rcParams['pcolormesh.snap'] = False + mpl.rcParams["mpl_toolkits.legacy_colorbar"] = legacy_colorbar X, Y = np.meshgrid(np.linspace(0, 6, 30), np.linspace(0, 6, 30)) diff --git a/lib/mpl_toolkits/tests/test_axisartist_axislines.py b/lib/mpl_toolkits/tests/test_axisartist_axislines.py index 7c2f83ff26ae..172633b0280f 100644 --- a/lib/mpl_toolkits/tests/test_axisartist_axislines.py +++ b/lib/mpl_toolkits/tests/test_axisartist_axislines.py @@ -62,6 +62,8 @@ def test_Axes(): @image_comparison(['ParasiteAxesAuxTrans_meshplot.png'], remove_text=True, style='default', tol=0.075) def test_ParasiteAxesAuxTrans(): + # Remove this line when this test image is regenerated. + plt.rcParams['pcolormesh.snap'] = False data = np.ones((6, 6)) data[2, 2] = 2 diff --git a/lib/mpl_toolkits/tests/test_mplot3d.py b/lib/mpl_toolkits/tests/test_mplot3d.py index 37532335e38d..1738f385cb65 100644 --- a/lib/mpl_toolkits/tests/test_mplot3d.py +++ b/lib/mpl_toolkits/tests/test_mplot3d.py @@ -299,6 +299,9 @@ def test_plot_3d_from_2d(): @mpl3d_image_comparison(['surface3d.png']) def test_surface3d(): + # Remove this line when this test image is regenerated. + plt.rcParams['pcolormesh.snap'] = False + fig = plt.figure() ax = fig.gca(projection='3d') X = np.arange(-5, 5, 0.25) diff --git a/matplotlibrc.template b/matplotlibrc.template index 8ed34d9c1852..ed9e153309ad 100644 --- a/matplotlibrc.template +++ b/matplotlibrc.template @@ -131,6 +131,9 @@ #markers.fillstyle: full # {full, left, right, bottom, top, none} #pcolor.shading : flat +#pcolormesh.snap : True # Whether to snap the mesh to pixel boundaries. This + # is provided solely to allow old test images to remain + # unchanged. Set to False to obtain the previous behavior. ## *************************************************************************** ## * PATCHES * diff --git a/src/_backend_agg.h b/src/_backend_agg.h index cf5a5d350043..8436f6840f24 100644 --- a/src/_backend_agg.h +++ b/src/_backend_agg.h @@ -1160,13 +1160,6 @@ inline void RendererAgg::draw_quad_mesh(GCAgg &gc, array::scalar linewidths(gc.linewidth); array::scalar antialiaseds(antialiased); DashesVector linestyles; - ColorArray *edgecolors_ptr = &edgecolors; - - if (edgecolors.size() == 0) { - if (antialiased) { - edgecolors_ptr = &facecolors; - } - } _draw_path_collection_generic(gc, master_transform, @@ -1178,12 +1171,12 @@ inline void RendererAgg::draw_quad_mesh(GCAgg &gc, offsets, offset_trans, facecolors, - *edgecolors_ptr, + edgecolors, linewidths, linestyles, antialiaseds, OFFSET_POSITION_FIGURE, - false, + true, // check_snap false); }