Fixes Agg/Cairo antialiased rendering of contourf/pcolor/pcolormesh by using a blend group by ayshih · Pull Request #32256 · matplotlib/matplotlib · GitHub
Skip to content

Fixes Agg/Cairo antialiased rendering of contourf/pcolor/pcolormesh by using a blend group - #32256

Draft
ayshih wants to merge 7 commits into
matplotlib:mainfrom
ayshih:fixes_using_blending
Draft

Fixes Agg/Cairo antialiased rendering of contourf/pcolor/pcolormesh by using a blend group#32256
ayshih wants to merge 7 commits into
matplotlib:mainfrom
ayshih:fixes_using_blending

Conversation

@ayshih

@ayshih ayshih commented Aug 28, 2026

Copy link
Copy Markdown
Contributor

PR summary

This PR fixes Agg/Cairo antialiased rendering of contourf/pcolor/pcolormesh by using a blend group (see #31162, where these fixes were separated out of). In the examples below, look at the right-most panel and see how the faint lines between antialiased patches are fixed by this PR. The middle panel shows why it is desirable to turn on antialiasing when alpha < 1.

Since these fixes require #31162, this PR cannot be backported to 3.11.x.

contourf

import matplotlib
#matplotlib.use('TkCairo')

import numpy as np

import matplotlib.pyplot as plt

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()
    axs[i].set_title(f"antialiased={antialiased}" if antialiased is not None else "default")

plt.show()

Before this PR

1 before

After this PR

1 after

pcolor/pcolormesh

import matplotlib
#matplotlib.use('TkCairo')

import numpy as np

import matplotlib.pyplot as plt
from matplotlib.transforms import Affine2D

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 = 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()
    axs[0, i].set_title(f"antialiased={antialiased}" if antialiased is not None else "default")

plt.show()

Before this PR

2 before

After this PR

2 after

AI Disclosure

No AI was used

PR quality check

  • Use an expressive title, e.g. "Fix title font property precedence"
  • New and changed code is tested
  • [N/A] Plotting related features are demonstrated in an example
  • New features and API changes have release notes
  • [N/A] Documentation complies with general and docstring guidelines

@QuLogic

QuLogic commented Aug 29, 2026

Copy link
Copy Markdown
Member

@iccir

iccir commented Aug 29, 2026

Copy link
Copy Markdown
Contributor

Can you explain what exactly is happening and why it's fixed via a plus blend mode? I think that may have gotten buried in the other PR.

My first reaction was "this makes basic sense, but I need to look up the formula for Plus again."

@iccir iccir left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Maybe add a comment about why "plus" was chosen, but there are also existing comments in the source base about this + intermediate buffers.

@rcomer

rcomer commented Aug 29, 2026

Copy link
Copy Markdown
Member

The pcolor docstring states

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.

Those artificial lines are now gone, so this explanation no longer makes sense. Should we change the default? The default for pcolormesh and contourfis alsoFalse, but I have not found statements about why for those.

@iccir

iccir commented Aug 29, 2026

Copy link
Copy Markdown
Contributor

Those artificial lines are now gone, so this explanation no longer makes sense. Should we change the default? The default for pcolormesh and contourfis alsoFalse, but I have not found statements about why for those.

I'm +1 on "default to the more-correct-looking option", unless there are other concerns.

@ayshih

ayshih commented Aug 31, 2026

Copy link
Copy Markdown
Contributor Author

Can you explain what exactly is happening and why it's fixed via a plus blend mode?

It's because the alpha at the edge of an antialiased patch represents how much the edge fills the pixel. When two patches are exactly adjacent and should fill the pixel entirely to a resulting alpha_combined, the alphas ought to be added up (alpha_1 + alpha_2 = alpha_combined). However, standard blend modes treat the alphas as unrelated opacities, so the resulting blended alpha is alpha_1 + alpha_2 - alpha_1 * alpha_2, which is less than alpha_combined. That's why the existing code produces lighter lines at patch edges; the alpha is smaller than it should be. So, the "plus" blend mode, which is actually a Porter-Duff compositing operator, is exactly what is needed to get the correct result.

Maybe add a comment about why "plus" was chosen, but there are also existing comments in the source base about this + intermediate buffers.

I've added (fairly wordy) comments in the code, which should help future maintainers.

Would it make sense/work to apply this to all Collections?

No. I believe these are the only collections where the patches are expressly constructed to be abutting with no overlaps. A collection in general could have overlapping patches, and the "plus" blend mode would give a bad result.

Those artificial lines are now gone, so this explanation no longer makes sense. Should we change the default? The default for pcolormesh and contourfis alsoFalse, but I have not found statements about why for those.

I've changed all three so they no longer forcefully disable antialiasing by default, and now the default is simply rcParams["patch.antialiased"].

@ayshih
ayshih force-pushed the fixes_using_blending branch from f796bc6 to 0f876cf Compare August 31, 2026 12:12
@rcomer

rcomer commented Aug 31, 2026

Copy link
Copy Markdown
Member

Would it make sense/work to apply this to all Collections?

No. I believe these are the only collections where the patches are expressly constructed to be abutting with no overlaps. A collection in general could have overlapping patches, and the "plus" blend mode would give a bad result.

Is there a way that downstream libraries could take advantage of this functionality for their own collections? For example, Cartopy has a collection that enables drawing choropleth maps.
https://cartopy.readthedocs.io/stable/gallery/scalar_data/geometry_data.html

@ayshih
ayshih marked this pull request as draft August 31, 2026 15:17
@ayshih

ayshih commented Aug 31, 2026

Copy link
Copy Markdown
Contributor Author

@iccir
iccir self-requested a review August 31, 2026 17:54
@ayshih
ayshih force-pushed the fixes_using_blending branch 2 times, most recently from b34d542 to 5d84818 Compare September 1, 2026 04:02
@ayshih
ayshih force-pushed the fixes_using_blending branch 2 times, most recently from 1f7e0d7 to d4e1f1c Compare September 1, 2026 13:42
@ayshih
ayshih force-pushed the fixes_using_blending branch 5 times, most recently from 714776f to ce3f7d7 Compare September 4, 2026 20:31
@ayshih
ayshih force-pushed the fixes_using_blending branch from 4b5f5ca to 32b1c1c Compare September 5, 2026 05:55
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants