Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions doc/api/next_api_changes/behavior/32256-AYS.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
Better antialiasing default for contiguous filled patches
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
The plotting methods that produce a collection of contiguous filled patches
(`.Axes.contourf()`, `.Axes.pcolor()`, `.Axes.pcolormesh()`,
`.Axes.tricontourf()`, and `.Axes.tripcolor()`) would previously default to no
antialiasing when no edgecolor was specified to avoid a rendering artifact of
faint lines between patches. That rendering artifact has now been fixed.
These methods now use the value of rcParams ``patch.antialiased`` as the default
for antialiasing.
35 changes: 9 additions & 26 deletions lib/matplotlib/axes/_axes.py
Original file line number Diff line number Diff line change
Expand Up @@ -5534,7 +5534,7 @@ def scatter(self, x, y, s=None, c=None, marker=None, cmap=None, norm=None,
def hexbin(self, x, y, C=None, gridsize=100, bins=None,
xscale='linear', yscale='linear', extent=None,
cmap=None, norm=None, vmin=None, vmax=None,
alpha=None, linewidths=None, edgecolors='face',
alpha=None, linewidths=None, edgecolors='none',
reduce_C_function=np.mean, mincnt=None, marginals=False,
colorizer=None, **kwargs):
"""
Expand Down Expand Up @@ -5654,12 +5654,11 @@ def hexbin(self, x, y, C=None, gridsize=100, bins=None,
linewidths : float, default: *None*
If *None*, defaults to :rc:`patch.linewidth`.

edgecolors : {'face', 'none', *None*} or color, default: 'face'
edgecolors : {'face', 'none', *None*} or color, default: 'none'
The color of the hexagon edges. Possible values are:

- 'face': Draw the edges in the same color as the fill color.
- 'none': No edges are drawn. This can sometimes lead to unsightly
unpainted pixels between the hexagons.
- 'none': No edges are drawn.
- *None*: Draw outlines in the default color.
- An explicit color.

Expand Down Expand Up @@ -5833,6 +5832,7 @@ def reduce_C_function(C: array) -> float
offsets=offsets,
offset_transform=mtransforms.AffineDeltaTransform(self.transData)
)
collection._treat_patches_as_contiguous = True

# Set normalizer if bins is 'log'
if cbook._str_equal(bins, 'log'):
Expand Down Expand Up @@ -5913,7 +5913,8 @@ def reduce_C_function(C: array) -> float

trans = getattr(self, f"get_{zname}axis_transform")(which="grid")
bar = mcoll.PolyCollection(
verts, transform=trans, edgecolors="face")
verts, transform=trans, edgecolors="none")
bar._treat_patches_as_contiguous = True
bar.set_array(values)
bar.set_cmap(cmap)
bar.set_norm(norm)
Expand Down Expand Up @@ -6693,15 +6694,6 @@ def pcolor(self, *args, shading=None, alpha=None, norm=None, cmap=None,

Other Parameters
----------------
antialiaseds : bool, default: False
The default *antialiaseds* is False if the default
*edgecolors*\ ="none" is used. This eliminates artificial lines
at patch boundaries, and works regardless of the value of alpha.
If *edgecolors* is not "none", then the default *antialiaseds*
is taken from :rc:`patch.antialiased`.
Stroking the edges may be preferred if *alpha* is 1, but will
cause artifacts otherwise.

data : indexable object, optional
DATA_PARAMETER_PLACEHOLDER

Expand Down Expand Up @@ -6761,15 +6753,6 @@ def pcolor(self, *args, shading=None, alpha=None, norm=None, cmap=None,
kwargs['edgecolors'] = kwargs.pop('edgecolor')
ec = kwargs.setdefault('edgecolors', 'none')

# aa setting will default via collections to patch.antialiased
# unless the boundary is not stroked, in which case the
# default will be False; with unstroked boundaries, aa
# makes artifacts that are often disturbing.
if 'antialiaseds' in kwargs:
kwargs['antialiased'] = kwargs.pop('antialiaseds')
if 'antialiased' not in kwargs and cbook._str_lower_equal(ec, "none"):
kwargs['antialiased'] = False

kwargs.setdefault('snap', False)

if np.ma.isMaskedArray(X) or np.ma.isMaskedArray(Y):
Expand All @@ -6796,7 +6779,7 @@ def pcolor(self, *args, shading=None, alpha=None, norm=None, cmap=None,
@_preprocess_data()
@_docstring.interpd
def pcolormesh(self, *args, alpha=None, norm=None, cmap=None, vmin=None,
vmax=None, colorizer=None, shading=None, antialiased=False,
vmax=None, colorizer=None, shading=None,
**kwargs):
"""
Create a pseudocolor plot with a non-regular rectangular grid.
Expand Down Expand Up @@ -7007,7 +6990,7 @@ def pcolormesh(self, *args, alpha=None, norm=None, cmap=None, vmin=None,
kwargs.setdefault('snap', mpl.rcParams['pcolormesh.snap'])

collection = mcoll.QuadMesh(
coords, antialiased=antialiased, shading=shading,
coords, shading=shading,
array=C, colorizer=colorizer, alpha=alpha, **kwargs)
collection._scale_norm(norm, vmin, vmax)

Expand Down Expand Up @@ -7206,7 +7189,7 @@ def pcolorfast(self, *args, alpha=None, norm=None, cmap=None, vmin=None,
collection = mcoll.QuadMesh(
coords, array=C,
alpha=alpha, cmap=cmap, norm=norm, colorizer=colorizer,
antialiased=False, edgecolors="none")
edgecolors="none")
self.add_collection(collection, autolim=False)
xl, xr, yb, yt = x.min(), x.max(), y.min(), y.max()
ret = collection
Expand Down
2 changes: 2 additions & 0 deletions lib/matplotlib/backend_bases.py
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,8 @@ class RendererBase:
* `draw_path_collection`
* `draw_quad_mesh`
"""
_supports_isolated_group_and_plus_blend_mode = False

def __init__(self):
super().__init__()
self._texmanager = None
Expand Down
1 change: 1 addition & 0 deletions lib/matplotlib/backends/backend_agg.py
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ class RendererAgg(RendererBase):
The renderer handles all the drawing primitives using a graphics
context instance that controls the colors/styles
"""
_supports_isolated_group_and_plus_blend_mode = True

def __init__(self, width, height, dpi):
super().__init__()
Expand Down
28 changes: 24 additions & 4 deletions lib/matplotlib/backends/backend_cairo.py
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,8 @@ def attr(field):


class RendererCairo(RendererBase):
_supports_isolated_group_and_plus_blend_mode = True

def __init__(self, dpi):
self.dpi = dpi
self.gc = GraphicsContextCairo(renderer=self)
Expand Down Expand Up @@ -375,13 +377,31 @@ def close_blend_group(self):
if group_state.blend_mode is not None:
ctx = self.gc.ctx
group = ctx.pop_group()

ctx.save()
self.gc.set_blend_mode(group_state.blend_mode)
ctx.set_source(group)
if group_state.alpha != 1:
ctx.paint_with_alpha(group_state.alpha)

if group_state.blend_mode in {'knockout', 'clear'}:
mask_surface = ctx.get_target().create_similar_image(
cairo.FORMAT_A1, self.width, self.height)
mask_ctx = cairo.Context(mask_surface)
mask_ctx.set_source(group)
mask_ctx.paint()
mask_pattern = cairo.SurfacePattern(mask_surface)

group_surface = ctx.get_target().create_similar_image(
cairo.FORMAT_ARGB32, self.width, self.height)
group_ctx = cairo.Context(group_surface)
group_ctx.set_source(group)
group_ctx.paint_with_alpha(group_state.alpha)

ctx.set_source_surface(group_surface)
ctx.mask(mask_pattern)

else:
ctx.paint()
ctx.set_source(group)
ctx.paint_with_alpha(group_state.alpha)

ctx.restore()


Expand Down
108 changes: 74 additions & 34 deletions lib/matplotlib/collections.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
they are meant to be fast for common use cases (e.g., a large set of solid
line segments).
"""

import contextlib
import itertools
import functools
import math
Expand Down Expand Up @@ -173,6 +173,12 @@ def __init__(self, *,
self._us_lw = [0]
self._linewidths = [0]

# For drawing, indicates whether the collection elements that share an edge
# are to be treated as contiguous (e.g., the output of pcolor) as opposed to
# merely happenstance. This should not be set to True if the collection
# elements may overlap.
self._treat_patches_as_contiguous = False

self._gapcolor = None # Currently only used by LineCollection.

# Flags set by _set_mappable_flags: are colors from mapping an array?
Expand Down Expand Up @@ -357,6 +363,32 @@ def _prepare_points(self):

return transform, offset_trf, offsets, paths

@contextlib.contextmanager
def _prep_for_contiguous_drawing(self, renderer, gc):
# Context manager to wrap the specific rendering commands within draw()

# If supported by the renderer, draw contiguous patches (must be antialiased
# and not stroked) using the "plus" blend mode in an isolated blend group so
# that the fractional alphas at edges are added to make the proper total alpha.
# If the alphas were instead blended as if they were unrelated opacities,
# the resulting alpha would be smaller (i.e., more transparent), which would
# manifest as slightly transparent line artifacts along shared patch edges.
isolate = (renderer._supports_isolated_group_and_plus_blend_mode
and self._treat_patches_as_contiguous
and np.all(self._antialiaseds)
and (self._edgecolors.size == 0
or np.all(self._edgecolors[:, 3] == 0)))
if isolate:
blend_mode = gc.get_blend_mode()
gc.set_blend_mode("plus")
renderer.open_blend_group(blend_mode)
try:
yield
finally:
if isolate:
renderer.close_blend_group()
gc.set_blend_mode(blend_mode)

@artist.allow_rasterization
def draw(self, renderer):
if not self.get_visible():
Expand Down Expand Up @@ -426,9 +458,10 @@ def draw(self, renderer):
gc.set_dashes(*self._linestyles[0])
gc.set_antialiased(self._antialiaseds[0])
gc.set_url(self._urls[0])
renderer.draw_markers(
gc, paths[0], combined_transform.frozen(),
mpath.Path(offsets), offset_trf, tuple(facecolors[0]))
with self._prep_for_contiguous_drawing(renderer, gc):
renderer.draw_markers(
gc, paths[0], combined_transform.frozen(),
mpath.Path(offsets), offset_trf, tuple(facecolors[0]))
else:
# The current new API of draw_path_collection() is provisional
# and will be changed in a future PR.
Expand Down Expand Up @@ -491,25 +524,27 @@ def draw(self, renderer):
self._linewidths, self._linestyles, self._antialiaseds, self._urls,
"screen"]

if hatchcolors_arg_supported:
renderer.draw_path_collection(gc, transform.frozen(), paths,
self.get_transforms(), *args,
hatchcolors=self.get_hatchcolor())
else:
if hatchcolors_not_needed:
with self._prep_for_contiguous_drawing(renderer, gc):
if hatchcolors_arg_supported:
renderer.draw_path_collection(gc, transform.frozen(), paths,
self.get_transforms(), *args)
self.get_transforms(), *args,
hatchcolors=self.get_hatchcolor())
else:
path_ids = renderer._iter_collection_raw_paths(
transform.frozen(), paths, self.get_transforms())
for xo, yo, path_id, gc0, rgbFace in renderer._iter_collection(
gc, list(path_ids), *args, hatchcolors=self.get_hatchcolor(),
):
path, transform = path_id
if xo != 0 or yo != 0:
transform = transform.frozen()
transform.translate(xo, yo)
renderer.draw_path(gc0, path, transform, rgbFace)
if hatchcolors_not_needed:
renderer.draw_path_collection(gc, transform.frozen(), paths,
self.get_transforms(), *args)
else:
path_ids = renderer._iter_collection_raw_paths(
transform.frozen(), paths, self.get_transforms())
for xo, yo, path_id, gc0, rgbFace in renderer._iter_collection(
gc, list(path_ids), *args,
hatchcolors=self.get_hatchcolor(),
):
path, transform = path_id
if xo != 0 or yo != 0:
transform = transform.frozen()
transform.translate(xo, yo)
renderer.draw_path(gc0, path, transform, rgbFace)

gc.restore()
renderer.close_group(self.__class__.__name__)
Expand Down Expand Up @@ -2524,11 +2559,13 @@ def __init__(self, coordinates, *, antialiased=True, shading='flat',
super().__init__(coordinates=coordinates, shading=shading)
Collection.__init__(self, **kwargs)

self._antialiased = antialiased
self._antialiaseds = antialiased
self._bbox = transforms.Bbox.unit()
self._bbox.update_from_data_xy(self._coordinates.reshape(-1, 2))
self.set_mouseover(False)

self._treat_patches_as_contiguous = True

def get_paths(self):
if self._paths is None:
self.set_paths()
Expand Down Expand Up @@ -2575,18 +2612,19 @@ def draw(self, renderer):
gc.set_blend_mode(self.get_blend_mode())
gc.set_linewidth(self.get_linewidth()[0])

if self._shading == 'gouraud':
triangles, colors = self._convert_mesh_to_triangles(coordinates)
renderer.draw_gouraud_triangles(
gc, triangles, colors, transform.frozen())
else:
renderer.draw_quad_mesh(
gc, transform.frozen(),
coordinates.shape[1] - 1, coordinates.shape[0] - 1,
coordinates, offsets, offset_trf,
# Backends expect flattened rgba arrays (n*m, 4) for fc and ec
self.get_facecolor().reshape((-1, 4)),
self._antialiased, self.get_edgecolors().reshape((-1, 4)))
with self._prep_for_contiguous_drawing(renderer, gc):
if self._shading == 'gouraud':
triangles, colors = self._convert_mesh_to_triangles(coordinates)
renderer.draw_gouraud_triangles(
gc, triangles, colors, transform.frozen())
else:
renderer.draw_quad_mesh(
gc, transform.frozen(),
coordinates.shape[1] - 1, coordinates.shape[0] - 1,
coordinates, offsets, offset_trf,
# Backends expect flattened rgba arrays (n*m, 4) for fc and ec
self.get_facecolor().reshape((-1, 4)),
self._antialiaseds, self.get_edgecolors().reshape((-1, 4)))
gc.restore()
renderer.close_group(self.__class__.__name__)
self.stale = False
Expand Down Expand Up @@ -2639,6 +2677,8 @@ def __init__(self, coordinates, **kwargs):
# have all been processed and available for the masking calculations
self._set_unmasked_verts()

self._treat_patches_as_contiguous = True

def _get_unmasked_polys(self):
"""Get the unmasked regions using the coordinates and array"""
# mask(X) | mask(Y)
Expand Down
Loading
Loading