Skip to content
Draft
Show file tree
Hide file tree
Changes from 1 commit
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
Prev Previous commit
Next Next commit
Fix Agg/Cairo contiguous collections by using an isolated blend group
  • Loading branch information
ayshih committed Oct 9, 2026
commit 4e3fee4a1f0cc07eba6f5c6bf23bc891b15f1e89
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.hexbin()`, `.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.
2 changes: 2 additions & 0 deletions lib/matplotlib/axes/_axes.py
Original file line number Diff line number Diff line change
Expand Up @@ -5830,6 +5830,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 @@ -5911,6 +5912,7 @@ 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")
bar._treat_patches_as_contiguous = True
bar.set_array(values)
bar.set_cmap(cmap)
bar.set_norm(norm)
Expand Down
27 changes: 27 additions & 0 deletions lib/matplotlib/collections.py
Original file line number Diff line number Diff line change
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 @@ -373,6 +379,21 @@ def draw(self, renderer):

transform, offset_trf, offsets, paths = self._prepare_points()

# If supported by the renderer, draw contiguous, antialiased patches without
# strokes using the "plus" blend mode in an isolated blend group so that the
# fractional alphas at edges are added to make the intended 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 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 self._edgecolors[0][3] == 0))
if isolate:
blend_mode = self.get_blend_mode()
self.set_blend_mode("plus")
renderer.open_blend_group(blend_mode)

gc = renderer.new_gc()
self._set_gc_clip(gc)
gc.set_blend_mode(self.get_blend_mode())
Expand Down Expand Up @@ -511,6 +532,9 @@ def draw(self, renderer):
transform.translate(xo, yo)
renderer.draw_path(gc0, path, transform, rgbFace)

if isolate:
renderer.close_blend_group()
self.set_blend_mode(blend_mode)
gc.restore()
renderer.close_group(self.__class__.__name__)
self.stale = False
Expand Down Expand Up @@ -2546,6 +2570,7 @@ def draw(self, renderer):
if not self.get_visible():
return
renderer.open_group(self.__class__.__name__, self.get_gid())

transform = self.get_transform()
offset_trf = self.get_offset_transform()
offsets = self.get_offsets()
Expand Down Expand Up @@ -2655,6 +2680,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
1 change: 1 addition & 0 deletions lib/matplotlib/contour.py
Original file line number Diff line number Diff line change
Expand Up @@ -679,6 +679,7 @@ def __init__(self, ax, *args,
transform=transform,
colorizer=colorizer,
)
self._treat_patches_as_contiguous = True
self.axes = ax
self.levels = levels
self.filled = filled
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
26 changes: 26 additions & 0 deletions lib/matplotlib/tests/test_axes.py
Original file line number Diff line number Diff line change
Expand Up @@ -1507,6 +1507,32 @@ def test_pcolormesh_alpha():
ax4.pcolormesh(Qx, Qy, Z, cmap=cmap, shading='gouraud', zorder=1)


@image_comparison(['pcolormesh_antialiasing.png'], style='mpl20')
def test_pcolormesh_antialiasing():
N = 5
data = np.arange(N**2, dtype=float).reshape((N, N))
data[2, 2] = np.nan

x, y = np.meshgrid(np.arange(N + 1), np.arange(N + 1))

rotation = mtransforms.Affine2D().rotate(1)

fig, axs = plt.subplots(2, 3, figsize=(5, 3.5), layout="constrained")

for i, antialiased in enumerate([None, False, True]):
kwargs = {'cmap': 'jet', 'alpha': 0.5}
if antialiased is not None:
kwargs['antialiased'] = antialiased

axs[0, i].pcolormesh(x, y, data, transform=rotation + axs[0, i].transData,
**kwargs)
axs[1, i].pcolor(x, y, data, transform=rotation + axs[1, i].transData, **kwargs)

for j in range(2):
axs[j, i].set_aspect("equal")
axs[j, i].set_axis_off()


@pytest.mark.parametrize("dims,alpha", [(3, 1), (4, 0.5)])
@check_figures_equal()
def test_pcolormesh_rgba(fig_test, fig_ref, dims, alpha):
Expand Down
20 changes: 20 additions & 0 deletions lib/matplotlib/tests/test_contour.py
Original file line number Diff line number Diff line change
Expand Up @@ -896,3 +896,23 @@ def test_clabel_manual_subset():
cs = ax.contour([[1, 2], [3, 4]], levels=[1.5, 2.5, 3.5])
# Attempt to label only one specific level manually
ax.clabel(cs, levels=[2.5], manual=[(0.5, 0.5)])


@image_comparison(['contourf_antialiasing.png'], style='mpl20')
def test_contourf_antialiasing():
x = np.arange(1, 6)
y = x.reshape(-1, 1)
data = (x * y).astype(float)
data[2, 2] = np.nan

fig, axs = plt.subplots(1, 3, figsize=(5, 2), layout="constrained")

for i, antialiased in enumerate([None, False, True]):
kwargs = {'cmap': 'jet', 'alpha': 0.5}
if antialiased is not None:
kwargs['antialiased'] = antialiased

axs[i].contourf(data, levels=np.arange(1, 25, 1), extend="both", **kwargs)

axs[i].set_aspect("equal")
axs[i].set_axis_off()
71 changes: 54 additions & 17 deletions lib/matplotlib/tests/test_triangulation.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,20 @@
from matplotlib.testing.decorators import image_comparison, check_figures_equal


@pytest.fixture
def sample_triang():
x = np.asarray([0, 0.5, 1, 0, 0.5, 1, 0, 0.5, 1, 0.75])
y = np.asarray([0, 0, 0, 0.5, 0.5, 0.5, 1, 1, 1, 0.75])
triangles = np.asarray([
[0, 1, 3], [1, 4, 3],
[1, 2, 4], [2, 5, 4],
[3, 4, 6], [4, 7, 6],
[4, 5, 9], [7, 4, 9], [8, 7, 9], [5, 8, 9]])

# Triangulation with same number of points and triangles.
return mtri.Triangulation(x, y, triangles)


class TestTriangulationParams:
x = [-1, 0, 1, 0]
y = [0, -1, 0, 1]
Expand Down Expand Up @@ -233,30 +247,19 @@ def tris_contain_point(triang, xy):


@image_comparison(['tripcolor1.png'], style='mpl20')
def test_tripcolor():
x = np.asarray([0, 0.5, 1, 0, 0.5, 1, 0, 0.5, 1, 0.75])
y = np.asarray([0, 0, 0, 0.5, 0.5, 0.5, 1, 1, 1, 0.75])
triangles = np.asarray([
[0, 1, 3], [1, 4, 3],
[1, 2, 4], [2, 5, 4],
[3, 4, 6], [4, 7, 6],
[4, 5, 9], [7, 4, 9], [8, 7, 9], [5, 8, 9]])

# Triangulation with same number of points and triangles.
triang = mtri.Triangulation(x, y, triangles)

Cpoints = x + 0.5*y
def test_tripcolor(sample_triang):
Cpoints = sample_triang.x + 0.5*sample_triang.y

xmid = x[triang.triangles].mean(axis=1)
ymid = y[triang.triangles].mean(axis=1)
xmid = sample_triang.x[sample_triang.triangles].mean(axis=1)
ymid = sample_triang.y[sample_triang.triangles].mean(axis=1)
Cfaces = 0.5*xmid + ymid

plt.subplot(121)
plt.tripcolor(triang, Cpoints, edgecolors='k')
plt.tripcolor(sample_triang, Cpoints, edgecolors='k')
plt.title('point colors')

plt.subplot(122)
plt.tripcolor(triang, facecolors=Cfaces, edgecolors='k')
plt.tripcolor(sample_triang, facecolors=Cfaces, edgecolors='k')
plt.title('facecolors')


Expand Down Expand Up @@ -1403,3 +1406,37 @@ def test_tricontourf_path():
assert_array_almost_equal(paths[0].vertices, expected_vertices)
assert_array_equal(paths[0].codes, [1, 2, 2, 2, 79, 1, 2, 2, 2, 79])
assert_array_almost_equal(paths[0].to_polygons(), np.split(expected_vertices, [5]))


@image_comparison(['tripcolor_antialiasing.png'], style='mpl20')
def test_tripcolor_antialiasing(sample_triang):
data = np.arange(len(sample_triang.triangles))

fig, axs = plt.subplots(1, 3, figsize=(5, 2), layout="constrained")

for i, antialiased in enumerate([None, False, True]):
kwargs = {'cmap': 'inferno', 'alpha': 0.5}
if antialiased is not None:
kwargs['antialiased'] = antialiased

axs[i].tripcolor(sample_triang, data, **kwargs)

axs[i].set_aspect("equal")
axs[i].set_axis_off()


@image_comparison(['tricontourf_antialiasing.png'], style='mpl20')
def test_tricontourf_antialiasing(sample_triang):
data = sample_triang.x + sample_triang.y**2

fig, axs = plt.subplots(1, 3, figsize=(5, 2), layout="constrained")

for i, antialiased in enumerate([None, False, True]):
kwargs = {'cmap': 'inferno', 'alpha': 0.5}
if antialiased is not None:
kwargs['antialiased'] = antialiased

axs[i].tricontourf(sample_triang, data, **kwargs)

axs[i].set_aspect("equal")
axs[i].set_axis_off()
1 change: 1 addition & 0 deletions lib/matplotlib/tri/_tripcolor.py
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,7 @@ def tripcolor(ax, *args, alpha=1.0, norm=None, cmap=None, vmin=None,
colors = facecolors
collection = PolyCollection(verts, alpha=alpha, array=colors,
cmap=cmap, norm=norm, **kwargs)
collection._treat_patches_as_contiguous = True

collection._scale_norm(norm, vmin, vmax)
ax.grid(False)
Expand Down